From 6ccb716c9c1550e328f4adbd3ef20d72b7e4b36c Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Fri, 5 Feb 2021 08:59:41 -0800 Subject: [PATCH 01/44] [Alerting UI] Added EuiThemeProvider as an application wrapper for triggers_actions_ui (#90312) --- .../public/application/app.tsx | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx index 85f3818484a13..0a59cff98ce26 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx @@ -10,6 +10,7 @@ import { Switch, Route, Redirect, Router } from 'react-router-dom'; import { ChromeBreadcrumb, CoreStart, ScopedHistory } from 'kibana/public'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; +import useObservable from 'react-use/lib/useObservable'; import { KibanaFeature } from '../../../features/common'; import { Section, routeToAlertDetails } from './constants'; import { ActionTypeRegistryContract, AlertTypeRegistryContract } from '../types'; @@ -18,6 +19,7 @@ import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { PluginStartContract as AlertingStart } from '../../../alerts/public'; import { suspendedComponentWithProps } from './lib/suspended_component_with_props'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; +import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; import { setSavedObjectsClient } from '../common/lib/data_apis'; import { KibanaContextProvider } from '../common/lib/kibana'; @@ -41,25 +43,31 @@ export interface TriggersAndActionsUiServices extends CoreStart { } export const renderApp = (deps: TriggersAndActionsUiServices) => { - const { element, savedObjects } = deps; + const { element } = deps; + render(, element); + return () => { + unmountComponentAtNode(element); + }; +}; + +export const App = ({ deps }: { deps: TriggersAndActionsUiServices }) => { + const { savedObjects, uiSettings } = deps; const sections: Section[] = ['alerts', 'connectors']; + const isDarkMode = useObservable(uiSettings.get$('theme:darkMode')); const sectionsRegex = sections.join('|'); setSavedObjectsClient(savedObjects.client); - - render( + return ( - - - - - - , - element + + + + + + + + ); - return () => { - unmountComponentAtNode(element); - }; }; export const AppWithoutRouter = ({ sectionsRegex }: { sectionsRegex: string }) => { From 3166ff3761ba306fa254f2d8bf6049d46267433a Mon Sep 17 00:00:00 2001 From: Constance Date: Fri, 5 Feb 2021 09:01:01 -0800 Subject: [PATCH 02/44] [Enterprise Search] eslint rule update: react/jsx-boolean-value (#90345) * [Setup] Split rule that explicitly allows `any` in test/mock files into its own section - so that the rules we're about to add apply correctly to all files * Add react/jsx-boolean-value rule * Run --fix Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .eslintrc.js | 10 +++++++++- .../components/credentials/credentials.tsx | 2 +- .../credentials/credentials_flyout/footer.tsx | 2 +- .../credentials/credentials_flyout/header.tsx | 2 +- .../credentials/credentials_flyout/index.tsx | 4 ++-- .../credentials_list/credentials_list.test.tsx | 2 +- .../documents/document_creation_button.tsx | 2 +- .../search_experience/customization_modal.tsx | 8 ++++---- .../search_experience/search_experience.tsx | 2 +- .../views/multi_checkbox_facets_view.tsx | 2 +- .../search_experience/views/result_view.test.tsx | 2 +- .../search_experience/views/result_view.tsx | 2 +- .../app_search/components/engine/engine_nav.tsx | 4 ++-- .../app_search/components/library/library.tsx | 2 +- .../app_search/components/result/result.test.tsx | 6 +++--- .../components/result/result_header.test.tsx | 8 ++++---- .../public/applications/app_search/index.test.tsx | 2 +- .../components/product_card/product_card.tsx | 2 +- .../applications/shared/hidden_text/hidden_text.tsx | 2 +- .../shared/indexing_status/indexing_status.tsx | 2 +- .../applications/shared/layout/layout.test.tsx | 2 +- .../react_router_helpers/eui_components.test.tsx | 2 +- .../shared/schema/schema_add_field_modal.tsx | 10 +++++----- .../shared/credential_item/credential_item.tsx | 12 +++--------- .../applications/workplace_search/index.test.tsx | 2 +- .../components/add_source/add_source_header.tsx | 2 +- .../components/add_source/add_source_list.tsx | 2 +- .../components/display_settings/display_settings.tsx | 2 +- .../display_settings/field_editor_modal.tsx | 8 ++++---- .../components/display_settings/result_detail.tsx | 4 ++-- .../components/display_settings/search_results.tsx | 12 ++++++------ .../content_sources/components/schema/schema.tsx | 4 ++-- .../components/schema/schema_fields_table.tsx | 2 +- .../views/content_sources/private_sources.tsx | 2 +- .../views/groups/components/add_group_modal.tsx | 2 +- .../groups/components/filterable_users_popover.tsx | 4 ++-- .../components/group_source_prioritization.tsx | 2 +- .../views/groups/components/groups_table.tsx | 2 +- .../components/table_filter_users_dropdown.tsx | 2 +- .../views/groups/components/table_filters.tsx | 2 +- .../workplace_search/views/groups/groups.tsx | 2 +- 41 files changed, 76 insertions(+), 74 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index dadebc922df9e..e85792c4f4ba6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1114,10 +1114,18 @@ module.exports = { * Enterprise Search overrides */ { + // All files files: ['x-pack/plugins/enterprise_search/**/*.{ts,tsx}'], - excludedFiles: ['x-pack/plugins/enterprise_search/**/*.{test,mock}.{ts,tsx}'], rules: { 'react-hooks/exhaustive-deps': 'off', + 'react/jsx-boolean-value': ['error', 'never'], + }, + }, + { + // Source files only - allow `any` in test/mock files + files: ['x-pack/plugins/enterprise_search/**/*.{ts,tsx}'], + excludedFiles: ['x-pack/plugins/enterprise_search/**/*.{test,mock}.{ts,tsx}'], + rules: { '@typescript-eslint/no-explicit-any': 'error', }, }, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx index e21a01d2b97ec..0266b64f97104 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx @@ -106,7 +106,7 @@ export const Credentials: React.FC = () => { showCredentialsForm()} > {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.createKey', { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.tsx index 68fae9d942e9d..dc2d52a073b36 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.tsx @@ -35,7 +35,7 @@ export const CredentialsFlyoutFooter: React.FC = () => { { const { activeApiToken } = useValues(CredentialsLogic); return ( - +

{activeApiToken.id diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.tsx index 1e0c2d3eb822c..1335a3cdeea18 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.tsx @@ -22,8 +22,8 @@ export const CredentialsFlyout: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx index 3c3f02106fe12..dd3d8ef8069ba 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx @@ -207,7 +207,7 @@ describe('Credentials', () => { isHidden: expect.any(Boolean), text: ( - ••••••• + ••••••• ), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.tsx index b26a244397cba..a05005fefa082 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.tsx @@ -19,7 +19,7 @@ export const DocumentCreationButton: React.FC = () => { return ( <> = ({ defaultMessage: 'Filter fields', } )} - fullWidth={true} + fullWidth helpText={i18n.translate( 'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.filterFields', { @@ -93,7 +93,7 @@ export const CustomizationModal: React.FC = ({ > = ({ defaultMessage: 'Sort fields', } )} - fullWidth={true} + fullWidth helpText={i18n.translate( 'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.sortFields', { @@ -117,7 +117,7 @@ export const CustomizationModal: React.FC = ({ > {
= ({ options={checkboxGroupOptions} idToSelectedMap={idToSelectedMap} onChange={onChange} - compressed={true} + compressed /> {showMore && ( <> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.test.tsx index 0eb0861ee3b02..e06603894c288 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.test.tsx @@ -34,7 +34,7 @@ describe('ResultView', () => { it('renders', () => { const wrapper = shallow( - + ); expect(wrapper.find(Result).props()).toEqual({ result, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.tsx index 441216f75a40c..9dd3fcea5f754 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.tsx @@ -22,7 +22,7 @@ export const ResultView: React.FC = ({ result, schemaForTypeHighlights, i
  • diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx index a828747788f77..b1b31c245eb99 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx @@ -105,7 +105,7 @@ export const EngineNav: React.FC = () => { {canViewEngineAnalytics && ( {ANALYTICS_TITLE} @@ -114,7 +114,7 @@ export const EngineNav: React.FC = () => { {canViewEngineDocuments && ( {DOCUMENTS_TITLE} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx index 9d7b05e68baf4..2d39b5a9aa05c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx @@ -198,7 +198,7 @@ export const Library: React.FC = () => {

    With a link

    - + diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx index cbec65ec9f884..0c3749d1ccb3d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx @@ -62,7 +62,7 @@ describe('Result', () => { }); it('passes showScore, resultMeta, and isMetaEngine to ResultHeader', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(ResultHeader).props()).toEqual({ isMetaEngine: true, showScore: true, @@ -76,7 +76,7 @@ describe('Result', () => { describe('document detail link', () => { it('will render a link if shouldLinkToDetailPage is true', () => { - const wrapper = shallow(); + const wrapper = shallow(); wrapper.find(ReactRouterHelper).forEach((link) => { expect(link.prop('to')).toEqual('/engines/my-engine/documents/1'); }); @@ -96,7 +96,7 @@ describe('Result', () => { it('will render field details with type highlights if schemaForTypeHighlights has been provided', () => { const wrapper = shallow( - + ); expect(wrapper.find(ResultField).map((rf) => rf.prop('type'))).toEqual([ 'text', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx index 1e7be7027f7b3..9d90b3ae35a8f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx @@ -34,7 +34,7 @@ describe('ResultHeader', () => { describe('score', () => { it('renders score if showScore is true ', () => { const wrapper = shallow( - + ); expect(wrapper.find('[data-test-subj="ResultScore"]').prop('value')).toEqual(100); }); @@ -51,12 +51,12 @@ describe('ResultHeader', () => { it('renders engine name if this is a meta engine', () => { const wrapper = shallow( ); expect(wrapper.find('[data-test-subj="ResultEngine"]').prop('value')).toBe('my-engine'); @@ -65,7 +65,7 @@ describe('ResultHeader', () => { it('does not render an engine if this is not a meta engine', () => { const wrapper = shallow( { const initializeAppData = jest.fn(); setMockActions({ initializeAppData }); - shallow(); + shallow(); expect(initializeAppData).toHaveBeenCalledWith({ ilmEnabled: true }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx index d4e879ebc11ce..162ea7f427306 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx @@ -67,7 +67,7 @@ export const ProductCard: React.FC = ({ product, image }) => { sendEnterpriseSearchTelemetry({ action: 'clicked', diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx index 1886afb468404..5503baf0bdae4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx @@ -27,7 +27,7 @@ export const HiddenText: React.FC = ({ text, children }) => { }); const hiddenText = isHidden ? ( - {text.replace(/./g, '•')} + {text.replace(/./g, '•')} ) : ( text diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx index 6bcdc9623cb91..3898eda126415 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx @@ -41,7 +41,7 @@ export const IndexingStatus: React.FC = ({ return ( <> {percentageComplete < 100 && ( - + )} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx index 3f6d4e781cda1..c67518e977de2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx @@ -57,7 +57,7 @@ describe('Layout', () => { }); it('renders a read-only mode callout', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(EuiCallOut)).toHaveLength(1); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.test.tsx index f9269e425f84a..4de43ce997b48 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.test.tsx @@ -53,7 +53,7 @@ describe('EUI & React Router Component Helpers', () => { }); it('passes down all ...rest props', () => { - const wrapper = shallow(); + const wrapper = shallow(); const link = wrapper.find(EuiLink); expect(link.prop('external')).toEqual(true); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.tsx index 1ef665a52c782..bbde6c5d3b55d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.tsx @@ -94,7 +94,7 @@ export const SchemaAddFieldModal: React.FC = ({ = ({ placeholder="name" type="text" onChange={handleChange} - required={true} + required value={rawFieldName} - fullWidth={true} - autoFocus={true} + fullWidth + autoFocus isLoading={loading} data-test-subj="SchemaAddFieldNameField" /> @@ -132,7 +132,7 @@ export const SchemaAddFieldModal: React.FC = ({ {FIELD_NAME_MODAL_CANCEL} = ({ {!isVisible ? ( - + ) : ( )} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index 2b09babbb03fc..73ee7662888bb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -63,7 +63,7 @@ describe('WorkplaceSearchConfigured', () => { }); it('initializes app data with passed props', () => { - shallow(); + shallow(); expect(initializeAppData).toHaveBeenCalledWith({ isFederatedAuth: true }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx index 39c432eb27491..f12c24feb8e1a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx @@ -36,7 +36,7 @@ export const AddSourceHeader: React.FC = ({ diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx index 0dd3850b86de8..3a0db0f44047d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx @@ -109,7 +109,7 @@ export const AddSourceList: React.FC = () => { data-test-subj="FilterSourcesInput" value={filterValue} onChange={handleFilterChange} - fullWidth={true} + fullWidth placeholder={ADD_SOURCE_PLACEHOLDER} /> diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx index bc697a39984c0..62beb4e40793b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx @@ -118,7 +118,7 @@ export const DisplaySettings: React.FC = ({ tabId }) => { description={DISPLAY_SETTINGS_DESCRIPTION} action={ hasDocuments ? ( - + {SAVE_BUTTON} ) : null diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx index 6171bddbd1527..9a6af035c1c8d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx @@ -73,9 +73,9 @@ export const FieldEditorModal: React.FC = () => { { setLabel(e.target.value)} @@ -95,7 +95,7 @@ export const FieldEditorModal: React.FC = () => { {CANCEL_BUTTON} - + {ACTION_LABEL} {FIELD_LABEL} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx index 3930768628aba..8382ddc9e82b3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx @@ -79,7 +79,7 @@ export const ResultDetail: React.FC = () => { <> {detailFields.map(({ fieldName, label }, index) => ( @@ -87,7 +87,7 @@ export const ResultDetail: React.FC = () => { key={`${fieldName}-${index}`} index={index} draggableId={`${fieldName}-${index}`} - customDragHandle={true} + customDragHandle spacing="m" > {(provided) => ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx index f7491ae8778c3..b2ba2b13e5ec3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx @@ -76,10 +76,10 @@ export const SearchResults: React.FC = () => { > setTitleField(e.target.value)} @@ -88,9 +88,9 @@ export const SearchResults: React.FC = () => { setUrlField(e.target.value)} @@ -110,7 +110,7 @@ export const SearchResults: React.FC = () => { @@ -129,7 +129,7 @@ export const SearchResults: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx index 936dceba89e56..fe48e1c14ff41 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx @@ -122,7 +122,7 @@ export const Schema: React.FC = () => { {addFieldButton} {percentageComplete < 100 ? ( - + {SCHEMA_UPDATING} ) : ( @@ -130,7 +130,7 @@ export const Schema: React.FC = () => { disabled={formUnchanged} data-test-subj="UpdateTypesButton" onClick={updateFields} - fill={true} + fill > {SCHEMA_SAVE_BUTTON} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx index d93bafe6b972e..a683d9384f636 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx @@ -57,7 +57,7 @@ export const SchemaFieldsTable: React.FC = () => { disabled={fieldName === 'id'} key={fieldName} fieldName={fieldName} - hideName={true} + hideName fieldType={filteredSchemaFields[fieldName]} updateExistingFieldType={updateExistingFieldType} /> diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx index 6dcc4379515a3..d68b451ffa6f5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx @@ -109,7 +109,7 @@ export const PrivateSources: React.FC = () => { const privateSourcesTable = ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx index b19003e431ee5..f49c978d06e90 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx @@ -73,7 +73,7 @@ export const AddGroupModal: React.FC<{}> = () => { {ADD_GROUP_SUBMIT} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.tsx index 6cba9fcb509ea..b47232197c47f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.tsx @@ -41,7 +41,7 @@ export const FilterableUsersPopover: React.FC = ({ return ( = ({ addFilteredUser={addFilteredUser} allGroupUsersLoading={allGroupUsersLoading} removeFilteredUser={removeFilteredUser} - isPopover={true} + isPopover /> diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.tsx index 4fb9350d0b362..6907618e40b46 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.tsx @@ -96,7 +96,7 @@ export const GroupSourcePrioritization: React.FC = () => { {HEADER_ACTION_TEXT} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx index ff596e41f5538..31f549c3e2065 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx @@ -91,7 +91,7 @@ export const GroupsTable: React.FC<{}> = () => { - {showPagination && } + {showPagination && } ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.tsx index 49dc3bfa671d9..9ddb955767c14 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.tsx @@ -44,7 +44,7 @@ export const TableFilterUsersDropdown: React.FC<{}> = () => { { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx index 144aaabba407d..7a8b9343691f9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx @@ -86,7 +86,7 @@ export const Groups: React.FC = () => { const headerAction = ( - + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.groups.addGroupForm.action', { defaultMessage: 'Create a group', })} From db899a92740b7f4de7488eb1f5cef9f89442a28a Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Fri, 5 Feb 2021 10:14:58 -0700 Subject: [PATCH 03/44] Upgrade EUI to v31.4.0 (#89648) * Bump EUI to v31.4.0 * fix datagrid functional tests * fix Lens unit tests * fix table cell filter test * Fix discover grid doc view test * stabilize data table tests * fix dashboard embeddable datagrid test * Fix x-pack functional tests * fix ml accessibility tests * Fix discover grid context test * Adapt expected nr of documents being displayed * stabilize Lens a11y tests and skip data table * Fix 2 ml functional tests * enable lens datatable test; disable axe rule for datatable * fix ml test * fix Lens table test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Joe Reuter Co-authored-by: Matthias Wilhelm Co-authored-by: Michail Yasonik --- package.json | 2 +- .../services/a11y/analyze_with_axe.js | 4 + .../apps/dashboard/embeddable_data_grid.ts | 7 +- .../apps/discover/_data_grid_context.ts | 1 + .../apps/discover/_data_grid_doc_table.ts | 6 +- .../apps/discover/_data_grid_field_data.ts | 7 +- test/functional/apps/visualize/_data_table.ts | 18 +-- .../_data_table_notimeindex_filters.ts | 15 ++- .../apps/visualize/_embedding_chart.ts | 35 +++--- .../page_objects/visualize_chart_page.ts | 2 +- test/functional/services/data_grid.ts | 108 +++++++++--------- .../components/table_basic.test.tsx | 10 +- x-pack/test/accessibility/apps/lens.ts | 4 +- x-pack/test/accessibility/apps/ml.ts | 12 +- .../test/functional/apps/lens/smokescreen.ts | 17 +-- x-pack/test/functional/apps/lens/table.ts | 17 +-- .../apps/transform/creation_index_pattern.ts | 10 +- .../apps/transform/creation_saved_search.ts | 10 +- .../test/functional/page_objects/lens_page.ts | 10 +- .../ml/data_frame_analytics_results.ts | 11 +- .../functional/services/transform/wizard.ts | 43 ++++--- yarn.lock | 8 +- 22 files changed, 207 insertions(+), 150 deletions(-) diff --git a/package.json b/package.json index 27cbbf3fb1299..fc5cd02a03253 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "@elastic/datemath": "link:packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary", "@elastic/ems-client": "7.11.0", - "@elastic/eui": "31.3.0", + "@elastic/eui": "31.4.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "^9.0.1-kibana3", "@elastic/node-crypto": "1.2.1", diff --git a/test/accessibility/services/a11y/analyze_with_axe.js b/test/accessibility/services/a11y/analyze_with_axe.js index 301d03ec17fb1..3d1e257235f55 100644 --- a/test/accessibility/services/a11y/analyze_with_axe.js +++ b/test/accessibility/services/a11y/analyze_with_axe.js @@ -30,6 +30,10 @@ export function analyzeWithAxe(context, options, callback) { id: 'aria-roles', selector: '[data-test-subj="comboBoxSearchInput"] *', }, + { + id: 'aria-required-parent', + selector: '[class=*"euiDataGridRowCell"][role="gridcell"] ', + }, ], }); return window.axe.run(context, options); diff --git a/test/functional/apps/dashboard/embeddable_data_grid.ts b/test/functional/apps/dashboard/embeddable_data_grid.ts index a81f855198843..54fa9f08c5763 100644 --- a/test/functional/apps/dashboard/embeddable_data_grid.ts +++ b/test/functional/apps/dashboard/embeddable_data_grid.ts @@ -36,10 +36,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('saved search filters', function () { it('are added when a cell filter is clicked', async function () { await dashboardAddPanel.addSavedSearch('Rendering-Test:-saved-search'); - await find.clickByCssSelector(`[role="gridcell"]:nth-child(2)`); + await find.clickByCssSelector(`[role="gridcell"]:nth-child(3)`); + // needs a short delay between becoming visible & being clickable + await PageObjects.common.sleep(250); await find.clickByCssSelector(`[data-test-subj="filterOutButton"]`); await PageObjects.header.waitUntilLoadingHasFinished(); - await find.clickByCssSelector(`[role="gridcell"]:nth-child(2)`); + await find.clickByCssSelector(`[role="gridcell"]:nth-child(3)`); + await PageObjects.common.sleep(250); await find.clickByCssSelector(`[data-test-subj="filterForButton"]`); const filterCount = await filterBar.getFilterCount(); expect(filterCount).to.equal(2); diff --git a/test/functional/apps/discover/_data_grid_context.ts b/test/functional/apps/discover/_data_grid_context.ts index 898efff558702..8f817dbea35c3 100644 --- a/test/functional/apps/discover/_data_grid_context.ts +++ b/test/functional/apps/discover/_data_grid_context.ts @@ -27,6 +27,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('discover data grid context tests', () => { before(async () => { + await esArchiver.load('discover'); await esArchiver.loadIfNeeded('logstash_functional'); await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await kibanaServer.uiSettings.update(defaultSettings); diff --git a/test/functional/apps/discover/_data_grid_doc_table.ts b/test/functional/apps/discover/_data_grid_doc_table.ts index 1775b096fecd8..5eeafc4d78f67 100644 --- a/test/functional/apps/discover/_data_grid_doc_table.ts +++ b/test/functional/apps/discover/_data_grid_doc_table.ts @@ -22,8 +22,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }; describe('discover data grid doc table', function describeIndexTests() { - const defaultRowsLimit = 25; - before(async function () { log.debug('load kibana index with default index pattern'); await esArchiver.load('discover'); @@ -38,10 +36,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.uiSettings.replace({}); }); - it('should show the first 50 rows by default', async function () { + it('should show the first 12 rows by default', async function () { // with the default range the number of hits is ~14000 const rows = await dataGrid.getDocTableRows(); - expect(rows.length).to.be(defaultRowsLimit); + expect(rows.length).to.be(12); }); it('should refresh the table content when changing time window', async function () { diff --git a/test/functional/apps/discover/_data_grid_field_data.ts b/test/functional/apps/discover/_data_grid_field_data.ts index 068ed82a7c603..e8fcb06d06193 100644 --- a/test/functional/apps/discover/_data_grid_field_data.ts +++ b/test/functional/apps/discover/_data_grid_field_data.ts @@ -67,9 +67,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dataGrid.clickDocSortAsc(); await PageObjects.discover.waitUntilSearchingHasFinished(); - await retry.try(async function tryingForTime() { - const rowData = await dataGrid.getFields(); - expect(rowData[0][0].startsWith(expectedTimeStamp)).to.be.ok(); + await retry.waitFor('first cell contains expected timestamp', async () => { + const cell = await dataGrid.getCellElement(1, 2); + const text = await cell.getVisibleText(); + return text === expectedTimeStamp; }); }); diff --git a/test/functional/apps/visualize/_data_table.ts b/test/functional/apps/visualize/_data_table.ts index c98126dd01843..0b9cedd0ca94c 100644 --- a/test/functional/apps/visualize/_data_table.ts +++ b/test/functional/apps/visualize/_data_table.ts @@ -267,14 +267,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should apply correct filter', async () => { - await PageObjects.visChart.filterOnTableCell(1, 3); - await PageObjects.visChart.waitForVisualizationRenderingStabilized(); - const data = await PageObjects.visChart.getTableVisContent(); - expect(data).to.be.eql([ - ['png', '1,373'], - ['gif', '918'], - ['Other', '445'], - ]); + await retry.try(async () => { + await PageObjects.visChart.filterOnTableCell(1, 3); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + const data = await PageObjects.visChart.getTableVisContent(); + expect(data).to.be.eql([ + ['png', '1,373'], + ['gif', '918'], + ['Other', '445'], + ]); + }); }); }); diff --git a/test/functional/apps/visualize/_data_table_notimeindex_filters.ts b/test/functional/apps/visualize/_data_table_notimeindex_filters.ts index df3af20fca613..df219edc1d2d5 100644 --- a/test/functional/apps/visualize/_data_table_notimeindex_filters.ts +++ b/test/functional/apps/visualize/_data_table_notimeindex_filters.ts @@ -14,6 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const filterBar = getService('filterBar'); const renderable = getService('renderable'); + const retry = getService('retry'); const dashboardAddPanel = getService('dashboardAddPanel'); const PageObjects = getPageObjects([ 'common', @@ -66,13 +67,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.clickNewDashboard(); await dashboardAddPanel.addVisualization(vizName1); - // hover and click on cell to filter - await PageObjects.visChart.filterOnTableCell(1, 2); + await retry.try(async () => { + // hover and click on cell to filter + await PageObjects.visChart.filterOnTableCell(1, 2); - await PageObjects.header.waitUntilLoadingHasFinished(); - await renderable.waitForRender(); - const filterCount = await filterBar.getFilterCount(); - expect(filterCount).to.be(1); + await PageObjects.header.waitUntilLoadingHasFinished(); + await renderable.waitForRender(); + const filterCount = await filterBar.getFilterCount(); + expect(filterCount).to.be(1); + }); await filterBar.removeAllFilters(); }); diff --git a/test/functional/apps/visualize/_embedding_chart.ts b/test/functional/apps/visualize/_embedding_chart.ts index 6bf42d5948d4e..a6f0b21f96b35 100644 --- a/test/functional/apps/visualize/_embedding_chart.ts +++ b/test/functional/apps/visualize/_embedding_chart.ts @@ -14,6 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const filterBar = getService('filterBar'); const renderable = getService('renderable'); const embedding = getService('embedding'); + const retry = getService('retry'); const PageObjects = getPageObjects([ 'visualize', 'visEditor', @@ -80,23 +81,25 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should allow to change timerange from the visualization in embedded mode', async () => { - await PageObjects.visChart.filterOnTableCell(1, 7); - await PageObjects.header.waitUntilLoadingHasFinished(); - await renderable.waitForRender(); + await retry.try(async () => { + await PageObjects.visChart.filterOnTableCell(1, 7); + await PageObjects.header.waitUntilLoadingHasFinished(); + await renderable.waitForRender(); - const data = await PageObjects.visChart.getTableVisContent(); - expect(data).to.be.eql([ - ['03:00', '0B', '1'], - ['03:00', '1.953KB', '1'], - ['03:00', '3.906KB', '1'], - ['03:00', '5.859KB', '2'], - ['03:10', '0B', '1'], - ['03:10', '5.859KB', '1'], - ['03:10', '7.813KB', '1'], - ['03:15', '0B', '1'], - ['03:15', '1.953KB', '1'], - ['03:20', '1.953KB', '1'], - ]); + const data = await PageObjects.visChart.getTableVisContent(); + expect(data).to.be.eql([ + ['03:00', '0B', '1'], + ['03:00', '1.953KB', '1'], + ['03:00', '3.906KB', '1'], + ['03:00', '5.859KB', '2'], + ['03:10', '0B', '1'], + ['03:10', '5.859KB', '1'], + ['03:10', '7.813KB', '1'], + ['03:15', '0B', '1'], + ['03:15', '1.953KB', '1'], + ['03:20', '1.953KB', '1'], + ]); + }); }); }); }); diff --git a/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts index 87ec9ac27902f..abd5975b95d0a 100644 --- a/test/functional/page_objects/visualize_chart_page.ts +++ b/test/functional/page_objects/visualize_chart_page.ts @@ -418,7 +418,7 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr public async filterOnTableCell(columnIndex: number, rowIndex: number) { await retry.try(async () => { const cell = await dataGrid.getCellElement(rowIndex, columnIndex); - await cell.moveMouseTo(); + await cell.focus(); const filterBtn = await testSubjects.findDescendant( 'tbvChartCell__filterForCellValue', cell diff --git a/test/functional/services/data_grid.ts b/test/functional/services/data_grid.ts index 60f75b692ff0e..c0a7e0f82e692 100644 --- a/test/functional/services/data_grid.ts +++ b/test/functional/services/data_grid.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { chunk } from 'lodash'; import { FtrProviderContext } from '../ftr_provider_context'; import { WebElementWrapper } from './lib/web_element_wrapper'; @@ -31,14 +32,11 @@ export function DataGridProvider({ getService, getPageObjects }: FtrProviderCont const columns = $('.euiDataGridHeaderCell__content') .toArray() .map((cell) => $(cell).text()); - const rows = $.findTestSubjects('dataGridRow') + const cells = $.findTestSubjects('dataGridRowCell') .toArray() - .map((row) => - $(row) - .find('.euiDataGridRowCell__truncate') - .toArray() - .map((cell) => $(cell).text()) - ); + .map((cell) => $(cell).text()); + + const rows = chunk(cells, columns.length); return { columns, @@ -56,20 +54,18 @@ export function DataGridProvider({ getService, getPageObjects }: FtrProviderCont cellDataTestSubj: string ): Promise { const $ = await element.parseDomContent(); - return $('[data-test-subj="dataGridRow"]') + const columnNumber = $('.euiDataGridHeaderCell__content').length; + const cells = $.findTestSubjects('dataGridRowCell') .toArray() - .map((row) => - $(row) - .findTestSubjects('dataGridRowCell') - .toArray() - .map((cell) => - $(cell) - .findTestSubject(cellDataTestSubj) - .text() - .replace(/ /g, '') - .trim() - ) + .map((cell) => + $(cell) + .findTestSubject(cellDataTestSubj) + .text() + .replace(/ /g, '') + .trim() ); + + return chunk(cells, columnNumber); } /** @@ -90,62 +86,72 @@ export function DataGridProvider({ getService, getPageObjects }: FtrProviderCont * @param columnIndex column index starting from 1 (1 means 1st column) */ public async getCellElement(rowIndex: number, columnIndex: number) { + const table = await find.byCssSelector('.euiDataGrid'); + const $ = await table.parseDomContent(); + const columnNumber = $('.euiDataGridHeaderCell__content').length; return await find.byCssSelector( - `[data-test-subj="dataGridWrapper"] [data-test-subj="dataGridRow"]:nth-of-type(${ - rowIndex + 1 - }) - [data-test-subj="dataGridRowCell"]:nth-of-type(${columnIndex})` + `[data-test-subj="dataGridWrapper"] [data-test-subj="dataGridRowCell"]:nth-of-type(${ + columnNumber * (rowIndex - 1) + columnIndex + 1 + })` ); } public async getFields() { - const rows = await find.allByCssSelector('.euiDataGridRow'); - - const result = []; - for (const row of rows) { - const cells = await row.findAllByClassName('euiDataGridRowCell__truncate'); - const cellsText = []; - let cellIdx = 0; - for (const cell of cells) { - if (cellIdx > 0) { - cellsText.push(await cell.getVisibleText()); - } - cellIdx++; + const cells = await find.allByCssSelector('.euiDataGridRowCell'); + + const rows: string[][] = []; + let rowIdx = -1; + for (const cell of cells) { + if (await cell.elementHasClass('euiDataGridRowCell--firstColumn')) { + // first column contains expand icon + rowIdx++; + rows[rowIdx] = []; + } + if (!(await cell.elementHasClass('euiDataGridRowCell--controlColumn'))) { + rows[rowIdx].push(await cell.getVisibleText()); } - result.push(cellsText); } - return result; + return rows; } public async getTable(selector: string = 'docTable') { return await testSubjects.find(selector); } - public async getBodyRows(): Promise { - const table = await this.getTable(); - return await table.findAllByTestSubject('dataGridRow'); + public async getBodyRows(): Promise { + return this.getDocTableRows(); } + /** + * Returns an array of rows (which are array of cells) + */ public async getDocTableRows() { const table = await this.getTable(); - return await table.findAllByTestSubject('dataGridRow'); - } - - public async getAnchorRow(): Promise { - const table = await this.getTable(); - return await table.findByTestSubject('~docTableAnchorRow'); + const cells = await table.findAllByCssSelector('.euiDataGridRowCell'); + + const rows: WebElementWrapper[][] = []; + let rowIdx = -1; + for (const cell of cells) { + if (await cell.elementHasClass('euiDataGridRowCell--firstColumn')) { + rowIdx++; + rows[rowIdx] = []; + } + rows[rowIdx].push(cell); + } + return rows; } - public async getRow(options: SelectOptions): Promise { - return options.isAnchorRow - ? await this.getAnchorRow() - : (await this.getBodyRows())[options.rowIndex]; + /** + * Returns an array of cells for that row + */ + public async getRow(options: SelectOptions): Promise { + return (await this.getBodyRows())[options.rowIndex]; } public async clickRowToggle( options: SelectOptions = { isAnchorRow: false, rowIndex: 0 } ): Promise { const row = await this.getRow(options); - const toggle = await row.findByTestSubject('~docTableExpandToggleColumn'); + const toggle = await row[0]; await toggle.click(); } diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx index 50d040bc5c397..588340fbe97fa 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx @@ -161,6 +161,8 @@ describe('DatatableComponent', () => { /> ); + wrapper.find('[data-test-subj="dataGridRowCell"]').first().simulate('focus'); + wrapper.find('[data-test-subj="lensDatatableFilterOut"]').first().simulate('click'); expect(onDispatchEvent).toHaveBeenCalledWith({ @@ -200,7 +202,9 @@ describe('DatatableComponent', () => { /> ); - wrapper.find('[data-test-subj="lensDatatableFilterFor"]').at(3).simulate('click'); + wrapper.find('[data-test-subj="dataGridRowCell"]').at(1).simulate('focus'); + + wrapper.find('[data-test-subj="lensDatatableFilterFor"]').first().simulate('click'); expect(onDispatchEvent).toHaveBeenCalledWith({ name: 'filter', @@ -278,7 +282,9 @@ describe('DatatableComponent', () => { /> ); - wrapper.find('[data-test-subj="lensDatatableFilterFor"]').at(1).simulate('click'); + wrapper.find('[data-test-subj="dataGridRowCell"]').at(0).simulate('focus'); + + wrapper.find('[data-test-subj="lensDatatableFilterFor"]').first().simulate('click'); expect(onDispatchEvent).toHaveBeenCalledWith({ name: 'filter', diff --git a/x-pack/test/accessibility/apps/lens.ts b/x-pack/test/accessibility/apps/lens.ts index 229bc76a229ee..2f5ebe3c1a2dc 100644 --- a/x-pack/test/accessibility/apps/lens.ts +++ b/x-pack/test/accessibility/apps/lens.ts @@ -44,8 +44,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.configureDimension({ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', - operation: 'date_histogram', - field: 'timestamp', + operation: 'terms', + field: 'DestCityName', }); await PageObjects.lens.configureDimension({ diff --git a/x-pack/test/accessibility/apps/ml.ts b/x-pack/test/accessibility/apps/ml.ts index baa5e9df61768..0dbc7cbb041d7 100644 --- a/x-pack/test/accessibility/apps/ml.ts +++ b/x-pack/test/accessibility/apps/ml.ts @@ -235,7 +235,9 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsResults.assertOutlierTablePanelExists(); await ml.dataFrameAnalyticsResults.assertResultsTableExists(); await ml.dataFrameAnalyticsResults.assertResultsTableNotEmpty(); - await a11y.testAppSnapshot(); + // EuiDataGrid does not have row roles + // https://github.com/elastic/eui/issues/4471 + // await a11y.testAppSnapshot(); }); it('data frame analytics create job select index pattern modal', async () => { @@ -251,7 +253,9 @@ export default function ({ getService }: FtrProviderContext) { ); await ml.jobSourceSelection.selectSourceForAnalyticsJob(ihpIndexPattern); await ml.dataFrameAnalyticsCreation.assertConfigurationStepActive(); - await a11y.testAppSnapshot(); + // EuiDataGrid does not have row roles + // https://github.com/elastic/eui/issues/4471 + // await a11y.testAppSnapshot(); }); it('data frame analytics create job configuration step for outlier job', async () => { @@ -264,7 +268,9 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsCreation.enableSourceDataPreviewHistogramCharts(); await ml.testExecution.logTestStep('displays the include fields selection'); await ml.dataFrameAnalyticsCreation.assertIncludeFieldsSelectionExists(); - await a11y.testAppSnapshot(); + // EuiDataGrid does not have row roles + // https://github.com/elastic/eui/issues/4471 + // await a11y.testAppSnapshot(); }); it('data frame analytics create job additional options step for outlier job', async () => { diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 73c5838259f6e..a86a67d7c8d0d 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -11,6 +11,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); const find = getService('find'); + const retry = getService('retry'); const listingTable = getService('listingTable'); const testSubjects = getService('testSubjects'); const elasticChart = getService('elasticChart'); @@ -589,13 +590,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should able to use filters cell actions in table', async () => { const firstCellContent = await PageObjects.lens.getDatatableCellText(0, 0); - await PageObjects.lens.clickTableCellAction(0, 0, 'lensDatatableFilterOut'); - await PageObjects.header.waitUntilLoadingHasFinished(); - expect( - await find.existsByCssSelector( - `[data-test-subj*="filter-value-${firstCellContent}"][data-test-subj*="filter-negated"]` - ) - ).to.eql(true); + await retry.try(async () => { + await PageObjects.lens.clickTableCellAction(0, 0, 'lensDatatableFilterOut'); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect( + await find.existsByCssSelector( + `[data-test-subj*="filter-value-${firstCellContent}"][data-test-subj*="filter-negated"]` + ) + ).to.eql(true); + }); }); }); } diff --git a/x-pack/test/functional/apps/lens/table.ts b/x-pack/test/functional/apps/lens/table.ts index f79d1c342b72f..3f9cdf06da8ab 100644 --- a/x-pack/test/functional/apps/lens/table.ts +++ b/x-pack/test/functional/apps/lens/table.ts @@ -12,6 +12,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); const listingTable = getService('listingTable'); const find = getService('find'); + const retry = getService('retry'); describe('lens datatable', () => { it('should able to sort a table by a column', async () => { @@ -40,13 +41,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should able to use filters cell actions in table', async () => { const firstCellContent = await PageObjects.lens.getDatatableCellText(0, 0); - await PageObjects.lens.clickTableCellAction(0, 0, 'lensDatatableFilterOut'); - await PageObjects.header.waitUntilLoadingHasFinished(); - expect( - await find.existsByCssSelector( - `[data-test-subj*="filter-value-${firstCellContent}"][data-test-subj*="filter-negated"]` - ) - ).to.eql(true); + await retry.try(async () => { + await PageObjects.lens.clickTableCellAction(0, 0, 'lensDatatableFilterOut'); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect( + await find.existsByCssSelector( + `[data-test-subj*="filter-value-${firstCellContent}"][data-test-subj*="filter-negated"]` + ) + ).to.eql(true); + }); }); it('should allow to configure column visibility', async () => { diff --git a/x-pack/test/functional/apps/transform/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation_index_pattern.ts index 9c8b22803ccbe..c28b3cfec85ac 100644 --- a/x-pack/test/functional/apps/transform/creation_index_pattern.ts +++ b/x-pack/test/functional/apps/transform/creation_index_pattern.ts @@ -453,10 +453,12 @@ export default function ({ getService }: FtrProviderContext) { await transform.testExecution.logTestStep('shows the transform preview'); await transform.wizard.assertPivotPreviewChartHistogramButtonMissing(); - await transform.wizard.assertPivotPreviewColumnValues( - testData.expected.transformPreview.column, - testData.expected.transformPreview.values - ); + // cell virtualization means the last column is cutoff in the functional tests + // https://github.com/elastic/eui/issues/4470 + // await transform.wizard.assertPivotPreviewColumnValues( + // testData.expected.transformPreview.column, + // testData.expected.transformPreview.values + // ); await transform.testExecution.logTestStep('loads the details step'); await transform.wizard.advanceToDetailsStep(); diff --git a/x-pack/test/functional/apps/transform/creation_saved_search.ts b/x-pack/test/functional/apps/transform/creation_saved_search.ts index 620dd6e0823ac..673f5b3217fb5 100644 --- a/x-pack/test/functional/apps/transform/creation_saved_search.ts +++ b/x-pack/test/functional/apps/transform/creation_saved_search.ts @@ -292,10 +292,12 @@ export default function ({ getService }: FtrProviderContext) { await transform.testExecution.logTestStep( 'displays the transform preview in the expanded row' ); - await transform.table.assertTransformsExpandedRowPreviewColumnValues( - testData.expected.transformPreview.column, - testData.expected.transformPreview.values - ); + // cell virtualization means the last column is cutoff in the functional tests + // https://github.com/elastic/eui/issues/4470 + // await transform.table.assertTransformsExpandedRowPreviewColumnValues( + // testData.expected.transformPreview.column, + // testData.expected.transformPreview.values + // ); }); }); } diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index f6960600a6d7c..fc6842aae0345 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -350,6 +350,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont async switchToVisualization(subVisualizationId: string) { await this.openChartSwitchPopover(); await testSubjects.click(`lnsChartSwitchPopover_${subVisualizationId}`); + await PageObjects.header.waitUntilLoadingHasFinished(); }, async openChartSwitchPopover() { @@ -531,10 +532,13 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }, async getDatatableCell(rowIndex = 0, colIndex = 0) { + const table = await find.byCssSelector('.euiDataGrid'); + const $ = await table.parseDomContent(); + const columnNumber = $('.euiDataGridHeaderCell__content').length; return await find.byCssSelector( - `[data-test-subj="lnsDataTable"] [data-test-subj="dataGridRow"]:nth-child(${ - rowIndex + 2 // this is a bit specific for EuiDataGrid: the first row is the Header - }) [data-test-subj="dataGridRowCell"]:nth-child(${colIndex + 1})` + `[data-test-subj="lnsDataTable"] [data-test-subj="dataGridRowCell"]:nth-child(${ + rowIndex * columnNumber + colIndex + 2 + })` ); }, diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_results.ts b/x-pack/test/functional/services/ml/data_frame_analytics_results.ts index f1d9b08cc2438..b6aba13054f75 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_results.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_results.ts @@ -53,7 +53,9 @@ export function MachineLearningDataFrameAnalyticsResultsProvider({ }, async getResultTableRows() { - return await testSubjects.findAll('mlExplorationDataGrid loaded > dataGridRow'); + return (await testSubjects.find('mlExplorationDataGrid loaded')).findAllByTestSubject( + 'dataGridRowCell' + ); }, async assertResultsTableNotEmpty() { @@ -88,6 +90,7 @@ export function MachineLearningDataFrameAnalyticsResultsProvider({ this.assertResultsTableNotEmpty(); const featureImportanceCell = await this.getFirstFeatureImportanceCell(); + await featureImportanceCell.focus(); const interactionButton = await featureImportanceCell.findByTagName('button'); // simulate hover and wait for button to appear @@ -101,11 +104,9 @@ export function MachineLearningDataFrameAnalyticsResultsProvider({ async getFirstFeatureImportanceCell(): Promise { // get first row of the data grid - const firstDataGridRow = await testSubjects.find( - 'mlExplorationDataGrid loaded > dataGridRow' - ); + const dataGrid = await testSubjects.find('mlExplorationDataGrid loaded'); // find the feature importance cell in that row - const featureImportanceCell = await firstDataGridRow.findByCssSelector( + const featureImportanceCell = await dataGrid.findByCssSelector( '[data-test-subj="dataGridRowCell"][class*="featureImportance"]' ); return featureImportanceCell; diff --git a/x-pack/test/functional/services/transform/wizard.ts b/x-pack/test/functional/services/transform/wizard.ts index 7223d210cfb15..518accdeaf47e 100644 --- a/x-pack/test/functional/services/transform/wizard.ts +++ b/x-pack/test/functional/services/transform/wizard.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { chunk } from 'lodash'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -88,18 +89,24 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { async parseEuiDataGrid(tableSubj: string) { const table = await testSubjects.find(`~${tableSubj}`); const $ = await table.parseDomContent(); - const rows = []; - - // For each row, get the content of each cell and - // add its values as an array to each row. - for (const tr of $.findTestSubjects(`~dataGridRow`).toArray()) { - rows.push( - $(tr) - .find('.euiDataGridRowCell__truncate') - .toArray() - .map((cell) => $(cell).text().trim()) + + // find columns to help determine number of rows + const columns = $('.euiDataGridHeaderCell__content') + .toArray() + .map((cell) => $(cell).text()); + + // Get the content of each cell and divide them up into rows + const cells = $.findTestSubjects('dataGridRowCell') + .find('.euiDataGridRowCell__truncate') + .toArray() + .map((cell) => + $(cell) + .text() + .trim() + .replace(/Row: \d+, Column: \d+:$/g, '') ); - } + + const rows = chunk(cells, columns.length); return rows; }, @@ -139,12 +146,14 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { `EuiDataGrid rows should be '${expectedNumberOfRows}' (got '${rowsData.length}')` ); - rowsData.map((r, i) => - expect(r).to.length( - columns, - `EuiDataGrid row #${i + 1} column count should be '${columns}' (got '${r.length}')` - ) - ); + // cell virtualization means the last column is cutoff in the functional tests + // https://github.com/elastic/eui/issues/4470 + // rowsData.map((r, i) => + // expect(r).to.length( + // columns, + // `EuiDataGrid row #${i + 1} column count should be '${columns}' (got '${r.length}')` + // ) + // ); }); }, diff --git a/yarn.lock b/yarn.lock index 9be907922c2a6..24fe6463fa41c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2204,10 +2204,10 @@ resolved "https://registry.yarnpkg.com/@elastic/eslint-plugin-eui/-/eslint-plugin-eui-0.0.2.tgz#56b9ef03984a05cc213772ae3713ea8ef47b0314" integrity sha512-IoxURM5zraoQ7C8f+mJb9HYSENiZGgRVcG4tLQxE61yHNNRDXtGDWTZh8N1KIHcsqN1CEPETjuzBXkJYF/fDiQ== -"@elastic/eui@31.3.0": - version "31.3.0" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-31.3.0.tgz#f39eecc09d588e4b22150faceb67e5e169afbbd8" - integrity sha512-1Sjhf5HVakx7VGWQkKP8wzGUf7HzyoNnAxjg5P3NH8k+ctJFagS1Wlz9zogwClEuj3FMTMC4tzbJyo06OgHECw== +"@elastic/eui@31.4.0": + version "31.4.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-31.4.0.tgz#d2c8cc91fc538f7b1c5e5229663e186fa0c9207c" + integrity sha512-ADdUeNxj2uiN13U7AkF0ishLAN0xcqFWHC+xjEmx8Wedyaj5DFrmmJEuH9aXv+XSQG5l8ppMgZQb3pMDjR2mKw== dependencies: "@types/chroma-js" "^2.0.0" "@types/lodash" "^4.14.160" From 5d9b84ff7539b188e17b534d0c363b91f753cf16 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 5 Feb 2021 09:16:28 -0800 Subject: [PATCH 04/44] [DOCS] Clean up text (#90359) --- docs/developer/architecture/index.asciidoc | 4 ++-- .../running-kibana-advanced.asciidoc | 18 ------------------ 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/docs/developer/architecture/index.asciidoc b/docs/developer/architecture/index.asciidoc index 7fa7d80ef9729..4bdd693979b49 100644 --- a/docs/developer/architecture/index.asciidoc +++ b/docs/developer/architecture/index.asciidoc @@ -13,8 +13,8 @@ To begin plugin development, we recommend reading our overview of how plugins wo * <> Our developer services are changing all the time. One of the best ways to discover and learn about them is to read the available -READMEs from all the plugins inside our {kib-repo}tree/{branch}/src/plugins[open source plugins folder] and our -{kib-repo}/tree/{branch}/x-pack/plugins[commercial plugins folder]. +READMEs inside our plugins folders: {kib-repo}tree/{branch}/src/plugins[src/plugins] and +{kib-repo}/tree/{branch}/x-pack/plugins[x-pack/plugins]. A few services also automatically generate api documentation which can be browsed inside the {kib-repo}tree/{branch}/docs/development[docs/development section of our repo] diff --git a/docs/developer/getting-started/running-kibana-advanced.asciidoc b/docs/developer/getting-started/running-kibana-advanced.asciidoc index 277e52a3dc8e9..68a4951ea1c21 100644 --- a/docs/developer/getting-started/running-kibana-advanced.asciidoc +++ b/docs/developer/getting-started/running-kibana-advanced.asciidoc @@ -23,24 +23,6 @@ By default, you can log in with username `elastic` and password `changeme`. See the `--help` options on `yarn es ` if you’d like to configure a different password. -[discrete] -=== Running {kib} in Open-Source mode - -If you’re looking to only work with the open-source software, supply the -license type to `yarn es`: - -[source,bash] ----- -yarn es snapshot --license oss ----- - -And start {kib} with only open-source code: - -[source,bash] ----- -yarn start --oss ----- - [discrete] === Unsupported URL Type From 43e8ff8f8f17770174f2e505f513a8e2b67d963d Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Fri, 5 Feb 2021 18:17:03 +0100 Subject: [PATCH 05/44] [Lens] Add new drag and drop capabilities (#89745) --- .../__snapshots__/drag_drop.test.tsx.snap | 10 +- .../lens/public/drag_drop/announcements.tsx | 180 +++ .../lens/public/drag_drop/drag_drop.scss | 12 + .../lens/public/drag_drop/drag_drop.test.tsx | 505 +++++--- .../lens/public/drag_drop/drag_drop.tsx | 311 ++--- .../lens/public/drag_drop/providers.tsx | 155 ++- .../plugins/lens/public/drag_drop/readme.md | 7 +- .../draggable_dimension_button.tsx | 103 +- .../config_panel/empty_dimension_button.tsx | 79 +- .../config_panel/layer_panel.test.tsx | 78 +- .../editor_frame/config_panel/layer_panel.tsx | 26 +- .../editor_frame/editor_frame.test.tsx | 12 +- .../editor_frame/suggestion_helpers.test.ts | 5 +- .../workspace_panel/workspace_panel.test.tsx | 9 +- .../workspace_panel/workspace_panel.tsx | 17 +- .../public/editor_frame_service/mocks.tsx | 2 +- .../datapanel.test.tsx | 6 +- .../dimension_panel/dimension_panel.test.tsx | 3 - .../dimension_panel/droppable.test.ts | 1040 ++++++++++------- .../dimension_panel/droppable.ts | 275 +++-- .../indexpattern_datasource/field_item.scss | 18 + .../indexpattern_datasource/field_item.tsx | 30 +- .../indexpattern_datasource/indexpattern.tsx | 13 +- .../public/indexpattern_datasource/mocks.ts | 1 + .../public/indexpattern_datasource/types.ts | 5 + .../public/indexpattern_datasource/utils.ts | 3 +- x-pack/plugins/lens/public/types.ts | 18 +- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - .../functional/apps/lens/drag_and_drop.ts | 125 +- .../test/functional/page_objects/lens_page.ts | 79 +- 31 files changed, 2039 insertions(+), 1092 deletions(-) create mode 100644 x-pack/plugins/lens/public/drag_drop/announcements.tsx diff --git a/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap index 6423a9f6190a7..b3b695b22ad71 100644 --- a/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap +++ b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`DragDrop droppable is reflected in the className 1`] = ` +exports[`DragDrop defined dropType is reflected in the className 1`] = ` ); @@ -46,10 +48,10 @@ describe('DragDrop', () => { expect(component).toMatchSnapshot(); }); - test('dragover calls preventDefault if droppable is true', () => { + test('dragover calls preventDefault if dropType is defined', () => { const preventDefault = jest.fn(); const component = mount( - + ); @@ -59,10 +61,10 @@ describe('DragDrop', () => { expect(preventDefault).toBeCalled(); }); - test('dragover does not call preventDefault if droppable is false', () => { + test('dragover does not call preventDefault if dropType is undefined', () => { const preventDefault = jest.fn(); const component = mount( - + ); @@ -75,9 +77,15 @@ describe('DragDrop', () => { test('dragstart sets dragging in the context', async () => { const setDragging = jest.fn(); + const setA11yMessage = jest.fn(); const component = mount( - - + + @@ -87,8 +95,9 @@ describe('DragDrop', () => { jest.runAllTimers(); - expect(dataTransfer.setData).toBeCalledWith('text', 'drag label'); + expect(dataTransfer.setData).toBeCalledWith('text', 'hello'); expect(setDragging).toBeCalledWith(value); + expect(setA11yMessage).toBeCalledWith('Lifted hello'); }); test('drop resets all the things', async () => { @@ -100,10 +109,10 @@ describe('DragDrop', () => { const component = mount( - + @@ -116,18 +125,22 @@ describe('DragDrop', () => { expect(preventDefault).toBeCalled(); expect(stopPropagation).toBeCalled(); expect(setDragging).toBeCalledWith(undefined); - expect(onDrop).toBeCalledWith({ id: '2', label: 'hi' }, { id: '1', label: 'hello' }); + expect(onDrop).toBeCalledWith({ id: '2', humanData: { label: 'label1' } }, 'field_add'); }); - test('drop function is not called on droppable=false', async () => { + test('drop function is not called on dropType undefined', async () => { const preventDefault = jest.fn(); const stopPropagation = jest.fn(); const setDragging = jest.fn(); const onDrop = jest.fn(); const component = mount( - - + + @@ -143,14 +156,15 @@ describe('DragDrop', () => { expect(onDrop).not.toHaveBeenCalled(); }); - test('droppable is reflected in the className', () => { + test('defined dropType is reflected in the className', () => { const component = render( { throw x; }} - droppable + dropType="field_add" value={value} + order={[2, 0, 1, 0]} > @@ -159,13 +173,18 @@ describe('DragDrop', () => { expect(component).toMatchSnapshot(); }); - test('items that have droppable=false get special styling when another item is dragged', () => { + test('items that has dropType=undefined get special styling when another item is dragged', () => { const component = mount( - + - {}} droppable={false} value={{ id: '2' }}> + {}} + dropType={undefined} + value={{ id: '2', humanData: { label: 'label2' } }} + > @@ -175,30 +194,39 @@ describe('DragDrop', () => { }); test('additional styles are reflected in the className until drop', () => { - let dragging: { id: '1' } | undefined; - const getAdditionalClasses = jest.fn().mockReturnValue('additional'); + let dragging: { id: '1'; humanData: { label: 'label1' } } | undefined; + const getAdditionalClassesOnEnter = jest.fn().mockReturnValue('additional'); + const getAdditionalClassesOnDroppable = jest.fn().mockReturnValue('droppable'); + const setA11yMessage = jest.fn(); let activeDropTarget; const component = mount( { - dragging = { id: '1' }; + dragging = { id: '1', humanData: { label: 'label1' } }; }} setActiveDropTarget={(val) => { activeDropTarget = { activeDropTarget: val }; }} activeDropTarget={activeDropTarget} > - + {}} - droppable - getAdditionalClassesOnEnter={getAdditionalClasses} + dropType="field_add" + getAdditionalClassesOnEnter={getAdditionalClassesOnEnter} + getAdditionalClassesOnDroppable={getAdditionalClassesOnDroppable} > @@ -210,6 +238,7 @@ describe('DragDrop', () => { .first() .simulate('dragstart', { dataTransfer }); jest.runAllTimers(); + expect(setA11yMessage).toBeCalledWith('Lifted ignored'); component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragover'); component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('drop'); @@ -217,8 +246,9 @@ describe('DragDrop', () => { }); test('additional enter styles are reflected in the className until dragleave', () => { - let dragging: { id: '1' } | undefined; + let dragging: { id: '1'; humanData: { label: 'label1' } } | undefined; const getAdditionalClasses = jest.fn().mockReturnValue('additional'); + const getAdditionalClassesOnDroppable = jest.fn().mockReturnValue('droppable'); const setActiveDropTarget = jest.fn(); const component = mount( @@ -226,7 +256,7 @@ describe('DragDrop', () => { setA11yMessage={jest.fn()} dragging={dragging} setDragging={() => { - dragging = { id: '1' }; + dragging = { id: '1', humanData: { label: 'label1' } }; }} setActiveDropTarget={setActiveDropTarget} activeDropTarget={ @@ -234,15 +264,22 @@ describe('DragDrop', () => { } keyboardMode={false} setKeyboardMode={(keyboardMode) => true} + registerDropTarget={jest.fn()} > - + {}} - droppable + dropType="field_add" getAdditionalClassesOnEnter={getAdditionalClasses} + getAdditionalClassesOnDroppable={getAdditionalClassesOnDroppable} > @@ -257,19 +294,137 @@ describe('DragDrop', () => { component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragover'); expect(component.find('.additional')).toHaveLength(1); - component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragleave'); expect(setActiveDropTarget).toBeCalledWith(undefined); }); + test('Keyboard navigation: User receives proper drop Targets highlighted when pressing arrow keys', () => { + const onDrop = jest.fn(); + const setActiveDropTarget = jest.fn(); + const setA11yMessage = jest.fn(); + const items = [ + { + draggable: true, + value: { + id: '1', + humanData: { label: 'label1', position: 1 }, + }, + children: '1', + order: [2, 0, 0, 0], + }, + { + draggable: true, + dragType: 'move' as 'copy' | 'move', + + value: { + id: '2', + + humanData: { label: 'label2', position: 1 }, + }, + onDrop, + dropType: 'move_compatible' as DropType, + order: [2, 0, 1, 0], + }, + { + draggable: true, + dragType: 'move' as 'copy' | 'move', + value: { + id: '3', + humanData: { label: 'label3', position: 1 }, + }, + onDrop, + dropType: 'replace_compatible' as DropType, + order: [2, 0, 2, 0], + }, + { + draggable: true, + dragType: 'move' as 'copy' | 'move', + value: { + id: '4', + humanData: { label: 'label4', position: 2 }, + }, + order: [2, 0, 2, 1], + }, + ]; + const component = mount( + + {items.map((props) => ( + +
    + + ))} + + ); + const keyboardHandler = component + .find('[data-test-subj="lnsDragDrop-keyboardHandler"]') + .first() + .simulate('focus'); + act(() => { + keyboardHandler.simulate('keydown', { key: 'ArrowRight' }); + expect(setActiveDropTarget).toBeCalledWith({ + ...items[2].value, + onDrop, + dropType: items[2].dropType, + }); + keyboardHandler.simulate('keydown', { key: 'Enter' }); + expect(setA11yMessage).toBeCalledWith( + 'Selected label3 in group at position 1. Press space or enter to replace label3 with label1.' + ); + expect(setActiveDropTarget).toBeCalledWith(undefined); + expect(onDrop).toBeCalledWith( + { humanData: { label: 'label1', position: 1 }, id: '1' }, + 'move_compatible' + ); + }); + }); + describe('reordering', () => { + const onDrop = jest.fn(); + const items = [ + { + id: '1', + humanData: { label: 'label1', position: 1 }, + onDrop, + dropType: 'reorder' as DropType, + }, + { + id: '2', + humanData: { label: 'label2', position: 2 }, + onDrop, + dropType: 'reorder' as DropType, + }, + { + id: '3', + humanData: { label: 'label3', position: 3 }, + onDrop, + dropType: 'reorder' as DropType, + }, + ]; const mountComponent = ( dragContext: Partial | undefined, - onDrop: DropHandler = jest.fn() + onDropHandler?: () => void ) => { let dragging = dragContext?.dragging; let keyboardMode = !!dragContext?.keyboardMode; let activeDropTarget = dragContext?.activeDropTarget; + + const setA11yMessage = jest.fn(); + const registerDropTarget = jest.fn(); const baseContext = { dragging, setDragging: (val?: DragDropIdentifier) => { @@ -280,70 +435,51 @@ describe('DragDrop', () => { keyboardMode = mode; }), setActiveDropTarget: (target?: DragDropIdentifier) => { - activeDropTarget = { activeDropTarget: target } as ActiveDropTarget; + activeDropTarget = { activeDropTarget: target } as DropTargets; }, activeDropTarget, - setA11yMessage: jest.fn(), + setA11yMessage, + registerDropTarget, + }; + + const dragDropSharedProps = { + draggable: true, + dragType: 'move' as 'copy' | 'move', + dropType: 'reorder' as DropType, + reorderableGroup: items.map(({ id }) => ({ id })), + onDrop: onDropHandler || onDrop, }; + return mount( 1 - + 2 - + 3 ); }; - test(`Inactive reorderable group renders properly`, () => { - const component = mountComponent(undefined, jest.fn()); - expect(component.find('.lnsDragDrop-reorderable')).toHaveLength(3); + test(`Inactive group renders properly`, () => { + const component = mountComponent(undefined); + expect(component.find('[data-test-subj="lnsDragDrop"]')).toHaveLength(3); }); test(`Reorderable group with lifted element renders properly`, () => { - const setDragging = jest.fn(); const setA11yMessage = jest.fn(); - const component = mountComponent( - { dragging: { id: '1' }, setA11yMessage, setDragging }, - jest.fn() - ); + const setDragging = jest.fn(); + const component = mountComponent({ dragging: items[0], setDragging, setA11yMessage }); act(() => { component .find('[data-test-subj="lnsDragDrop"]') @@ -352,8 +488,8 @@ describe('DragDrop', () => { jest.runAllTimers(); }); - expect(setDragging).toBeCalledWith({ id: '1' }); - expect(setA11yMessage).toBeCalledWith('You have lifted an item 1 in position 1'); + expect(setDragging).toBeCalledWith(items[0]); + expect(setA11yMessage).toBeCalledWith('Lifted label1'); expect( component .find('[data-test-subj="lnsDragDrop-reorderableGroup"]') @@ -362,7 +498,7 @@ describe('DragDrop', () => { }); test(`Reordered elements get extra styles to show the reorder effect when dragging`, () => { - const component = mountComponent({ dragging: { id: '1' } }, jest.fn()); + const component = mountComponent({ dragging: items[0] }); act(() => { component @@ -403,16 +539,13 @@ describe('DragDrop', () => { }); test(`Dropping an item runs onDrop function`, () => { - const setDragging = jest.fn(); - const setA11yMessage = jest.fn(); const preventDefault = jest.fn(); const stopPropagation = jest.fn(); - const onDrop = jest.fn(); - const component = mountComponent( - { dragging: { id: '1' }, setA11yMessage, setDragging }, - onDrop - ); + const setA11yMessage = jest.fn(); + const setDragging = jest.fn(); + + const component = mountComponent({ dragging: items[0], setDragging, setA11yMessage }); component .find('[data-test-subj="lnsDragDrop-reorderableDropLayer"]') @@ -421,23 +554,58 @@ describe('DragDrop', () => { jest.runAllTimers(); expect(setA11yMessage).toBeCalledWith( - 'You have dropped the item. You have moved the item from position 1 to positon 3' + 'You have dropped the item label1. You have moved the item from position 1 to positon 3' ); expect(preventDefault).toBeCalled(); expect(stopPropagation).toBeCalled(); - expect(onDrop).toBeCalledWith({ id: '1' }, { id: '3' }); + expect(onDrop).toBeCalledWith(items[0], 'reorder'); }); - test(`Keyboard navigation: user can drop element to an activeDropTarget`, () => { - const onDrop = jest.fn(); - const component = mountComponent( - { - dragging: { id: '1' }, - activeDropTarget: { activeDropTarget: { id: '3' } } as ActiveDropTarget, - keyboardMode: true, + test(`Keyboard Navigation: User cannot move an element outside of the group`, () => { + const setA11yMessage = jest.fn(); + const setActiveDropTarget = jest.fn(); + const component = mountComponent({ + dragging: items[0], + keyboardMode: true, + activeDropTarget: { + activeDropTarget: undefined, + dropTargetsByOrder: { + '2,0,0': undefined, + '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, + '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, + }, }, - onDrop + setActiveDropTarget, + setA11yMessage, + }); + const keyboardHandler = component.find('[data-test-subj="lnsDragDrop-keyboardHandler"]'); + + keyboardHandler.simulate('keydown', { key: 'Space' }); + keyboardHandler.simulate('keydown', { key: 'ArrowUp' }); + expect(setActiveDropTarget).not.toHaveBeenCalled(); + + keyboardHandler.simulate('keydown', { key: 'Space' }); + keyboardHandler.simulate('keydown', { key: 'ArrowDown' }); + + expect(setActiveDropTarget).toBeCalledWith(items[1]); + expect(setA11yMessage).toBeCalledWith( + 'You have moved the item label1 from position 1 to position 2' ); + }); + test(`Keyboard navigation: user can drop element to an activeDropTarget`, () => { + const component = mountComponent({ + dragging: items[0], + activeDropTarget: { + activeDropTarget: { ...items[2], dropType: 'reorder', onDrop }, + dropTargetsByOrder: { + '2,0,0': { ...items[0], onDrop, dropType: 'reorder' }, + '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, + '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, + }, + }, + + keyboardMode: true, + }); const keyboardHandler = component .find('[data-test-subj="lnsDragDrop-keyboardHandler"]') .simulate('focus'); @@ -447,15 +615,43 @@ describe('DragDrop', () => { keyboardHandler.simulate('keydown', { key: 'ArrowDown' }); keyboardHandler.simulate('keydown', { key: 'Enter' }); }); - expect(onDrop).toBeCalledWith({ id: '1' }, { id: '3' }); + expect(onDrop).toBeCalledWith(items[0], 'reorder'); + }); + + test(`Keyboard Navigation: Doesn't call onDrop when movement is cancelled`, () => { + const setA11yMessage = jest.fn(); + const onDropHandler = jest.fn(); + const component = mountComponent({ dragging: items[0], setA11yMessage }, onDropHandler); + const keyboardHandler = component.find('[data-test-subj="lnsDragDrop-keyboardHandler"]'); + keyboardHandler.simulate('keydown', { key: 'Space' }); + keyboardHandler.simulate('keydown', { key: 'Escape' }); + jest.runAllTimers(); + + expect(onDropHandler).not.toHaveBeenCalled(); + expect(setA11yMessage).toBeCalledWith('Movement cancelled'); + keyboardHandler.simulate('keydown', { key: 'Space' }); + keyboardHandler.simulate('keydown', { key: 'ArrowDown' }); + keyboardHandler.simulate('blur'); + + expect(onDropHandler).not.toHaveBeenCalled(); + expect(setA11yMessage).toBeCalledWith('Movement cancelled'); }); test(`Keyboard Navigation: Reordered elements get extra styles to show the reorder effect`, () => { const setA11yMessage = jest.fn(); - const component = mountComponent( - { dragging: { id: '1' }, keyboardMode: true, setA11yMessage }, - jest.fn() - ); + const component = mountComponent({ + dragging: items[0], + keyboardMode: true, + activeDropTarget: { + activeDropTarget: undefined, + dropTargetsByOrder: { + '2,0,0': undefined, + '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, + '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, + }, + }, + setA11yMessage, + }); const keyboardHandler = component.find('[data-test-subj="lnsDragDrop-keyboardHandler"]'); keyboardHandler.simulate('keydown', { key: 'Space' }); @@ -475,7 +671,7 @@ describe('DragDrop', () => { component.find('[data-test-subj="lnsDragDrop-translatableDrop"]').at(1).prop('style') ).toEqual(undefined); expect(setA11yMessage).toBeCalledWith( - 'You have moved the item 1 from position 1 to position 2' + 'You have moved the item label1 from position 1 to position 2' ); component @@ -490,63 +686,45 @@ describe('DragDrop', () => { ).toEqual(undefined); }); - test(`Keyboard Navigation: User cannot move an element outside of the group`, () => { - const onDrop = jest.fn(); - const setActiveDropTarget = jest.fn(); - const setA11yMessage = jest.fn(); - const component = mountComponent( - { dragging: { id: '1' }, keyboardMode: true, setActiveDropTarget, setA11yMessage }, - onDrop - ); - const keyboardHandler = component.find('[data-test-subj="lnsDragDrop-keyboardHandler"]'); - - keyboardHandler.simulate('keydown', { key: 'Space' }); - keyboardHandler.simulate('keydown', { key: 'ArrowUp' }); - expect(setActiveDropTarget).not.toHaveBeenCalled(); - - keyboardHandler.simulate('keydown', { key: 'Space' }); - keyboardHandler.simulate('keydown', { key: 'ArrowDown' }); - - expect(setActiveDropTarget).toBeCalledWith({ id: '2' }); - expect(setA11yMessage).toBeCalledWith( - 'You have moved the item 1 from position 1 to position 2' - ); - }); - test(`Keyboard Navigation: User cannot drop element to itself`, () => { - const setActiveDropTarget = jest.fn(); const setA11yMessage = jest.fn(); + const setActiveDropTarget = jest.fn(); const component = mount( 1 2 @@ -557,33 +735,8 @@ describe('DragDrop', () => { keyboardHandler.simulate('keydown', { key: 'Space' }); keyboardHandler.simulate('keydown', { key: 'ArrowUp' }); - expect(setActiveDropTarget).toBeCalledWith({ id: '1' }); - expect(setA11yMessage).toBeCalledWith('You have moved back the item 1 to position 1'); - }); - - test(`Keyboard Navigation: Doesn't call onDrop when movement is cancelled`, () => { - const setA11yMessage = jest.fn(); - const onDrop = jest.fn(); - - const component = mountComponent({ dragging: { id: '1' }, setA11yMessage }, onDrop); - const keyboardHandler = component.find('[data-test-subj="lnsDragDrop-keyboardHandler"]'); - keyboardHandler.simulate('keydown', { key: 'Space' }); - keyboardHandler.simulate('keydown', { key: 'Escape' }); - - jest.runAllTimers(); - - expect(onDrop).not.toHaveBeenCalled(); - expect(setA11yMessage).toBeCalledWith( - 'Movement cancelled. The item has returned to its starting position 1' - ); - keyboardHandler.simulate('keydown', { key: 'Space' }); - keyboardHandler.simulate('keydown', { key: 'ArrowDown' }); - keyboardHandler.simulate('blur'); - - expect(onDrop).not.toHaveBeenCalled(); - expect(setA11yMessage).toBeCalledWith( - 'Movement cancelled. The item has returned to its starting position 1' - ); + expect(setActiveDropTarget).toBeCalledWith(undefined); + expect(setA11yMessage).toBeCalledWith('You have moved the item label1 back to position 1'); }); }); }); diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx index e006e4f5af49e..898071e85ea79 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx @@ -9,23 +9,23 @@ import './drag_drop.scss'; import React, { useContext, useEffect, memo } from 'react'; import classNames from 'classnames'; import { keys, EuiScreenReaderOnly } from '@elastic/eui'; +import useShallowCompareEffect from 'react-use/lib/useShallowCompareEffect'; import { DragDropIdentifier, + DropIdentifier, DragContext, DragContextState, + nextValidDropTarget, ReorderContext, ReorderState, - reorderAnnouncements, + DropHandler, } from './providers'; +import { announce } from './announcements'; import { trackUiEvent } from '../lens_ui_telemetry'; +import { DropType } from '../types'; export type DroppableEvent = React.DragEvent; -/** - * A function that handles a drop event. - */ -export type DropHandler = (dropped: DragDropIdentifier, dropTarget: DragDropIdentifier) => void; - /** * The base props to the DragDrop component. */ @@ -34,10 +34,6 @@ interface BaseProps { * The CSS class(es) for the root element. */ className?: string; - /** - * The label for accessibility - */ - label?: string; /** * The event handler that fires when an item @@ -62,16 +58,15 @@ interface BaseProps { * Indicates whether or not this component is draggable. */ draggable?: boolean; - /** - * Indicates whether or not the currently dragged item - * can be dropped onto this component. - */ - droppable?: boolean; /** * Additional class names to apply when another element is over the drop target */ - getAdditionalClassesOnEnter?: () => string; + getAdditionalClassesOnEnter?: (dropType?: DropType) => string | undefined; + /** + * Additional class names to apply when another element is droppable for a currently dragged item + */ + getAdditionalClassesOnDroppable?: (dropType?: DropType) => string | undefined; /** * The optional test subject associated with this DOM element. @@ -81,35 +76,29 @@ interface BaseProps { /** * items belonging to the same group that can be reordered */ - reorderableGroup?: DragDropIdentifier[]; + reorderableGroup?: Array<{ id: string }>; /** * Indicates to the user whether the currently dragged item * will be moved or copied */ - dragType?: 'copy' | 'move' | 'reorder'; + dragType?: 'copy' | 'move'; /** - * Indicates to the user whether the drop action will - * replace something that is existing or add a new one + * Indicates the type of a drop - when undefined, the currently dragged item + * cannot be dropped onto this component. */ - dropType?: 'add' | 'replace' | 'reorder'; - + dropType?: DropType; /** - * temporary flag to exclude the draggable elements that don't have keyboard nav yet. To be removed along with the feature development + * Order for keyboard dragging. This takes an array of numbers which will be used to order hierarchically */ - noKeyboardSupportYet?: boolean; + order: number[]; } /** * The props for a draggable instance of that component. */ interface DragInnerProps extends BaseProps { - /** - * The label, which should be attached to the drag event, and which will e.g. - * be used if the element will be dropped into a text field. - */ - label?: string; isDragging: boolean; keyboardMode: boolean; setKeyboardMode: DragContextState['setKeyboardMode']; @@ -124,6 +113,7 @@ interface DragInnerProps extends BaseProps { ) => void; onDragEnd?: () => void; extraKeyboardHandler?: (e: React.KeyboardEvent) => void; + ariaDescribedBy?: string; } /** @@ -131,23 +121,16 @@ interface DragInnerProps extends BaseProps { */ interface DropInnerProps extends BaseProps, DragContextState { isDragging: boolean; - isNotDroppable: boolean; } -/** - * A draggable / droppable item. Items can be both draggable and droppable at - * the same time. - * - * @param props - */ - const lnsLayerPanelDimensionMargin = 8; export const DragDrop = (props: BaseProps) => { const { dragging, setDragging, + registerDropTarget, keyboardMode, setKeyboardMode, activeDropTarget, @@ -155,8 +138,7 @@ export const DragDrop = (props: BaseProps) => { setA11yMessage, } = useContext(DragContext); - const { value, draggable, droppable, reorderableGroup } = props; - + const { value, draggable, dropType, reorderableGroup } = props; const isDragging = !!(draggable && value.id === dragging?.id); const dragProps = { @@ -178,16 +160,17 @@ export const DragDrop = (props: BaseProps) => { setDragging, activeDropTarget, setActiveDropTarget, + registerDropTarget, isDragging, setA11yMessage, isNotDroppable: // If the configuration has provided a droppable flag, but this particular item is not // droppable, then it should be less prominent. Ignores items that are both // draggable and drop targets - !!(droppable === false && dragging && value.id !== dragging.id), + !!(!dropType && dragging && value.id !== dragging.id), }; - if (draggable && !droppable) { + if (draggable && !dropType) { if (reorderableGroup && reorderableGroup.length > 1) { return ( { if ( reorderableGroup && reorderableGroup.length > 1 && - reorderableGroup?.some((i) => i.id === value.id) + reorderableGroup?.some((i) => i.id === dragging?.id) ) { - return ; + return ; } return ; }; -const DragInner = memo(function DragDropInner({ +const DragInner = memo(function DragInner({ dataTestSubj, className, value, @@ -219,16 +202,16 @@ const DragInner = memo(function DragDropInner({ setDragging, setKeyboardMode, setActiveDropTarget, - label = '', + order, keyboardMode, isDragging, activeDropTarget, - onDrop, dragType, onDragStart, onDragEnd, extraKeyboardHandler, - noKeyboardSupportYet, + ariaDescribedBy, + setA11yMessage, }: DragInnerProps) { const dragStart = (e?: DroppableEvent | React.KeyboardEvent) => { // Setting stopPropgagation causes Chrome failures, so @@ -241,7 +224,7 @@ const DragInner = memo(function DragDropInner({ // We only can reach the dragStart method if the element is draggable, // so we know we have DraggableProps if we reach this code. if (e && 'dataTransfer' in e) { - e.dataTransfer.setData('text', label); + e.dataTransfer.setData('text', value.humanData.label); } // Chrome causes issues if you try to render from within a @@ -250,6 +233,7 @@ const DragInner = memo(function DragDropInner({ const currentTarget = e?.currentTarget; setTimeout(() => { setDragging(value); + setA11yMessage(announce.lifted(value.humanData)); if (onDragStart) { onDragStart(currentTarget); } @@ -261,53 +245,78 @@ const DragInner = memo(function DragDropInner({ setDragging(undefined); setActiveDropTarget(undefined); setKeyboardMode(false); + setA11yMessage(announce.cancelled()); if (onDragEnd) { onDragEnd(); } }; - const dropToActiveDropTarget = () => { if (isDragging && activeDropTarget?.activeDropTarget) { trackUiEvent('drop_total'); - if (onDrop) { - onDrop(value, activeDropTarget.activeDropTarget); - } + const { dropType, humanData, onDrop: onTargetDrop } = activeDropTarget.activeDropTarget; + setTimeout(() => setA11yMessage(announce.dropped(value.humanData, humanData, dropType))); + onTargetDrop(value, dropType); } }; + const setNextTarget = (reversed = false) => { + if (!order) { + return; + } + + const nextTarget = nextValidDropTarget( + activeDropTarget, + [order.join(',')], + (el) => el?.dropType !== 'reorder', + reversed + ); + + setActiveDropTarget(nextTarget); + setA11yMessage( + nextTarget + ? announce.selectedTarget(value.humanData, nextTarget?.humanData, nextTarget?.dropType) + : announce.noTarget() + ); + }; return ( -
    - {!noKeyboardSupportYet && ( - -
    ); }); const ReorderableDrop = memo(function ReorderableDrop( - props: DropInnerProps & { reorderableGroup: DragDropIdentifier[] } + props: DropInnerProps & { reorderableGroup: Array<{ id: string }> } ) { const { onDrop, value, - droppable, dragging, setDragging, setKeyboardMode, @@ -595,6 +606,7 @@ const ReorderableDrop = memo(function ReorderableDrop( setActiveDropTarget, reorderableGroup, setA11yMessage, + dropType, } = props; const currentIndex = reorderableGroup.findIndex((i) => i.id === value.id); @@ -628,15 +640,14 @@ const ReorderableDrop = memo(function ReorderableDrop( }, [isReordered, setReorderState, value.id]); const onReorderableDragOver = (e: DroppableEvent) => { - if (!droppable) { + if (!dropType) { return; } e.preventDefault(); // An optimization to prevent a bunch of React churn. - // todo: replace with custom function ? - if (!activeDropTargetMatches) { - setActiveDropTarget(value); + if (!activeDropTargetMatches && dropType && onDrop) { + setActiveDropTarget({ ...value, dropType, onDrop }); } const draggingIndex = reorderableGroup.findIndex((i) => i.id === dragging?.id); @@ -675,14 +686,12 @@ const ReorderableDrop = memo(function ReorderableDrop( setDragging(undefined); setKeyboardMode(false); - if (onDrop && droppable && dragging) { + if (onDrop && dropType && dragging) { trackUiEvent('drop_total'); - - onDrop(dragging, value); - const draggingIndex = reorderableGroup.findIndex((i) => i.id === dragging.id); + onDrop(dragging, 'reorder'); // setTimeout ensures it will run after dragEnd messaging setTimeout(() => - setA11yMessage(reorderAnnouncements.dropped(currentIndex + 1, draggingIndex + 1)) + setA11yMessage(announce.dropped(dragging.humanData, value.humanData, 'reorder')) ); } }; @@ -707,7 +716,7 @@ const ReorderableDrop = memo(function ReorderableDrop(
    void; export type DragDropIdentifier = Record & { id: string; + /** + * The data for accessibility, consists of required label and not required groupLabel and position in group + */ + humanData: HumanData; }; -export interface ActiveDropTarget { - activeDropTarget?: DragDropIdentifier; +export type DropIdentifier = DragDropIdentifier & { + dropType: DropType; + onDrop: DropHandler; +}; + +export interface DropTargets { + activeDropTarget?: DropIdentifier; + dropTargetsByOrder: Record; } /** * The shape of the drag / drop context. @@ -39,11 +56,12 @@ export interface DragContextState { */ setDragging: (dragging?: DragDropIdentifier) => void; - activeDropTarget?: ActiveDropTarget; + activeDropTarget?: DropTargets; - setActiveDropTarget: (newTarget?: DragDropIdentifier) => void; + setActiveDropTarget: (newTarget?: DropIdentifier) => void; setA11yMessage: (message: string) => void; + registerDropTarget: (order: number[], dropTarget?: DropIdentifier) => void; } /** @@ -59,6 +77,7 @@ export const DragContext = React.createContext({ activeDropTarget: undefined, setActiveDropTarget: () => {}, setA11yMessage: () => {}, + registerDropTarget: () => {}, }); /** @@ -89,10 +108,13 @@ export interface ProviderProps { setDragging: (dragging?: DragDropIdentifier) => void; activeDropTarget?: { - activeDropTarget?: DragDropIdentifier; + activeDropTarget?: DropIdentifier; + dropTargetsByOrder: Record; }; - setActiveDropTarget: (newTarget?: DragDropIdentifier) => void; + setActiveDropTarget: (newTarget?: DropIdentifier) => void; + + registerDropTarget: (order: number[], dropTarget?: DropIdentifier) => void; /** * The React children. @@ -116,9 +138,11 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode } const [keyboardModeState, setKeyboardModeState] = useState(false); const [a11yMessageState, setA11yMessageState] = useState(''); const [activeDropTargetState, setActiveDropTargetState] = useState<{ - activeDropTarget?: DragDropIdentifier; + activeDropTarget?: DropIdentifier; + dropTargetsByOrder: Record; }>({ activeDropTarget: undefined, + dropTargetsByOrder: {}, }); const setDragging = useMemo( @@ -131,11 +155,26 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode } ]); const setActiveDropTarget = useMemo( - () => (activeDropTarget?: DragDropIdentifier) => + () => (activeDropTarget?: DropIdentifier) => setActiveDropTargetState((s) => ({ ...s, activeDropTarget })), [setActiveDropTargetState] ); + const registerDropTarget = useMemo( + () => (order: number[], dropTarget?: DropIdentifier) => { + return setActiveDropTargetState((s) => { + return { + ...s, + dropTargetsByOrder: { + ...s.dropTargetsByOrder, + [order.join(',')]: dropTarget, + }, + }; + }); + }, + [setActiveDropTargetState] + ); + return (
    {children} @@ -155,9 +195,14 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode }

    {a11yMessageState}

    +

    + {i18n.translate('xpack.lens.dragDrop.keyboardInstructionsReorder', { + defaultMessage: `Press enter or space to dragging. When dragging, use the up/down arrow keys to reorder items in the group and left/right arrow keys to choose drop targets outside of the group. Press enter or space again to finish.`, + })} +

    {i18n.translate('xpack.lens.dragDrop.keyboardInstructions', { - defaultMessage: `Press enter or space to start reordering the dimension group. When dragging, use arrow keys to reorder. Press enter or space again to finish.`, + defaultMessage: `Press enter or space to start dragging. When dragging, use the left/right arrow keys to move between drop targets. Press enter or space again to finish.`, })}

    @@ -167,6 +212,45 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode } ); } +export function nextValidDropTarget( + activeDropTarget: DropTargets | undefined, + draggingOrder: [string], + filterElements: (el: DragDropIdentifier) => boolean = () => true, + reverse = false +) { + if (!activeDropTarget) { + return; + } + + const filteredTargets = [...Object.entries(activeDropTarget.dropTargetsByOrder)].filter( + ([, dropTarget]) => dropTarget && filterElements(dropTarget) + ); + + const nextDropTargets = [...filteredTargets, draggingOrder].sort(([orderA], [orderB]) => { + const parsedOrderA = orderA.split(',').map((v) => Number(v)); + const parsedOrderB = orderB.split(',').map((v) => Number(v)); + + const relevantLevel = parsedOrderA.findIndex((v, i) => parsedOrderA[i] !== parsedOrderB[i]); + return parsedOrderA[relevantLevel] - parsedOrderB[relevantLevel]; + }); + + let currentActiveDropIndex = nextDropTargets.findIndex( + ([_, dropTarget]) => dropTarget?.id === activeDropTarget?.activeDropTarget?.id + ); + + if (currentActiveDropIndex === -1) { + currentActiveDropIndex = nextDropTargets.findIndex( + ([targetOrder]) => targetOrder === draggingOrder[0] + ); + } + + const previousElement = + (nextDropTargets.length + currentActiveDropIndex - 1) % nextDropTargets.length; + const nextElement = (currentActiveDropIndex + 1) % nextDropTargets.length; + + return nextDropTargets[reverse ? previousElement : nextElement][1]; +} + /** * A React drag / drop provider that derives its state from a RootDragDropProvider. If * part of a React application is rendered separately from the root, this provider can @@ -182,6 +266,7 @@ export function ChildDragDropProvider({ activeDropTarget, setActiveDropTarget, setA11yMessage, + registerDropTarget, children, }: ProviderProps) { const value = useMemo( @@ -193,6 +278,7 @@ export function ChildDragDropProvider({ activeDropTarget, setActiveDropTarget, setA11yMessage, + registerDropTarget, }), [ setDragging, @@ -202,6 +288,7 @@ export function ChildDragDropProvider({ setKeyboardMode, keyboardMode, setA11yMessage, + registerDropTarget, ] ); return {children}; @@ -211,7 +298,7 @@ export interface ReorderState { /** * Ids of the elements that are translated up or down */ - reorderedItems: DragDropIdentifier[]; + reorderedItems: Array<{ id: string; height?: number }>; /** * Direction of the move of dragged element in the reordered list @@ -282,51 +369,3 @@ export function ReorderProvider({
    ); } - -export const reorderAnnouncements = { - moved: (itemLabel: string, position: number, prevPosition: number) => { - return prevPosition === position - ? i18n.translate('xpack.lens.dragDrop.elementMovedBack', { - defaultMessage: `You have moved back the item {itemLabel} to position {prevPosition}`, - values: { - itemLabel, - prevPosition, - }, - }) - : i18n.translate('xpack.lens.dragDrop.elementMoved', { - defaultMessage: `You have moved the item {itemLabel} from position {prevPosition} to position {position}`, - values: { - itemLabel, - position, - prevPosition, - }, - }); - }, - - lifted: (itemLabel: string, position: number) => - i18n.translate('xpack.lens.dragDrop.elementLifted', { - defaultMessage: `You have lifted an item {itemLabel} in position {position}`, - values: { - itemLabel, - position, - }, - }), - - cancelled: (position: number) => - i18n.translate('xpack.lens.dragDrop.abortMessageReorder', { - defaultMessage: - 'Movement cancelled. The item has returned to its starting position {position}', - values: { - position, - }, - }), - dropped: (position: number, prevPosition: number) => - i18n.translate('xpack.lens.dragDrop.dropMessageReorder', { - defaultMessage: - 'You have dropped the item. You have moved the item from position {prevPosition} to positon {position}', - values: { - position, - prevPosition, - }, - }), -}; diff --git a/x-pack/plugins/lens/public/drag_drop/readme.md b/x-pack/plugins/lens/public/drag_drop/readme.md index e48564a074986..55a9e3157c247 100644 --- a/x-pack/plugins/lens/public/drag_drop/readme.md +++ b/x-pack/plugins/lens/public/drag_drop/readme.md @@ -56,7 +56,7 @@ const { dragging } = useContext(DragContext); return ( onChange([...items, item])} > {items.map((x) => ( @@ -86,11 +86,14 @@ The children `DragDrop` components must have props defined as in the example: key={f.id} draggable droppable - dragType="reorder" + dragType="move" dropType="reorder" reorderableGroup={fields} // consists all reorderable elements in the group, eg. [{id:'3'}, {id:'5'}, {id:'1'}] value={{ id: f.id, + humanData: { + label: 'Label' + } }} onDrop={/*handler*/} > diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx index f6f4bed44b84d..e3e4f11e8450d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx @@ -7,14 +7,29 @@ import React, { useMemo } from 'react'; import { DragDrop, DragDropIdentifier, DragContextState } from '../../../drag_drop'; -import { Datasource, VisualizationDimensionGroupConfig, isDraggedOperation } from '../../../types'; +import { + Datasource, + VisualizationDimensionGroupConfig, + isDraggedOperation, + DropType, +} from '../../../types'; import { LayerDatasourceDropProps } from './types'; -const isFromTheSameGroup = (el1: DragDropIdentifier, el2?: DragDropIdentifier) => - el2 && isDraggedOperation(el2) && el1.groupId === el2.groupId && el1.columnId !== el2.columnId; +const getAdditionalClassesOnEnter = (dropType?: string) => { + if ( + dropType === 'field_replace' || + dropType === 'replace_compatible' || + dropType === 'replace_incompatible' + ) { + return 'lnsDragDrop-isReplacing'; + } +}; -const isSelf = (el1: DragDropIdentifier, el2?: DragDropIdentifier) => - isDraggedOperation(el2) && el1.columnId === el2.columnId; +const getAdditionalClassesOnDroppable = (dropType?: string) => { + if (dropType === 'move_incompatible' || dropType === 'replace_incompatible') { + return 'lnsDragDrop-notCompatible'; + } +}; export function DraggableDimensionButton({ layerId, @@ -34,7 +49,11 @@ export function DraggableDimensionButton({ layerId: string; groupIndex: number; layerIndex: number; - onDrop: (droppedItem: DragDropIdentifier, dropTarget: DragDropIdentifier) => void; + onDrop: ( + droppedItem: DragDropIdentifier, + dropTarget: DragDropIdentifier, + dropType?: DropType + ) => void; group: VisualizationDimensionGroupConfig; label: string; children: React.ReactElement; @@ -43,66 +62,52 @@ export function DraggableDimensionButton({ accessorIndex: number; columnId: string; }) { - const value = useMemo(() => { - return { + const dropType = layerDatasource.getDropTypes({ + ...layerDatasourceDropProps, + columnId, + filterOperations: group.filterOperations, + groupId: group.groupId, + }); + + const value = useMemo( + () => ({ columnId, groupId: group.groupId, layerId, id: columnId, - }; - }, [columnId, group.groupId, layerId]); - - const { dragging } = dragDropContext; - - const isCurrentGroup = group.groupId === dragging?.groupId; - const isOperationDragged = isDraggedOperation(dragging); - const canHandleDrop = - Boolean(dragDropContext.dragging) && - layerDatasource.canHandleDrop({ - ...layerDatasourceDropProps, - columnId, - filterOperations: group.filterOperations, - }); - - const dragType = isSelf(value, dragging) - ? 'move' - : isOperationDragged && isCurrentGroup - ? 'reorder' - : 'copy'; - - const dropType = isOperationDragged ? (!isCurrentGroup ? 'replace' : 'reorder') : 'add'; - - const isCompatibleFromOtherGroup = !isCurrentGroup && canHandleDrop; - - const isDroppable = isOperationDragged - ? dragType === 'reorder' - ? isFromTheSameGroup(value, dragging) - : isCompatibleFromOtherGroup - : canHandleDrop; + dropType, + humanData: { + label, + groupLabel: group.groupLabel, + position: accessorIndex + 1, + }, + }), + [columnId, group.groupId, accessorIndex, layerId, dropType, label, group.groupLabel] + ); + // todo: simplify by id and use drop targets? const reorderableGroup = useMemo( () => - group.accessors.map((a) => ({ - columnId: a.columnId, - id: a.columnId, - groupId: group.groupId, - layerId, + group.accessors.map((g) => ({ + id: g.columnId, })), - [group, layerId] + [group.accessors] ); return (
    1 ? reorderableGroup : undefined} value={value} - label={label} - droppable={dragging && isDroppable} - onDrop={onDrop} + onDrop={(drag: DragDropIdentifier, selectedDropType?: DropType) => + onDrop(drag, value, selectedDropType) + } > {children} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx index 1116cef1aa3ef..a83d4bde0383c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx @@ -5,17 +5,26 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useMemo, useState, useEffect } from 'react'; import { EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { generateId } from '../../../id_generator'; -import { DragDrop, DragDropIdentifier, DragContextState } from '../../../drag_drop'; -import { Datasource, VisualizationDimensionGroupConfig, isDraggedOperation } from '../../../types'; +import { DragDrop, DragDropIdentifier } from '../../../drag_drop'; +import { Datasource, VisualizationDimensionGroupConfig, DropType } from '../../../types'; import { LayerDatasourceDropProps } from './types'; +const label = i18n.translate('xpack.lens.indexPattern.emptyDimensionButton', { + defaultMessage: 'Empty dimension', +}); + +const getAdditionalClassesOnDroppable = (dropType?: string) => { + if (dropType === 'move_incompatible' || dropType === 'replace_incompatible') { + return 'lnsDragDrop-notCompatible'; + } +}; + export function EmptyDimensionButton({ - dragDropContext, group, layerDatasource, layerDatasourceDropProps, @@ -25,48 +34,58 @@ export function EmptyDimensionButton({ onClick, onDrop, }: { - dragDropContext: DragContextState; layerId: string; groupIndex: number; layerIndex: number; onClick: (id: string) => void; - onDrop: (droppedItem: DragDropIdentifier, dropTarget: DragDropIdentifier) => void; + onDrop: ( + droppedItem: DragDropIdentifier, + dropTarget: DragDropIdentifier, + dropType?: DropType + ) => void; group: VisualizationDimensionGroupConfig; - layerDatasource: Datasource; layerDatasourceDropProps: LayerDatasourceDropProps; }) { - const handleDrop = (droppedItem: DragDropIdentifier) => onDrop(droppedItem, value); + const itemIndex = group.accessors.length; - const value = useMemo(() => { - const newId = generateId(); - return { - columnId: newId, + const [newColumnId, setNewColumnId] = useState(generateId()); + useEffect(() => { + setNewColumnId(generateId()); + }, [itemIndex]); + + const dropType = layerDatasource.getDropTypes({ + ...layerDatasourceDropProps, + columnId: newColumnId, + filterOperations: group.filterOperations, + groupId: group.groupId, + }); + + const value = useMemo( + () => ({ + columnId: newColumnId, groupId: group.groupId, layerId, - isNew: true, - id: newId, - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [group.accessors.length, group.groupId, layerId]); + id: newColumnId, + dropType, + humanData: { + label, + groupLabel: group.groupLabel, + position: itemIndex + 1, + }, + }), + [dropType, newColumnId, group.groupId, layerId, group.groupLabel, itemIndex] + ); return (
    onDrop(droppedItem, value, selectedDropType)} + dropType={dropType} >
    {}, setA11yMessage: jest.fn(), + registerDropTarget: jest.fn(), }; describe('LayerPanel', () => { @@ -224,7 +225,7 @@ describe('LayerPanel', () => { }); it('should not update the visualization if the datasource is incomplete', () => { - (generateId as jest.Mock).mockReturnValueOnce(`newid`); + (generateId as jest.Mock).mockReturnValue(`newid`); const updateAll = jest.fn(); const updateDatasource = jest.fn(); @@ -439,9 +440,14 @@ describe('LayerPanel', () => { ], }); - mockDatasource.canHandleDrop.mockReturnValue(true); + mockDatasource.getDropTypes.mockReturnValue('field_add'); - const draggingField = { field: { name: 'dragged' }, indexPatternId: 'a', id: '1' }; + const draggingField = { + field: { name: 'dragged' }, + indexPatternId: 'a', + id: '1', + humanData: { label: 'Label' }, + }; const component = mountWithIntl( @@ -449,7 +455,7 @@ describe('LayerPanel', () => { ); - expect(mockDatasource.canHandleDrop).toHaveBeenCalledWith( + expect(mockDatasource.getDropTypes).toHaveBeenCalledWith( expect.objectContaining({ dragDropContext: expect.objectContaining({ dragging: draggingField, @@ -482,9 +488,16 @@ describe('LayerPanel', () => { ], }); - mockDatasource.canHandleDrop.mockImplementation(({ columnId }) => columnId !== 'a'); + mockDatasource.getDropTypes.mockImplementation(({ columnId }) => + columnId !== 'a' ? 'field_replace' : undefined + ); - const draggingField = { field: { name: 'dragged' }, indexPatternId: 'a', id: '1' }; + const draggingField = { + field: { name: 'dragged' }, + indexPatternId: 'a', + id: '1', + humanData: { label: 'Label' }, + }; const component = mountWithIntl( @@ -492,13 +505,13 @@ describe('LayerPanel', () => { ); - expect(mockDatasource.canHandleDrop).toHaveBeenCalledWith( + expect(mockDatasource.getDropTypes).toHaveBeenCalledWith( expect.objectContaining({ columnId: 'a' }) ); expect( - component.find('[data-test-subj="lnsGroup"] DragDrop').first().prop('droppable') - ).toEqual(false); + component.find('[data-test-subj="lnsGroup"] DragDrop').first().prop('dropType') + ).toEqual(undefined); component .find('[data-test-subj="lnsGroup"] DragDrop') @@ -533,9 +546,15 @@ describe('LayerPanel', () => { ], }); - mockDatasource.canHandleDrop.mockReturnValue(true); + mockDatasource.getDropTypes.mockReturnValue('replace_compatible'); - const draggingOperation = { layerId: 'first', columnId: 'a', groupId: 'a', id: 'a' }; + const draggingOperation = { + layerId: 'first', + columnId: 'a', + groupId: 'a', + id: 'a', + humanData: { label: 'Label' }, + }; const component = mountWithIntl( @@ -543,7 +562,7 @@ describe('LayerPanel', () => { ); - expect(mockDatasource.canHandleDrop).toHaveBeenCalledWith( + expect(mockDatasource.getDropTypes).toHaveBeenCalledWith( expect.objectContaining({ dragDropContext: expect.objectContaining({ dragging: draggingOperation, @@ -588,7 +607,13 @@ describe('LayerPanel', () => { ], }); - const draggingOperation = { layerId: 'first', columnId: 'a', groupId: 'a', id: 'a' }; + const draggingOperation = { + layerId: 'first', + columnId: 'a', + groupId: 'a', + id: 'a', + humanData: { label: 'Label' }, + }; const component = mountWithIntl( @@ -596,15 +621,10 @@ describe('LayerPanel', () => { ); - component.find(DragDrop).at(1).prop('onDrop')!(draggingOperation, { - layerId: 'first', - columnId: 'b', - groupId: 'a', - id: 'b', - }); + component.find(DragDrop).at(1).prop('onDrop')!(draggingOperation, 'reorder'); expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ - groupId: 'a', + dropType: 'reorder', droppedItem: draggingOperation, }) ); @@ -624,22 +644,24 @@ describe('LayerPanel', () => { ], }); - const draggingOperation = { layerId: 'first', columnId: 'a', groupId: 'a', id: 'a' }; + const draggingOperation = { + layerId: 'first', + columnId: 'a', + groupId: 'a', + id: 'a', + humanData: { label: 'Label' }, + }; const component = mountWithIntl( ); - - component.find('[data-test-subj="lnsGroup"] DragDrop').at(2).prop('onDrop')!( - (draggingOperation as unknown) as DroppableEvent - ); + component.find(DragDrop).at(2).prop('onDrop')!(draggingOperation, 'duplicate_in_group'); expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ - groupId: 'a', + dropType: 'duplicate_in_group', droppedItem: draggingOperation, - isNew: true, }) ); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index bfdd3ec3bb59a..80e9ed05b982d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -11,7 +11,7 @@ import React, { useContext, useState, useEffect, useMemo, useCallback } from 're import { EuiPanel, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { NativeRenderer } from '../../../native_renderer'; -import { StateSetter, Visualization } from '../../../types'; +import { StateSetter, Visualization, DraggedOperation, DropType } from '../../../types'; import { DragContext, DragDropIdentifier, @@ -115,13 +115,19 @@ export function LayerPanel( const layerDatasourceOnDrop = layerDatasource.onDrop; const onDrop = useMemo(() => { - return (droppedItem: DragDropIdentifier, targetItem: DragDropIdentifier) => { - const { columnId, groupId, layerId: targetLayerId, isNew } = (targetItem as unknown) as { - groupId: string; - columnId: string; - layerId: string; - isNew?: boolean; - }; + return ( + droppedItem: DragDropIdentifier, + targetItem: DragDropIdentifier, + dropType?: DropType + ) => { + if (!dropType) { + return; + } + const { + columnId, + groupId, + layerId: targetLayerId, + } = (targetItem as unknown) as DraggedOperation; // TODO: correct misleading name const filterOperations = groups.find(({ groupId: gId }) => gId === targetItem.groupId)?.filterOperations || @@ -131,10 +137,9 @@ export function LayerPanel( ...layerDatasourceDropProps, droppedItem, columnId, - groupId, layerId: targetLayerId, - isNew, filterOperations, + dropType, }); if (dropResult) { updateVisualization( @@ -317,7 +322,6 @@ export function LayerPanel( {group.supportsMoreColumns ? ( { getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()], renderDataPanel: (_element, { dragDropContext: { setDragging, dragging } }) => { if (!dragging || dragging.id !== 'draggedField') { - setDragging({ id: 'draggedField' }); + setDragging({ id: 'draggedField', humanData: { label: 'draggedField' } }); } }, }, @@ -1344,8 +1344,9 @@ describe('editor_frame', () => { indexPatternId: '1', field: {}, id: '1', + humanData: { label: 'draggedField' }, }, - { id: 'lnsWorkspace' } + 'field_replace' ); }); @@ -1424,7 +1425,7 @@ describe('editor_frame', () => { getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()], renderDataPanel: (_element, { dragDropContext: { setDragging, dragging } }) => { if (!dragging || dragging.id !== 'draggedField') { - setDragging({ id: 'draggedField' }); + setDragging({ id: 'draggedField', humanData: { label: '1' } }); } }, }, @@ -1445,8 +1446,11 @@ describe('editor_frame', () => { indexPatternId: '1', field: {}, id: '1', + humanData: { + label: 'label', + }, }, - { id: 'lnsWorkspace' } + 'field_replace' ); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts index bc2abb694eefe..0e8c9b962b995 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts @@ -532,7 +532,7 @@ describe('suggestion helpers', () => { { mockindexpattern: { state: mockDatasourceState, isLoading: false }, }, - { id: 'myfield' }, + { id: 'myfield', humanData: { label: 'myfieldLabel' } }, ]; }); @@ -543,6 +543,9 @@ describe('suggestion helpers', () => { mockDatasourceState, { id: 'myfield', + humanData: { + label: 'myfieldLabel', + }, } ); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index e3385f504763c..48aa56efdb3cc 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -775,7 +775,7 @@ describe('workspace_panel', () => { let mockGetSuggestionForField: jest.Mock; let frame: jest.Mocked; - const draggedField = { id: 'field' }; + const draggedField = { id: 'field', humanData: { label: 'Label' } }; beforeEach(() => { frame = createMockFramePublicAPI(); @@ -793,6 +793,7 @@ describe('workspace_panel', () => { keyboardMode={false} setKeyboardMode={() => {}} setA11yMessage={() => {}} + registerDropTarget={jest.fn()} > { }); initComponent(); - instance.find(DragDrop).prop('onDrop')!(draggedField, { id: 'lnsWorkspace' }); + instance.find(DragDrop).prop('onDrop')!(draggedField, 'field_replace'); expect(mockDispatch).toHaveBeenCalledWith({ type: 'SWITCH_VISUALIZATION', @@ -850,12 +851,12 @@ describe('workspace_panel', () => { visualizationState: {}, }); initComponent(); - expect(instance.find(DragDrop).prop('droppable')).toBeTruthy(); + expect(instance.find(DragDrop).prop('dropType')).toBeTruthy(); }); it('should refuse to drop if there are no suggestions', () => { initComponent(); - expect(instance.find(DragDrop).prop('droppable')).toBeFalsy(); + expect(instance.find(DragDrop).prop('dropType')).toBeFalsy(); }); }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index 208dc823c314c..2c4cecd356ced 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -84,7 +84,17 @@ interface WorkspaceState { expandError: boolean; } -const workspaceDropValue = { id: 'lnsWorkspace' }; +const dropProps = { + value: { + id: 'lnsWorkspace', + humanData: { + label: i18n.translate('xpack.lens.editorFrame.workspaceLabel', { + defaultMessage: 'Workspace', + }), + }, + }, + order: [1, 0, 0, 0], +}; // Exported for testing purposes only. export const WorkspacePanel = React.memo(function WorkspacePanel({ @@ -302,9 +312,10 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({ className="lnsWorkspacePanel__dragDrop" dataTestSubj="lnsWorkspace" draggable={false} - droppable={Boolean(suggestionForDraggedField)} + dropType={suggestionForDraggedField ? 'field_add' : undefined} onDrop={onDrop} - value={workspaceDropValue} + value={dropProps.value} + order={dropProps.order} >
    {renderVisualization()} diff --git a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx index 9bc4e5401f070..61404dd1b71be 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx @@ -88,7 +88,7 @@ export function createMockDatasource(id: string): DatasourceMock { uniqueLabels: jest.fn((_state) => ({})), renderDimensionTrigger: jest.fn(), renderDimensionEditor: jest.fn(), - canHandleDrop: jest.fn(), + getDropTypes: jest.fn(), onDrop: jest.fn(), // this is an additional property which doesn't exist on real datasources diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index e062c152f8ec4..03f281e90f6b5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -281,7 +281,7 @@ describe('IndexPattern Data Panel', () => { setState={setStateSpy} dragDropContext={{ ...createMockedDragDropContext(), - dragging: { id: '1' }, + dragging: { id: '1', humanData: { label: 'Label' } }, }} /> ); @@ -303,7 +303,7 @@ describe('IndexPattern Data Panel', () => { setState={jest.fn()} dragDropContext={{ ...createMockedDragDropContext(), - dragging: { id: '1' }, + dragging: { id: '1', humanData: { label: 'Label' } }, }} changeIndexPattern={jest.fn()} /> @@ -338,7 +338,7 @@ describe('IndexPattern Data Panel', () => { setState, dragDropContext: { ...createMockedDragDropContext(), - dragging: { id: '1' }, + dragging: { id: '1', humanData: { label: 'Label' } }, }, dateRange: { fromDate: '2019-01-01', toDate: '2020-01-01' }, state: { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 3273cdbfe1742..c26d35c4d9a5d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -106,9 +106,6 @@ const bytesColumn: IndexPatternColumn = { * * - Dimension trigger: Not tested here * - Dimension editor component: First half of the tests - * - * - canHandleDrop: Tests for dropping of fields or other dimensions - * - onDrop: Correct application of drop logic */ describe('IndexPatternDimensionEditorPanel', () => { let state: IndexPatternPrivateState; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts index 8c411aa3a5a6c..b374be98748f0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts @@ -7,14 +7,14 @@ import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; import { IndexPatternDimensionEditorProps } from './dimension_panel'; -import { onDrop, canHandleDrop } from './droppable'; +import { onDrop, getDropTypes } from './droppable'; import { DragContextState } from '../../drag_drop'; import { createMockedDragDropContext } from '../mocks'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup, CoreSetup } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { IndexPatternPrivateState } from '../types'; import { documentField } from '../document_field'; -import { OperationMetadata } from '../../types'; +import { OperationMetadata, DropType } from '../../types'; import { IndexPatternColumn } from '../operations'; import { getFieldByNameFactory } from '../pure_helpers'; @@ -66,6 +66,23 @@ const expectedIndexPatterns = { }, }; +const defaultDragging = { + columnId: 'col2', + groupId: 'a', + layerId: 'first', + id: 'col2', + humanData: { + label: 'Column 2', + }, +}; + +const draggingField = { + field: { type: 'number', name: 'bytes', aggregatable: true }, + indexPatternId: 'foo', + id: 'bar', + humanData: { label: 'Label' }, +}; + /** * The datasource exposes four main pieces of code which are tested at * an integration test level. The main reason for this fairly high level @@ -75,7 +92,7 @@ const expectedIndexPatterns = { * - Dimension trigger: Not tested here * - Dimension editor component: First half of the tests * - * - canHandleDrop: Tests for dropping of fields or other dimensions + * - getDropTypes: Returns drop types that are possible for the current dragging field or other dimension * - onDrop: Correct application of drop logic */ describe('IndexPatternDimensionEditorPanel', () => { @@ -157,522 +174,671 @@ describe('IndexPatternDimensionEditorPanel', () => { jest.clearAllMocks(); }); - it('is not droppable if no drag is happening', () => { - expect(canHandleDrop({ ...defaultProps, dragDropContext })).toBe(false); - }); + const groupId = 'a'; + describe('getDropTypes', () => { + it('returns undefined if no drag is happening', () => { + expect(getDropTypes({ ...defaultProps, groupId, dragDropContext })).toBe(undefined); + }); - it('is not droppable if the dragged item has no field', () => { - expect( - canHandleDrop({ - ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: { name: 'bar', id: 'bar' }, - }, - }) - ).toBe(false); - }); + it('returns undefined if the dragged item has no field', () => { + expect( + getDropTypes({ + ...defaultProps, + groupId, + dragDropContext: { + ...dragDropContext, + dragging: { name: 'bar', id: 'bar', humanData: { label: 'Label' } }, + }, + }) + ).toBe(undefined); + }); - it('is not droppable if field is not supported by filterOperations', () => { - expect( - canHandleDrop({ - ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: { - indexPatternId: 'foo', - field: { type: 'string', name: 'mystring', aggregatable: true }, - id: 'mystring', + it('returns undefined if field is not supported by filterOperations', () => { + expect( + getDropTypes({ + ...defaultProps, + groupId, + dragDropContext: { + ...dragDropContext, + dragging: { + indexPatternId: 'foo', + field: { type: 'string', name: 'mystring', aggregatable: true }, + id: 'mystring', + humanData: { label: 'Label' }, + }, + }, + filterOperations: () => false, + }) + ).toBe(undefined); + }); + + it('returns remove_add if the field is supported by filterOperations and the dropTarget is an existing column', () => { + expect( + getDropTypes({ + ...defaultProps, + groupId, + dragDropContext: { + ...dragDropContext, + dragging: draggingField, + }, + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + }) + ).toBe('field_replace'); + }); + + it('returns undefined if the field belongs to another index pattern', () => { + expect( + getDropTypes({ + ...defaultProps, + groupId, + dragDropContext: { + ...dragDropContext, + dragging: { + field: { type: 'number', name: 'bar', aggregatable: true }, + indexPatternId: 'foo2', + id: 'bar', + humanData: { label: 'Label' }, + }, + }, + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + }) + ).toBe(undefined); + }); + + it('returns undefined if the dragged field is already in use by this operation', () => { + expect( + getDropTypes({ + ...defaultProps, + groupId, + dragDropContext: { + ...dragDropContext, + dragging: { + field: { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + exists: true, + }, + indexPatternId: 'foo', + id: 'bar', + humanData: { label: 'Label' }, + }, + }, + }) + ).toBe(undefined); + }); + + it('returns move if the dragged column is compatible', () => { + expect( + getDropTypes({ + ...defaultProps, + groupId, + dragDropContext: { + ...dragDropContext, + dragging: { + columnId: 'col1', + groupId: 'b', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, + }, + }, + columnId: 'col2', + }) + ).toBe('move_compatible'); + }); + + it('returns undefined if the dragged column from different group uses the same field as the dropTarget', () => { + const testState = { ...state }; + testState.layers.first = { + indexPatternId: 'foo', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: testState.layers.first.columns.col1, + + col2: { + label: 'Date histogram of timestamp (1)', + customLabel: true, + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + params: { + interval: '1d', + }, + sourceField: 'timestamp', }, }, - filterOperations: () => false, - }) - ).toBe(false); - }); + }; - it('is droppable if the field is supported by filterOperations', () => { - expect( - canHandleDrop({ - ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: { - field: { type: 'number', name: 'bytes', aggregatable: true }, - indexPatternId: 'foo', - id: 'bar', + expect( + getDropTypes({ + ...defaultProps, + groupId, + dragDropContext: { + ...dragDropContext, + dragging: { + columnId: 'col1', + groupId: 'b', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, + }, + }, + columnId: 'col2', + }) + ).toEqual(undefined); + }); + + it('returns replace_incompatible if dropping column to existing incompatible column', () => { + const testState = { ...state }; + testState.layers.first = { + indexPatternId: 'foo', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: testState.layers.first.columns.col1, + + col2: { + label: 'Sum of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'sum', + sourceField: 'bytes', }, }, - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - }) - ).toBe(true); - }); + }; - it('is not droppable if the field belongs to another index pattern', () => { - expect( - canHandleDrop({ + expect( + getDropTypes({ + ...defaultProps, + groupId, + dragDropContext: { + ...dragDropContext, + dragging: { + columnId: 'col1', + groupId: 'b', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, + }, + }, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.isBucketed === false, + }) + ).toEqual('replace_incompatible'); + }); + }); + describe('onDrop', () => { + it('appends the dropped column when a field is dropped', () => { + onDrop({ ...defaultProps, dragDropContext: { ...dragDropContext, - dragging: { - field: { type: 'number', name: 'bar', aggregatable: true }, - indexPatternId: 'foo2', - id: 'bar', - }, + dragging: draggingField, }, + droppedItem: draggingField, + dropType: 'field_replace', + columnId: 'col2', filterOperations: (op: OperationMetadata) => op.dataType === 'number', - }) - ).toBe(false); - }); + }); - it('is not droppable if the dragged field is already in use by this operation', () => { - expect( - canHandleDrop({ - ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: { - field: { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - exists: true, + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columnOrder: ['col1', 'col2'], + columns: { + ...state.layers.first.columns, + col2: expect.objectContaining({ + dataType: 'number', + sourceField: 'bytes', + }), }, - indexPatternId: 'foo', - id: 'bar', }, }, - }) - ).toBe(false); - }); + }); + }); - it('is droppable if the dragged column is compatible', () => { - expect( - canHandleDrop({ + it('selects the specific operation that was valid on drop', () => { + onDrop({ ...defaultProps, dragDropContext: { ...dragDropContext, - dragging: { - columnId: 'col1', - groupId: 'a', - layerId: 'first', - id: 'col1', - }, + dragging: draggingField, }, + droppedItem: draggingField, columnId: 'col2', - }) - ).toBe(true); - }); + filterOperations: (op: OperationMetadata) => op.isBucketed, + dropType: 'field_replace', + }); - it('is not droppable if the dragged column is the same as the current column', () => { - expect( - canHandleDrop({ - ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: { - columnId: 'col1', - groupId: 'a', - layerId: 'first', - id: 'bar', + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columnOrder: ['col1', 'col2'], + columns: { + ...state.layers.first.columns, + col2: expect.objectContaining({ + dataType: 'number', + sourceField: 'bytes', + }), + }, }, }, - }) - ).toBe(false); - }); + }); + }); - it('is not droppable if the dragged column is incompatible', () => { - expect( - canHandleDrop({ + it('updates a column when a field is dropped', () => { + onDrop({ ...defaultProps, dragDropContext: { ...dragDropContext, - dragging: { - columnId: 'col1', - groupId: 'a', - layerId: 'first', - id: 'bar', - }, + dragging: draggingField, }, - columnId: 'col2', + droppedItem: draggingField, filterOperations: (op: OperationMetadata) => op.dataType === 'number', - }) - ).toBe(false); - }); - - it('appends the dropped column when a field is dropped', () => { - const dragging = { - field: { type: 'number', name: 'bytes', aggregatable: true }, - indexPatternId: 'foo', - id: 'bar', - }; - - onDrop({ - ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging, - }, - droppedItem: dragging, - columnId: 'col2', - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - groupId: '1', - }); + dropType: 'field_replace', + }); - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columnOrder: ['col1', 'col2'], - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - dataType: 'number', - sourceField: 'bytes', + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: expect.objectContaining({ + columns: expect.objectContaining({ + col1: expect.objectContaining({ + dataType: 'number', + sourceField: 'bytes', + }), }), - }, + }), }, - }, + }); }); - }); - it('selects the specific operation that was valid on drop', () => { - const dragging = { - field: { type: 'string', name: 'source', aggregatable: true }, - indexPatternId: 'foo', - id: 'bar', - }; - onDrop({ - ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging, - }, - droppedItem: dragging, - columnId: 'col2', - filterOperations: (op: OperationMetadata) => op.isBucketed, - groupId: '1', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columnOrder: ['col2', 'col1'], - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - dataType: 'string', - sourceField: 'source', - }), + it('keeps the operation when dropping a different compatible field', () => { + const dragging = { + field: { name: 'memory', type: 'number', aggregatable: true }, + indexPatternId: 'foo', + id: '1', + humanData: { label: 'Label' }, + }; + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: { + field: { name: 'memory', type: 'number', aggregatable: true }, + indexPatternId: 'foo', + id: '1', + }, + state: { + ...state, + layers: { + first: { + indexPatternId: 'foo', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Sum of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'sum', + sourceField: 'bytes', + }, + }, + }, }, }, - }, - }); - }); + dropType: 'field_replace', + }); - it('updates a column when a field is dropped', () => { - const dragging = { - field: { type: 'number', name: 'bytes', aggregatable: true }, - indexPatternId: 'foo', - id: 'bar', - }; - onDrop({ - ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging, - }, - droppedItem: dragging, - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - groupId: '1', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: expect.objectContaining({ - columns: expect.objectContaining({ - col1: expect.objectContaining({ - dataType: 'number', - sourceField: 'bytes', + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: expect.objectContaining({ + columns: expect.objectContaining({ + col1: expect.objectContaining({ + operationType: 'sum', + dataType: 'number', + sourceField: 'memory', + }), }), }), - }), - }, + }, + }); }); - }); - it('keeps the operation when dropping a different compatible field', () => { - const dragging = { - field: { name: 'memory', type: 'number', aggregatable: true }, - indexPatternId: 'foo', - id: '1', - }; - onDrop({ - ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging, - }, - droppedItem: dragging, - state: { + it('updates the column id when moving an operation to an empty dimension', () => { + const dragging = { + columnId: 'col1', + groupId: 'a', + layerId: 'first', + id: 'bar', + humanData: { label: 'Label' }, + }; + + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + columnId: 'col2', + dropType: 'move_compatible', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ ...state, layers: { first: { - indexPatternId: 'foo', - columnOrder: ['col1'], + ...state.layers.first, + columnOrder: ['col2'], columns: { - col1: { - label: 'Sum of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'sum', - sourceField: 'bytes', - }, + col2: state.layers.first.columns.col1, }, }, }, - }, - groupId: '1', + }); }); - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: expect.objectContaining({ - columns: expect.objectContaining({ - col1: expect.objectContaining({ - operationType: 'sum', - dataType: 'number', - sourceField: 'memory', - }), - }), - }), - }, - }); - }); - - it('updates the column id when moving an operation to an empty dimension', () => { - const dragging = { - columnId: 'col1', - groupId: 'a', - layerId: 'first', - id: 'bar', - }; - - onDrop({ - ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging, - }, - droppedItem: dragging, - columnId: 'col2', - groupId: '1', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columnOrder: ['col2'], - columns: { - col2: state.layers.first.columns.col1, + it('replaces an operation when moving to a populated dimension', () => { + const testState = { ...state }; + testState.layers.first = { + indexPatternId: 'foo', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: testState.layers.first.columns.col1, + + col2: { + label: 'Top values of src', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + params: { + orderBy: { type: 'column', columnId: 'col3' }, + orderDirection: 'desc', + size: 10, + }, + sourceField: 'src', }, - }, - }, - }); - }); - - it('replaces an operation when moving to a populated dimension', () => { - const dragging = { - columnId: 'col2', - groupId: 'a', - layerId: 'first', - id: 'col2', - }; - const testState = { ...state }; - testState.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: testState.layers.first.columns.col1, - - col2: { - label: 'Top values of src', - dataType: 'string', - isBucketed: true, - - // Private - operationType: 'terms', - params: { - orderBy: { type: 'column', columnId: 'col3' }, - orderDirection: 'desc', - size: 10, + col3: { + label: 'Count', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'count', + sourceField: 'Records', }, - sourceField: 'src', }, - col3: { - label: 'Count', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'count', - sourceField: 'Records', - }, - }, - }; + }; - onDrop({ - ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging, - }, - droppedItem: dragging, - state: testState, - groupId: '1', - }); + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging: defaultDragging, + }, + droppedItem: defaultDragging, + state: testState, + dropType: 'replace_compatible', + }); - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'col3'], - columns: { - col1: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col3'], + columns: { + col1: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + }, }, }, - }, + }); }); - }); - it('if dnd is reorder, it correctly reorders columns', () => { - const dragging = { - columnId: 'col1', - groupId: 'a', - layerId: 'first', - id: 'col1', - }; - const testState = { - ...state, - layers: { - first: { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: { - label: 'Date histogram of timestamp', - dataType: 'date', - isBucketed: true, - } as IndexPatternColumn, - col2: { - label: 'Top values of bar', - dataType: 'number', - isBucketed: true, - } as IndexPatternColumn, - col3: { - label: 'Top values of memory', - dataType: 'number', - isBucketed: true, - } as IndexPatternColumn, + it('copies a dimension if dropType is duplicate_in_group, respecting bucket metric order', () => { + const testState = { ...state }; + testState.layers.first = { + indexPatternId: 'foo', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: testState.layers.first.columns.col1, + + col2: { + label: 'Top values of src', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + params: { + orderBy: { type: 'column', columnId: 'col3' }, + orderDirection: 'desc', + size: 10, + }, + sourceField: 'src', + }, + col3: { + label: 'Count', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'count', + sourceField: 'Records', }, }, - }, - }; + }; - const defaultReorderDropParams = { - ...defaultProps, - isReorder: true, - dragDropContext: { - ...dragDropContext, - dragging, - }, - droppedItem: dragging, - state: testState, - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - groupId: 'a', - }; + const metricDragging = { + columnId: 'col3', + groupId: 'a', + layerId: 'first', + id: 'col3', + humanData: { label: 'Label' }, + }; - const stateWithColumnOrder = (columnOrder: string[]) => { - return { + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging: metricDragging, + }, + droppedItem: metricDragging, + state: testState, + dropType: 'duplicate_in_group', + columnId: 'newCol', + }); + // metric is appended + expect(setState).toHaveBeenCalledWith({ ...testState, layers: { first: { ...testState.layers.first, - columnOrder, + columnOrder: ['col1', 'col2', 'col3', 'newCol'], columns: { - ...testState.layers.first.columns, + col1: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + newCol: testState.layers.first.columns.col3, }, }, }, - }; - }; - - // first element to last - onDrop({ - ...defaultReorderDropParams, - columnId: 'col3', - }); - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col2', 'col3', 'col1'])); + }); - // last element to first - onDrop({ - ...defaultReorderDropParams, - columnId: 'col1', - droppedItem: { - columnId: 'col3', - groupId: 'a', - layerId: 'first', - id: 'col3', - }, - }); - expect(setState).toBeCalledTimes(2); - expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col3', 'col1', 'col2'])); - - // middle column to first - onDrop({ - ...defaultReorderDropParams, - columnId: 'col1', - droppedItem: { + const bucketDragging = { columnId: 'col2', groupId: 'a', layerId: 'first', id: 'col2', - }, + humanData: { label: 'Label' }, + }; + + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging: bucketDragging, + }, + droppedItem: bucketDragging, + state: testState, + dropType: 'duplicate_in_group', + columnId: 'newCol', + }); + + // bucket is placed after the last existing bucket + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col2', 'newCol', 'col3'], + columns: { + col1: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + newCol: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + }, + }, + }, + }); }); - expect(setState).toBeCalledTimes(3); - expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col2', 'col1', 'col3'])); - - // middle column to last - onDrop({ - ...defaultReorderDropParams, - columnId: 'col3', - droppedItem: { - columnId: 'col2', + + it('if dropType is reorder, it correctly reorders columns', () => { + const dragging = { + columnId: 'col1', groupId: 'a', layerId: 'first', - id: 'col2', - }, + id: 'col1', + humanData: { label: 'Label' }, + }; + const testState = { + ...state, + layers: { + first: { + indexPatternId: 'foo', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: { + label: 'Date histogram of timestamp', + dataType: 'date', + isBucketed: true, + } as IndexPatternColumn, + col2: { + label: 'Top values of bar', + dataType: 'number', + isBucketed: true, + } as IndexPatternColumn, + col3: { + label: 'Top values of memory', + dataType: 'number', + isBucketed: true, + } as IndexPatternColumn, + }, + }, + }, + }; + + const defaultReorderDropParams = { + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + dropType: 'reorder' as DropType, + }; + + const stateWithColumnOrder = (columnOrder: string[]) => { + return { + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder, + columns: { + ...testState.layers.first.columns, + }, + }, + }, + }; + }; + + // first element to last + onDrop({ + ...defaultReorderDropParams, + columnId: 'col3', + }); + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col2', 'col3', 'col1'])); + + // last element to first + onDrop({ + ...defaultReorderDropParams, + columnId: 'col1', + droppedItem: { + columnId: 'col3', + groupId: 'a', + layerId: 'first', + id: 'col3', + }, + }); + expect(setState).toBeCalledTimes(2); + expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col3', 'col1', 'col2'])); + + // middle column to first + onDrop({ + ...defaultReorderDropParams, + columnId: 'col1', + droppedItem: { + columnId: 'col2', + groupId: 'a', + layerId: 'first', + id: 'col2', + }, + }); + expect(setState).toBeCalledTimes(3); + expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col2', 'col1', 'col3'])); + + // middle column to last + onDrop({ + ...defaultReorderDropParams, + columnId: 'col3', + droppedItem: { + columnId: 'col2', + groupId: 'a', + layerId: 'first', + id: 'col2', + }, + }); + expect(setState).toBeCalledTimes(4); + expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col1', 'col3', 'col2'])); }); - expect(setState).toBeCalledTimes(4); - expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col1', 'col3', 'col2'])); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts index 3fa40911062cf..cbd599743f813 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts @@ -12,39 +12,46 @@ import { DraggedOperation, } from '../../types'; import { IndexPatternColumn } from '../indexpattern'; -import { insertOrReplaceColumn } from '../operations'; +import { insertOrReplaceColumn, deleteColumn } from '../operations'; import { mergeLayer } from '../state_helpers'; import { hasField, isDraggedField } from '../utils'; -import { IndexPatternPrivateState, IndexPatternField } from '../types'; +import { IndexPatternPrivateState, IndexPatternField, DraggedField } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; -import { getOperationSupportMatrix, OperationSupportMatrix } from './operation_support'; +import { getOperationSupportMatrix } from './operation_support'; -type DropHandlerProps = Pick< - DatasourceDimensionDropHandlerProps, - 'columnId' | 'setState' | 'state' | 'layerId' | 'droppedItem' -> & { +type DropHandlerProps = DatasourceDimensionDropHandlerProps & { droppedItem: T; - operationSupportMatrix: OperationSupportMatrix; }; -export function canHandleDrop(props: DatasourceDimensionDropProps) { - const operationSupportMatrix = getOperationSupportMatrix(props); - +export function getDropTypes( + props: DatasourceDimensionDropProps & { groupId: string } +) { const { dragging } = props.dragDropContext; + if (!dragging) { + return; + } + const layerIndexPatternId = props.state.layers[props.layerId].indexPatternId; function hasOperationForField(field: IndexPatternField) { - return Boolean(operationSupportMatrix.operationByField[field.name]); + return !!getOperationSupportMatrix(props).operationByField[field.name]; } + const currentColumn = props.state.layers[props.layerId].columns[props.columnId]; if (isDraggedField(dragging)) { - const currentColumn = props.state.layers[props.layerId].columns[props.columnId]; - return Boolean( - layerIndexPatternId === dragging.indexPatternId && - Boolean(hasOperationForField(dragging.field)) && - (!currentColumn || - (hasField(currentColumn) && currentColumn.sourceField !== dragging.field.name)) - ); + if ( + !!(layerIndexPatternId === dragging.indexPatternId && hasOperationForField(dragging.field)) + ) { + if (!currentColumn) { + return 'field_add'; + } else if ( + (hasField(currentColumn) && currentColumn.sourceField !== dragging.field.name) || + !hasField(currentColumn) + ) { + return 'field_replace'; + } + } + return; } if ( @@ -52,12 +59,72 @@ export function canHandleDrop(props: DatasourceDimensionDropProps) { + const { droppedItem, dropType } = props; + + if (dropType === 'field_add' || dropType === 'field_replace') { + return operationOnDropMap[dropType]({ + ...props, + droppedItem: droppedItem as DraggedField, + }); + } + return operationOnDropMap[dropType]({ + ...props, + droppedItem: droppedItem as DraggedOperation, + }); +} + +const operationOnDropMap = { + field_add: onFieldDrop, + field_replace: onFieldDrop, + reorder: onReorderDrop, + duplicate_in_group: onSameGroupDuplicateDrop, + move_compatible: onMoveDropToCompatibleGroup, + replace_compatible: onMoveDropToCompatibleGroup, + move_incompatible: onMoveDropToNonCompatibleGroup, + replace_incompatible: onMoveDropToNonCompatibleGroup, +}; + function reorderElements(items: string[], dest: string, src: string) { const result = items.filter((c) => c !== src); const destIndex = items.findIndex((c) => c === src); @@ -69,7 +136,13 @@ function reorderElements(items: string[], dest: string, src: string) { return result; } -const onReorderDrop = ({ columnId, setState, state, layerId, droppedItem }: DropHandlerProps) => { +function onReorderDrop({ + columnId, + setState, + state, + layerId, + droppedItem, +}: DropHandlerProps) { setState( mergeLayer({ state, @@ -85,15 +158,98 @@ const onReorderDrop = ({ columnId, setState, state, layerId, droppedItem }: Drop ); return true; -}; +} + +function onMoveDropToNonCompatibleGroup(props: DropHandlerProps) { + const { columnId, setState, state, layerId, droppedItem } = props; + + const layer = state.layers[layerId]; + const op = { ...layer.columns[droppedItem.columnId] }; + const field = + hasField(op) && state.indexPatterns[layer.indexPatternId].getFieldByName(op.sourceField); + if (!field) { + return false; + } + + const operationSupportMatrix = getOperationSupportMatrix(props); + const operationsForNewField = operationSupportMatrix.operationByField[field.name]; + + if (!operationsForNewField || operationsForNewField.size === 0) { + return false; + } + + const currentIndexPattern = state.indexPatterns[layer.indexPatternId]; + + const newLayer = insertOrReplaceColumn({ + layer: deleteColumn({ + layer, + columnId: droppedItem.columnId, + indexPattern: currentIndexPattern, + }), + columnId, + indexPattern: currentIndexPattern, + op: operationsForNewField.values().next().value, + field, + }); + + trackUiEvent('drop_onto_dimension'); + setState( + mergeLayer({ + state, + layerId, + newLayer: { + ...newLayer, + }, + }) + ); + + return { deleted: droppedItem.columnId }; +} -const onMoveDropToCompatibleGroup = ({ +function onSameGroupDuplicateDrop({ columnId, setState, state, layerId, droppedItem, -}: DropHandlerProps) => { +}: DropHandlerProps) { + const layer = state.layers[layerId]; + + const op = { ...layer.columns[droppedItem.columnId] }; + const newColumns = { + ...layer.columns, + [columnId]: op, + }; + + const newColumnOrder = [...layer.columnOrder]; + // put a new bucketed dimension just in front of the metric dimensions, a metric dimension in the back of the array + // TODO this logic does not take into account groups - we probably need to pass the current + // group config to this position to place the column right + const insertionIndex = op.isBucketed + ? newColumnOrder.findIndex((id) => !newColumns[id].isBucketed) + : newColumnOrder.length; + newColumnOrder.splice(insertionIndex, 0, columnId); + // Time to replace + setState( + mergeLayer({ + state, + layerId, + newLayer: { + columnOrder: newColumnOrder, + columns: newColumns, + }, + }) + ); + return true; +} + +function onMoveDropToCompatibleGroup({ + columnId, + setState, + state, + layerId, + droppedItem, +}: DropHandlerProps) { const layer = state.layers[layerId]; const op = { ...layer.columns[droppedItem.columnId] }; const newColumns = { ...layer.columns }; @@ -122,18 +278,14 @@ const onMoveDropToCompatibleGroup = ({ }) ); return { deleted: droppedItem.columnId }; -}; +} + +function onFieldDrop(props: DropHandlerProps) { + const { columnId, setState, state, layerId, droppedItem } = props; + const operationSupportMatrix = getOperationSupportMatrix(props); -const onFieldDrop = ({ - columnId, - setState, - state, - layerId, - droppedItem, - operationSupportMatrix, -}: DropHandlerProps) => { function hasOperationForField(field: IndexPatternField) { - return Boolean(operationSupportMatrix.operationByField[field.name]); + return !!operationSupportMatrix.operationByField[field.name]; } if (!isDraggedField(droppedItem) || !hasOperationForField(droppedItem.field)) { @@ -176,55 +328,4 @@ const onFieldDrop = ({ trackUiEvent(hasData ? 'drop_non_empty' : 'drop_empty'); setState(mergeLayer({ state, layerId, newLayer })); return true; -}; - -export function onDrop(props: DatasourceDimensionDropHandlerProps) { - const operationSupportMatrix = getOperationSupportMatrix(props); - const { setState, state, droppedItem, columnId, layerId, groupId, isNew } = props; - - if (!isDraggedOperation(droppedItem)) { - return onFieldDrop({ - columnId, - setState, - state, - layerId, - droppedItem, - operationSupportMatrix, - }); - } - const isExistingFromSameGroup = - droppedItem.groupId === groupId && droppedItem.columnId !== columnId && !isNew; - - // reorder in the same group - if (isExistingFromSameGroup) { - return onReorderDrop({ - columnId, - setState, - state, - layerId, - droppedItem, - operationSupportMatrix, - }); - } - - // replace or move to compatible group - const isFromOtherGroup = droppedItem.groupId !== groupId && droppedItem.layerId === layerId; - - if (isFromOtherGroup) { - const layer = state.layers[layerId]; - const op = { ...layer.columns[droppedItem.columnId] }; - - if (props.filterOperations(op)) { - return onMoveDropToCompatibleGroup({ - columnId, - setState, - state, - layerId, - droppedItem, - operationSupportMatrix, - }); - } - } - - return false; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.scss b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.scss index 8c10ca9d30b73..8a6e10c8be6e4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.scss +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.scss @@ -1,4 +1,5 @@ .lnsFieldItem { + width: 100%; .lnsFieldItem__infoIcon { visibility: hidden; opacity: 0; @@ -13,6 +14,23 @@ transition: opacity $euiAnimSpeedFast ease-in-out 1s; } } + + &:focus, + &:focus-within, + &.kbnFieldButton-isActive { + animation: none !important; // sass-lint:disable-line no-important + } + + &:focus .kbnFieldButton__name span, + &:focus-within .kbnFieldButton__name span, + &.kbnFieldButton-isActive .kbnFieldButton__name span { + background-color: transparentize($euiColorVis1, .9) !important; + text-decoration: underline !important; + } +} + +.kbnFieldButton__name { + transition: background-color $euiAnimSpeedFast ease-in-out; } .lnsFieldItem--missing { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index e598e85f2ff17..e0198d6d7903e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -48,11 +48,10 @@ import { } from '../../../../../src/plugins/data/public'; import { FieldButton } from '../../../../../src/plugins/kibana_react/public'; import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; -import { DraggedField } from './indexpattern'; import { DragDrop, DragDropIdentifier } from '../drag_drop'; import { DatasourceDataPanelProps, DataType } from '../types'; import { BucketedAggregation, FieldStatsResponse } from '../../common'; -import { IndexPattern, IndexPatternField } from './types'; +import { IndexPattern, IndexPatternField, DraggedField } from './types'; import { LensFieldIcon } from './lens_field_icon'; import { trackUiEvent } from '../lens_ui_telemetry'; @@ -103,6 +102,8 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { dateRange, filters, hideDetails, + itemIndex, + groupIndex, dropOntoWorkspace, } = props; @@ -167,9 +168,18 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { } const value = useMemo( - () => ({ field, indexPatternId: indexPattern.id, id: field.name } as DraggedField), - [field, indexPattern.id] + () => ({ + field, + indexPatternId: indexPattern.id, + id: field.name, + humanData: { + label: field.displayName, + position: itemIndex + 1, + }, + }), + [field, indexPattern.id, itemIndex] ); + const order = useMemo(() => [0, groupIndex, itemIndex], [groupIndex, itemIndex]); const lensFieldIcon = ; const lensInfoIcon = ( @@ -204,9 +214,8 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { container={document.querySelector('.application') || undefined} button={ @@ -271,6 +280,9 @@ function FieldPanelHeader({ indexPatternId, id: field.name, field, + humanData: { + label: field.displayName, + }, }; return ( @@ -641,11 +653,7 @@ const DragToWorkspaceButton = ({ dropOntoWorkspace, isEnabled, }: { - field: { - indexPatternId: string; - id: string; - field: IndexPatternField; - }; + field: DraggedField; dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace']; isEnabled: boolean; }) => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 5571700b15b61..6cc89d3dab119 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -31,7 +31,7 @@ import { toExpression } from './to_expression'; import { IndexPatternDimensionTrigger, IndexPatternDimensionEditor, - canHandleDrop, + getDropTypes, onDrop, } from './dimension_panel'; import { IndexPatternDataPanel } from './datapanel'; @@ -44,7 +44,7 @@ import { import { isDraggedField, normalizeOperationDataType } from './utils'; import { LayerPanel } from './layerpanel'; import { IndexPatternColumn, getErrorMessages, IncompleteColumn } from './operations'; -import { IndexPatternField, IndexPatternPrivateState, IndexPatternPersistedState } from './types'; +import { IndexPatternPrivateState, IndexPatternPersistedState } from './types'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { VisualizeFieldContext } from '../../../../../src/plugins/ui_actions/public'; @@ -52,15 +52,9 @@ import { mergeLayer } from './state_helpers'; import { Datasource, StateSetter } from '../types'; import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; import { deleteColumn, isReferenced } from './operations'; -import { DragDropIdentifier } from '../drag_drop/providers'; export { OperationType, IndexPatternColumn, deleteColumn } from './operations'; -export type DraggedField = DragDropIdentifier & { - field: IndexPatternField; - indexPatternId: string; -}; - export function columnToOperation(column: IndexPatternColumn, uniqueLabel?: string): Operation { const { dataType, label, isBucketed, scale } = column; return { @@ -314,8 +308,7 @@ export function getIndexPatternDatasource({ domElement ); }, - - canHandleDrop, + getDropTypes, onDrop, // Reset the temporary invalid state when closing the editor, but don't diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts index 306c87fa765e5..06560bb0fa244 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts @@ -253,5 +253,6 @@ export function createMockedDragDropContext(): jest.Mocked { keyboardMode: false, setKeyboardMode: jest.fn(), setA11yMessage: jest.fn(), + registerDropTarget: jest.fn(), }; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts index 10b1f7f1799da..f45f963ee174f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts @@ -8,6 +8,7 @@ import { IFieldType } from 'src/plugins/data/common'; import { IndexPatternColumn, IncompleteColumn } from './operations'; import { IndexPatternAggRestrictions } from '../../../../../src/plugins/data/public'; +import { DragDropIdentifier } from '../drag_drop/providers'; export { IndexPatternColumn, @@ -32,6 +33,10 @@ export { MovingAverageIndexPatternColumn, } from './operations'; +export type DraggedField = DragDropIdentifier & { + field: IndexPatternField; + indexPatternId: string; +}; export interface IndexPattern { id: string; fields: IndexPatternField[]; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts index 515d205637505..d4c9da188be61 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts @@ -6,8 +6,7 @@ */ import { DataType } from '../types'; -import { IndexPattern, IndexPatternLayer } from './types'; -import { DraggedField } from './indexpattern'; +import { IndexPattern, IndexPatternLayer, DraggedField } from './types'; import type { BaseIndexPatternColumn, FieldBasedIndexPatternColumn, diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 4b1c0f755b3ae..cccc35acb3fca 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -138,6 +138,16 @@ export type TableChangeType = | 'reorder' | 'layers'; +export type DropType = + | 'field_add' + | 'field_replace' + | 'reorder' + | 'duplicate_in_group' + | 'move_compatible' + | 'replace_compatible' + | 'move_incompatible' + | 'replace_incompatible'; + export interface DatasourceSuggestion { state: T; table: TableSuggestion; @@ -179,7 +189,9 @@ export interface Datasource { renderDimensionTrigger: (domElement: Element, props: DatasourceDimensionTriggerProps) => void; renderDimensionEditor: (domElement: Element, props: DatasourceDimensionEditorProps) => void; renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => void; - canHandleDrop: (props: DatasourceDimensionDropProps) => boolean; + getDropTypes: ( + props: DatasourceDimensionDropProps & { groupId: string } + ) => DropType | undefined; onDrop: (props: DatasourceDimensionDropHandlerProps) => false | true | { deleted: string }; updateStateOnCloseDimension?: (props: { layerId: string; @@ -299,13 +311,11 @@ export type DatasourceDimensionDropProps = SharedDimensionProps & { state: T; setState: StateSetter; dragDropContext: DragContextState; - isReorder?: boolean; }; export type DatasourceDimensionDropHandlerProps = DatasourceDimensionDropProps & { droppedItem: unknown; - groupId: string; - isNew?: boolean; + dropType: DropType; }; export type DataType = 'document' | 'string' | 'number' | 'date' | 'boolean' | 'ip'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3cfaaba5f8538..6658671b84682 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11183,8 +11183,6 @@ "xpack.lens.dimensionContainer.close": "閉じる", "xpack.lens.dimensionContainer.closeConfiguration": "構成を閉じる", "xpack.lens.discover.visualizeFieldLegend": "Visualize フィールド", - "xpack.lens.dragDrop.elementLifted": "位置 {position} のアイテム {itemLabel} を持ち上げました。", - "xpack.lens.dragDrop.elementMoved": "位置 {prevPosition} から位置 {position} までアイテム {itemLabel} を移動しました", "xpack.lens.editLayerSettings": "レイヤー設定を編集", "xpack.lens.editLayerSettingsChartType": "レイヤー設定を編集、{chartType}", "xpack.lens.editorFrame.buildExpressionError": "グラフの準備中に予期しないエラーが発生しました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ebc0c6f88ecfa..9602583e8d215 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11212,8 +11212,6 @@ "xpack.lens.dimensionContainer.close": "关闭", "xpack.lens.dimensionContainer.closeConfiguration": "关闭配置", "xpack.lens.discover.visualizeFieldLegend": "可视化字段", - "xpack.lens.dragDrop.elementLifted": "您已将项目 {itemLabel} 提升到位置 {position}", - "xpack.lens.dragDrop.elementMoved": "您已将项目 {itemLabel} 从位置 {prevPosition} 移到位置 {position}", "xpack.lens.editLayerSettings": "编辑图层设置", "xpack.lens.editLayerSettingsChartType": "编辑图层设置 {chartType}", "xpack.lens.editorFrame.buildExpressionError": "准备图表时发生意外错误", diff --git a/x-pack/test/functional/apps/lens/drag_and_drop.ts b/x-pack/test/functional/apps/lens/drag_and_drop.ts index 7f8d60f9ffccf..5b3a984f00519 100644 --- a/x-pack/test/functional/apps/lens/drag_and_drop.ts +++ b/x-pack/test/functional/apps/lens/drag_and_drop.ts @@ -53,7 +53,7 @@ export default function ({ getPageObjects }: FtrProviderContext) { }); it('should reorder the elements for the table', async () => { - await PageObjects.lens.reorderDimensions('lnsDatatable_column', 2, 0); + await PageObjects.lens.reorderDimensions('lnsDatatable_column', 3, 1); await PageObjects.header.waitUntilLoadingHasFinished(); expect(await PageObjects.lens.getDimensionTriggersTexts('lnsDatatable_column')).to.eql([ 'Top values of @message.raw', @@ -83,6 +83,129 @@ export default function ({ getPageObjects }: FtrProviderContext) { await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') ).to.eql(['Top values of @message.raw']); }); + + it('should move the column to non-compatible dimension group', async () => { + expect( + await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') + ).to.eql(['Top values of @message.raw']); + + await PageObjects.lens.dragDimensionToDimension( + 'lnsXY_splitDimensionPanel > lns-dimensionTrigger', + 'lnsXY_yDimensionPanel > lns-dimensionTrigger' + ); + + expect( + await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') + ).to.eql([]); + expect( + await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') + ).to.eql([]); + expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_yDimensionPanel')).to.eql([ + 'Unique count of @message.raw', + ]); + }); + it('should duplicate the column when dragging to empty dimension in the same group', async () => { + await PageObjects.lens.dragDimensionToDimension( + 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + 'lnsXY_yDimensionPanel > lns-empty-dimension' + ); + await PageObjects.lens.dragDimensionToDimension( + 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + 'lnsXY_yDimensionPanel > lns-empty-dimension' + ); + expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_yDimensionPanel')).to.eql([ + 'Unique count of @message.raw', + 'Unique count of @message.raw [1]', + 'Unique count of @message.raw [2]', + ]); + }); + it('should duplicate the column when dragging to empty dimension in the same group', async () => { + await PageObjects.lens.dragDimensionToDimension( + 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + 'lnsXY_xDimensionPanel > lns-empty-dimension' + ); + expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_yDimensionPanel')).to.eql([ + 'Unique count of @message.raw', + 'Unique count of @message.raw [1]', + ]); + expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql([ + 'Top values of @message.raw', + ]); + }); + }); + describe('keyboard drag and drop', () => { + it('should drop a field to workspace', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.dragFieldWithKeyboard('@timestamp'); + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel')).to.eql( + '@timestamp' + ); + }); + it('should drop a field to empty dimension', async () => { + await PageObjects.lens.dragFieldWithKeyboard('bytes', 4); + expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_yDimensionPanel')).to.eql([ + 'Count of records', + 'Average of bytes', + ]); + await PageObjects.lens.dragFieldWithKeyboard('@message.raw', 1, true); + expect( + await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') + ).to.eql(['Top values of @message.raw']); + }); + it('should drop a field to an existing dimension replacing the old one', async () => { + await PageObjects.lens.dragFieldWithKeyboard('clientip', 1, true); + expect( + await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') + ).to.eql(['Top values of clientip']); + }); + it('should duplicate an element in a group', async () => { + await PageObjects.lens.dimensionKeyboardDragDrop('lnsXY_yDimensionPanel', 0, 1); + expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_yDimensionPanel')).to.eql([ + 'Count of records', + 'Average of bytes', + 'Count of records [1]', + ]); + }); + + it('should move dimension to compatible dimension', async () => { + await PageObjects.lens.dimensionKeyboardDragDrop('lnsXY_xDimensionPanel', 0, 5); + expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql( + [] + ); + expect( + await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') + ).to.eql(['@timestamp']); + + await PageObjects.lens.dimensionKeyboardDragDrop('lnsXY_splitDimensionPanel', 0, 5, true); + expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql([ + '@timestamp', + ]); + expect( + await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') + ).to.eql([]); + }); + it('should move dimension to incompatible dimension', async () => { + await PageObjects.lens.dimensionKeyboardDragDrop('lnsXY_yDimensionPanel', 1, 2); + expect( + await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') + ).to.eql(['bytes']); + + await PageObjects.lens.dimensionKeyboardDragDrop('lnsXY_xDimensionPanel', 0, 2); + expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_yDimensionPanel')).to.eql([ + 'Count of records', + 'Unique count of @timestamp', + ]); + }); + it('should reorder elements with keyboard', async () => { + await PageObjects.lens.dimensionKeyboardReorder('lnsXY_yDimensionPanel', 0, 1); + expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_yDimensionPanel')).to.eql([ + 'Unique count of @timestamp', + 'Count of records', + ]); + }); }); describe('workspace drop', () => { diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index fc6842aae0345..aae161ef9fcf1 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -163,6 +163,73 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await PageObjects.header.waitUntilLoadingHasFinished(); }, + /** + * Copies field to chosen destination that is defined by distance of `steps` + * (right arrow presses) from it + * + * @param fieldName - the desired field for the dimension + * @param steps - number of steps user has to press right + * @param reverse - defines the direction of going through drops + * */ + async dragFieldWithKeyboard(fieldName: string, steps = 1, reverse = false) { + const field = await find.byCssSelector( + `[data-test-subj="lnsDragDrop_draggable-${fieldName}"] [data-test-subj="lnsDragDrop-keyboardHandler"]` + ); + await field.focus(); + await browser.pressKeys(browser.keys.ENTER); + for (let i = 0; i < steps; i++) { + await browser.pressKeys(reverse ? browser.keys.LEFT : browser.keys.RIGHT); + } + await browser.pressKeys(browser.keys.ENTER); + + await PageObjects.header.waitUntilLoadingHasFinished(); + }, + + /** + * Selects draggable element and moves it by number of `steps` + * + * @param group - the group of the element + * @param index - the index of the element in the group + * @param steps - number of steps of presses right or left + * @param reverse - defines the direction of going through drops + * */ + async dimensionKeyboardDragDrop(group: string, index = 0, steps = 1, reverse = false) { + const elements = await find.allByCssSelector( + `[data-test-subj="${group}"] [data-test-subj="lnsDragDrop-keyboardHandler"]` + ); + const el = elements[index]; + await el.focus(); + await browser.pressKeys(browser.keys.ENTER); + for (let i = 0; i < steps; i++) { + await browser.pressKeys(reverse ? browser.keys.LEFT : browser.keys.RIGHT); + } + await browser.pressKeys(browser.keys.ENTER); + + await PageObjects.header.waitUntilLoadingHasFinished(); + }, + /** + * Selects draggable element and reorders it by number of `steps` + * + * @param group - the group of the element + * @param index - the index of the element in the group + * @param steps - number of steps of presses right or left + * @param reverse - defines the direction of going through drops + * */ + async dimensionKeyboardReorder(group: string, index = 0, steps = 1, reverse = false) { + const elements = await find.allByCssSelector( + `[data-test-subj="${group}"] [data-test-subj="lnsDragDrop-keyboardHandler"]` + ); + const el = elements[index]; + await el.focus(); + await browser.pressKeys(browser.keys.ENTER); + for (let i = 0; i < steps; i++) { + await browser.pressKeys(reverse ? browser.keys.ARROW_UP : browser.keys.ARROW_DOWN); + } + await browser.pressKeys(browser.keys.ENTER); + + await PageObjects.header.waitUntilLoadingHasFinished(); + }, + /** * Drags field to dimension trigger * @@ -194,16 +261,12 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont /** * Reorder elements within the group * - * @param startIndex - the index of dragging element - * @param endIndex - the index of drop + * @param startIndex - the index of dragging element starting from 1 + * @param endIndex - the index of drop starting from 1 * */ async reorderDimensions(dimension: string, startIndex: number, endIndex: number) { - const dragging = `[data-test-subj='${dimension}']:nth-of-type(${ - startIndex + 1 - }) .lnsDragDrop`; - const dropping = `[data-test-subj='${dimension}']:nth-of-type(${ - endIndex + 1 - }) [data-test-subj='lnsDragDrop-reorderableDropLayer'`; + const dragging = `[data-test-subj='${dimension}']:nth-of-type(${startIndex}) .lnsDragDrop`; + const dropping = `[data-test-subj='${dimension}']:nth-of-type(${endIndex}) [data-test-subj='lnsDragDrop-reorderableDropLayer'`; await browser.html5DragAndDrop(dragging, dropping); await PageObjects.header.waitUntilLoadingHasFinished(); }, From 455538f99c3b0c7a33e7900fc42f862accfb22a5 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 5 Feb 2021 18:18:49 +0100 Subject: [PATCH 06/44] [Dashboard] fix destroy on embeddable container is never called (#90306) Co-authored-by: Devon Thomson --- .../hooks/use_dashboard_container.test.tsx | 173 ++++++++++++++++++ .../hooks/use_dashboard_container.ts | 44 ++++- 2 files changed, 209 insertions(+), 8 deletions(-) create mode 100644 src/plugins/dashboard/public/application/hooks/use_dashboard_container.test.tsx diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_container.test.tsx b/src/plugins/dashboard/public/application/hooks/use_dashboard_container.test.tsx new file mode 100644 index 0000000000000..d14b4056a64c6 --- /dev/null +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_container.test.tsx @@ -0,0 +1,173 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { useDashboardContainer } from './use_dashboard_container'; +import { renderHook, act } from '@testing-library/react-hooks'; +import { KibanaContextProvider } from '../../../../kibana_react/public'; +import React from 'react'; +import { DashboardStateManager } from '../dashboard_state_manager'; +import { getSavedDashboardMock } from '../test_helpers'; +import { createKbnUrlStateStorage, defer } from '../../../../kibana_utils/public'; +import { createBrowserHistory } from 'history'; +import { dataPluginMock } from '../../../../data/public/mocks'; +import { embeddablePluginMock } from '../../../../embeddable/public/mocks'; +import { DashboardCapabilities } from '../types'; +import { EmbeddableFactory } from '../../../../embeddable/public'; +import { HelloWorldEmbeddable } from '../../../../embeddable/public/tests/fixtures'; +import { DashboardContainer } from '../embeddable'; + +const savedDashboard = getSavedDashboardMock(); + +// TS is *very* picky with type guards / predicates. can't just use jest.fn() +function mockHasTaggingCapabilities(obj: any): obj is any { + return false; +} + +const history = createBrowserHistory(); +const createDashboardState = () => + new DashboardStateManager({ + savedDashboard, + hideWriteControls: false, + allowByValueEmbeddables: false, + kibanaVersion: '7.0.0', + kbnUrlStateStorage: createKbnUrlStateStorage(), + history: createBrowserHistory(), + hasTaggingCapabilities: mockHasTaggingCapabilities, + }); + +const defaultCapabilities: DashboardCapabilities = { + show: false, + createNew: false, + saveQuery: false, + createShortUrl: false, + hideWriteControls: true, + mapsCapabilities: { save: false }, + visualizeCapabilities: { save: false }, + storeSearchSession: true, +}; + +const services = { + dashboardCapabilities: defaultCapabilities, + data: dataPluginMock.createStartContract(), + embeddable: embeddablePluginMock.createStartContract(), + scopedHistory: history, +}; + +const setupEmbeddableFactory = () => { + const embeddable = new HelloWorldEmbeddable({ id: 'id' }); + const deferEmbeddableCreate = defer(); + services.embeddable.getEmbeddableFactory.mockImplementation( + () => + (({ + create: () => deferEmbeddableCreate.promise, + } as unknown) as EmbeddableFactory) + ); + const destroySpy = jest.spyOn(embeddable, 'destroy'); + + return { + destroySpy, + embeddable, + createEmbeddable: () => { + act(() => { + deferEmbeddableCreate.resolve(embeddable); + }); + }, + }; +}; + +test('container is destroyed on unmount', async () => { + const { createEmbeddable, destroySpy, embeddable } = setupEmbeddableFactory(); + + const state = createDashboardState(); + const { result, unmount, waitForNextUpdate } = renderHook( + () => useDashboardContainer(state, history, false), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + expect(result.current).toBeNull(); // null on initial render + + createEmbeddable(); + + await waitForNextUpdate(); + + expect(embeddable).toBe(result.current); + expect(destroySpy).not.toBeCalled(); + + unmount(); + + expect(destroySpy).toBeCalled(); +}); + +test('old container is destroyed on new dashboardStateManager', async () => { + const embeddableFactoryOld = setupEmbeddableFactory(); + + const { result, waitForNextUpdate, rerender } = renderHook< + DashboardStateManager, + DashboardContainer | null + >((dashboardState) => useDashboardContainer(dashboardState, history, false), { + wrapper: ({ children }) => ( + {children} + ), + initialProps: createDashboardState(), + }); + + expect(result.current).toBeNull(); // null on initial render + + embeddableFactoryOld.createEmbeddable(); + + await waitForNextUpdate(); + + expect(embeddableFactoryOld.embeddable).toBe(result.current); + expect(embeddableFactoryOld.destroySpy).not.toBeCalled(); + + const embeddableFactoryNew = setupEmbeddableFactory(); + rerender(createDashboardState()); + + embeddableFactoryNew.createEmbeddable(); + + await waitForNextUpdate(); + + expect(embeddableFactoryNew.embeddable).toBe(result.current); + + expect(embeddableFactoryNew.destroySpy).not.toBeCalled(); + expect(embeddableFactoryOld.destroySpy).toBeCalled(); +}); + +test('destroyed if rerendered before resolved', async () => { + const embeddableFactoryOld = setupEmbeddableFactory(); + + const { result, waitForNextUpdate, rerender } = renderHook< + DashboardStateManager, + DashboardContainer | null + >((dashboardState) => useDashboardContainer(dashboardState, history, false), { + wrapper: ({ children }) => ( + {children} + ), + initialProps: createDashboardState(), + }); + + expect(result.current).toBeNull(); // null on initial render + + const embeddableFactoryNew = setupEmbeddableFactory(); + rerender(createDashboardState()); + embeddableFactoryNew.createEmbeddable(); + await waitForNextUpdate(); + expect(embeddableFactoryNew.embeddable).toBe(result.current); + expect(embeddableFactoryNew.destroySpy).not.toBeCalled(); + + embeddableFactoryOld.createEmbeddable(); + + await act(() => Promise.resolve()); // Can't use waitFor from hooks, because there is no hook update + expect(embeddableFactoryNew.embeddable).toBe(result.current); + expect(embeddableFactoryNew.destroySpy).not.toBeCalled(); + expect(embeddableFactoryOld.destroySpy).toBeCalled(); +}); diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts index a3a31ee52836f..b27322b6bec53 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts @@ -7,7 +7,6 @@ */ import { useEffect, useState } from 'react'; -import _ from 'lodash'; import { History } from 'history'; import { useKibana } from '../../services/kibana_react'; @@ -15,6 +14,7 @@ import { ContainerOutput, EmbeddableFactoryNotFoundError, EmbeddableInput, + ErrorEmbeddable, isErrorEmbeddable, ViewMode, } from '../../services/embeddable'; @@ -70,8 +70,10 @@ export const useDashboardContainer = ( const incomingEmbeddable = embeddable.getStateTransfer().getIncomingEmbeddablePackage(true); + let canceled = false; + let pendingContainer: DashboardContainer | ErrorEmbeddable | null | undefined; (async function createContainer() { - const newContainer = await dashboardFactory.create( + pendingContainer = await dashboardFactory.create( getDashboardContainerInput({ dashboardCapabilities, dashboardStateManager, @@ -82,12 +84,27 @@ export const useDashboardContainer = ( }) ); - if (!newContainer || isErrorEmbeddable(newContainer)) { + // already new container is being created + // no longer interested in the pending one + if (canceled) { + try { + pendingContainer?.destroy(); + pendingContainer = null; + } catch (e) { + // destroy could throw if something has already destroyed the container + // eslint-disable-next-line no-console + console.warn(e); + } + + return; + } + + if (!pendingContainer || isErrorEmbeddable(pendingContainer)) { return; } // inject switch view mode callback for the empty screen to use - newContainer.switchViewMode = (newViewMode: ViewMode) => + pendingContainer.switchViewMode = (newViewMode: ViewMode) => dashboardStateManager.switchViewMode(newViewMode); // If the incoming embeddable is newly created, or doesn't exist in the current panels list, @@ -96,17 +113,28 @@ export const useDashboardContainer = ( incomingEmbeddable && (!incomingEmbeddable?.embeddableId || (incomingEmbeddable.embeddableId && - !newContainer.getInput().panels[incomingEmbeddable.embeddableId])) + !pendingContainer.getInput().panels[incomingEmbeddable.embeddableId])) ) { dashboardStateManager.switchViewMode(ViewMode.EDIT); - newContainer.addNewEmbeddable( + pendingContainer.addNewEmbeddable( incomingEmbeddable.type, incomingEmbeddable.input ); } - setDashboardContainer(newContainer); + setDashboardContainer(pendingContainer); })(); - return () => setDashboardContainer(null); + return () => { + canceled = true; + try { + pendingContainer?.destroy(); + } catch (e) { + // destroy could throw if something has already destroyed the container + // eslint-disable-next-line no-console + console.warn(e); + } + + setDashboardContainer(null); + }; }, [ dashboardCapabilities, dashboardStateManager, From 5dee629a6da0930b9854d8f20fa12cb362c19b7b Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Fri, 5 Feb 2021 10:22:23 -0700 Subject: [PATCH 07/44] GA Geo containment alerts. Remove Geo containment alert experimental config settings and refs (#90301) --- docs/user/alerting/geo-alert-types.asciidoc | 9 ++------- x-pack/plugins/stack_alerts/common/config.ts | 1 - .../public/alert_types/geo_containment/readme.md | 2 -- .../plugins/stack_alerts/public/alert_types/index.ts | 4 +--- x-pack/plugins/stack_alerts/server/index.ts | 10 +--------- 5 files changed, 4 insertions(+), 22 deletions(-) diff --git a/docs/user/alerting/geo-alert-types.asciidoc b/docs/user/alerting/geo-alert-types.asciidoc index c04cf4bca4320..f79885e3bc716 100644 --- a/docs/user/alerting/geo-alert-types.asciidoc +++ b/docs/user/alerting/geo-alert-types.asciidoc @@ -2,13 +2,8 @@ [[geo-alert-types]] == Geo alert types -experimental[] Two additional stack alerts are available: -<> and <>. To enable, -add the following configuration to your `kibana.yml`: - -```yml -xpack.stack_alerts.enableGeoAlerting: true -``` +Two additional stack alerts are available: +<> and <>. As with other stack alerts, you need `all` access to the *Stack Alerts* feature to be able to create and edit either of the geo alerts. diff --git a/x-pack/plugins/stack_alerts/common/config.ts b/x-pack/plugins/stack_alerts/common/config.ts index 1bd7b2728a95c..ebc12ee563350 100644 --- a/x-pack/plugins/stack_alerts/common/config.ts +++ b/x-pack/plugins/stack_alerts/common/config.ts @@ -9,7 +9,6 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), - enableGeoAlerting: schema.boolean({ defaultValue: false }), }); export type Config = TypeOf; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/readme.md b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/readme.md index 798beed8d17bd..b48a28fbdf99b 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/readme.md +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/readme.md @@ -19,8 +19,6 @@ project. To edit it, open this file in your editor of choice, add the line descr the next step to the bottom of the file (or really anywhere) and save. For more details on different config modifications or on how to make production config modifications, see [the current docs](https://www.elastic.co/guide/en/kibana/current/settings.html) -- Set the following configuration settings in your `config/kibana.yml`: -`xpack.stack_alerts.enableGeoAlerting: true` ### 2. Run ES/Kibana dev env with ssl enabled - In two terminals, run the normal commands to launch both elasticsearch and kibana but diff --git a/x-pack/plugins/stack_alerts/public/alert_types/index.ts b/x-pack/plugins/stack_alerts/public/alert_types/index.ts index 55819785d628b..d6f9f97939b79 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/index.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/index.ts @@ -18,9 +18,7 @@ export function registerAlertTypes({ alertTypeRegistry: TriggersAndActionsUIPublicPluginSetup['alertTypeRegistry']; config: Config; }) { - if (config.enableGeoAlerting) { - alertTypeRegistry.register(getGeoContainmentAlertType()); - } + alertTypeRegistry.register(getGeoContainmentAlertType()); alertTypeRegistry.register(getThresholdAlertType()); alertTypeRegistry.register(getEsQueryAlertType()); } diff --git a/x-pack/plugins/stack_alerts/server/index.ts b/x-pack/plugins/stack_alerts/server/index.ts index 4834749ab5917..bd10a486fa531 100644 --- a/x-pack/plugins/stack_alerts/server/index.ts +++ b/x-pack/plugins/stack_alerts/server/index.ts @@ -11,16 +11,8 @@ import { configSchema, Config } from '../common/config'; export { ID as INDEX_THRESHOLD_ID } from './alert_types/index_threshold/alert_type'; export const config: PluginConfigDescriptor = { - exposeToBrowser: { - enableGeoAlerting: true, - }, + exposeToBrowser: {}, schema: configSchema, - deprecations: ({ renameFromRoot }) => [ - renameFromRoot( - 'xpack.triggers_actions_ui.enableGeoTrackingThresholdAlert', - 'xpack.stack_alerts.enableGeoAlerting' - ), - ], }; export const plugin = (ctx: PluginInitializerContext) => new AlertingBuiltinsPlugin(ctx); From 4190ea4237d24314a3b88b6fd16d70a3acd56fed Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 5 Feb 2021 10:52:39 -0700 Subject: [PATCH 08/44] [eslint] stop ignoring .storybook files (#90447) Co-authored-by: spalger --- .eslintignore | 1 + src/plugins/dashboard/.storybook/main.js | 5 +++-- src/plugins/dashboard/.storybook/storyshots.test.tsx | 5 +++-- src/plugins/embeddable/.storybook/main.js | 5 +++-- .../kibana_react/public/code_editor/.storybook/main.js | 6 ++++-- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.eslintignore b/.eslintignore index 2d169c45214fe..5513ad1320232 100644 --- a/.eslintignore +++ b/.eslintignore @@ -16,6 +16,7 @@ target snapshots.js !/.eslintrc.js +!.storybook # plugin overrides /src/core/lib/kbn_internal_native_observable diff --git a/src/plugins/dashboard/.storybook/main.js b/src/plugins/dashboard/.storybook/main.js index 86b48c32f103e..8dc3c5d1518f4 100644 --- a/src/plugins/dashboard/.storybook/main.js +++ b/src/plugins/dashboard/.storybook/main.js @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ module.exports = require('@kbn/storybook').defaultConfig; diff --git a/src/plugins/dashboard/.storybook/storyshots.test.tsx b/src/plugins/dashboard/.storybook/storyshots.test.tsx index a75a9d178f0dd..80e8aa795ed40 100644 --- a/src/plugins/dashboard/.storybook/storyshots.test.tsx +++ b/src/plugins/dashboard/.storybook/storyshots.test.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import fs from 'fs'; diff --git a/src/plugins/embeddable/.storybook/main.js b/src/plugins/embeddable/.storybook/main.js index 86b48c32f103e..8dc3c5d1518f4 100644 --- a/src/plugins/embeddable/.storybook/main.js +++ b/src/plugins/embeddable/.storybook/main.js @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ module.exports = require('@kbn/storybook').defaultConfig; diff --git a/src/plugins/kibana_react/public/code_editor/.storybook/main.js b/src/plugins/kibana_react/public/code_editor/.storybook/main.js index 86b48c32f103e..742239e638b8a 100644 --- a/src/plugins/kibana_react/public/code_editor/.storybook/main.js +++ b/src/plugins/kibana_react/public/code_editor/.storybook/main.js @@ -1,8 +1,10 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ +// eslint-disable-next-line import/no-commonjs module.exports = require('@kbn/storybook').defaultConfig; From be53a06925393e5e1591a9eb720474ae693dd77f Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Fri, 5 Feb 2021 09:56:32 -0800 Subject: [PATCH 09/44] Fix state sharing between home integration components, prevent full page reload when clicking Fleet link (#90334) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../tutorial_directory_header_link.tsx | 25 +++++++++--------- .../tutorial_directory_notice.tsx | 26 +++++++++++++------ 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/home_integration/tutorial_directory_header_link.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/home_integration/tutorial_directory_header_link.tsx index 12d3647aeb524..cd378ec842679 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/home_integration/tutorial_directory_header_link.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/home_integration/tutorial_directory_header_link.tsx @@ -6,19 +6,16 @@ */ import React, { memo, useState, useEffect } from 'react'; -import { BehaviorSubject } from 'rxjs'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty } from '@elastic/eui'; import type { TutorialDirectoryHeaderLinkComponent } from 'src/plugins/home/public'; -import { useLink, useCapabilities } from '../../hooks'; - -const tutorialDirectoryNoticeState$ = new BehaviorSubject({ - settingsDataLoaded: false, - hasSeenNotice: false, -}); +import { RedirectAppLinks } from '../../../../../../../../src/plugins/kibana_react/public'; +import { useLink, useCapabilities, useStartServices } from '../../hooks'; +import { tutorialDirectoryNoticeState$ } from './tutorial_directory_notice'; const TutorialDirectoryHeaderLink: TutorialDirectoryHeaderLinkComponent = memo(() => { const { getHref } = useLink(); + const { application } = useStartServices(); const { show: hasIngestManager } = useCapabilities(); const [noticeState, setNoticeState] = useState({ settingsDataLoaded: false, @@ -33,12 +30,14 @@ const TutorialDirectoryHeaderLink: TutorialDirectoryHeaderLinkComponent = memo(( }, []); return hasIngestManager && noticeState.settingsDataLoaded && noticeState.hasSeenNotice ? ( - - - + + + + + ) : null; }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/home_integration/tutorial_directory_notice.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/home_integration/tutorial_directory_notice.tsx index 57a2803038301..8ea0c8730fdb5 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/home_integration/tutorial_directory_notice.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/home_integration/tutorial_directory_notice.tsx @@ -19,7 +19,14 @@ import { EuiSpacer, } from '@elastic/eui'; import type { TutorialDirectoryNoticeComponent } from 'src/plugins/home/public'; -import { sendPutSettings, useGetSettings, useLink, useCapabilities } from '../../hooks'; +import { RedirectAppLinks } from '../../../../../../../../src/plugins/kibana_react/public'; +import { + sendPutSettings, + useGetSettings, + useLink, + useCapabilities, + useStartServices, +} from '../../hooks'; const FlexItemButtonWrapper = styled(EuiFlexItem)` &&& { @@ -27,13 +34,14 @@ const FlexItemButtonWrapper = styled(EuiFlexItem)` } `; -const tutorialDirectoryNoticeState$ = new BehaviorSubject({ +export const tutorialDirectoryNoticeState$ = new BehaviorSubject({ settingsDataLoaded: false, hasSeenNotice: false, }); const TutorialDirectoryNotice: TutorialDirectoryNoticeComponent = memo(() => { const { getHref } = useLink(); + const { application } = useStartServices(); const { show: hasIngestManager } = useCapabilities(); const { data: settingsData, isLoading } = useGetSettings(); const [dismissedNotice, setDismissedNotice] = useState(false); @@ -98,12 +106,14 @@ const TutorialDirectoryNotice: TutorialDirectoryNoticeComponent = memo(() => {
    - - - + + + + +
    From 70d61436bc53ba80e46652026aa9554fb13c9eae Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Fri, 5 Feb 2021 11:58:57 -0600 Subject: [PATCH 10/44] [ML] Add Lens and Discover integration to index based Data Visualizer (#89471) --- x-pack/plugins/ml/kibana.json | 3 +- x-pack/plugins/ml/public/application/app.tsx | 1 + .../data_recognizer/data_recognizer.d.ts | 4 +- .../contexts/kibana/kibana_context.ts | 4 +- .../index_based/common/combined_query.ts | 11 + .../index_based/common/index.ts | 1 + .../actions_panel/actions_panel.tsx | 219 +++++++++---- .../components/expanded_row/expanded_row.tsx | 7 +- .../expanded_row/geo_point_content.tsx | 11 +- .../field_data_row/action_menu/actions.ts | 49 +++ .../field_data_row/action_menu/index.ts | 8 + .../field_data_row/action_menu/lens_utils.ts | 288 ++++++++++++++++++ .../datavisualizer/index_based/page.tsx | 53 ++-- .../data_visualizer_stats_table.tsx | 12 +- x-pack/plugins/ml/public/plugin.ts | 3 + x-pack/plugins/ml/tsconfig.json | 1 + .../data_visualizer/file_data_visualizer.ts | 4 +- .../data_visualizer/index_data_visualizer.ts | 30 +- .../index_data_visualizer_actions_panel.ts | 36 ++- .../apps/ml/permissions/full_ml_access.ts | 7 +- .../apps/ml/permissions/read_ml_access.ts | 12 +- .../ml/data_visualizer_index_based.ts | 40 +++ .../services/ml/data_visualizer_table.ts | 29 +- .../apps/ml/data_visualizer/index.ts | 3 +- .../index_data_visualizer_actions_panel.ts | 57 ++++ .../apps/ml/permissions/full_ml_access.ts | 7 +- .../apps/ml/permissions/read_ml_access.ts | 7 +- 27 files changed, 805 insertions(+), 102 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/index_based/common/combined_query.ts create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/actions.ts create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/index.ts create mode 100644 x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts create mode 100644 x-pack/test/functional_basic/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index ede6b8abbd09c..a73a68445a391 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -25,7 +25,8 @@ "spaces", "management", "licenseManagement", - "maps" + "maps", + "lens" ], "server": true, "ui": true, diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index 44558fb9dcfeb..0199e13e93d8c 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -77,6 +77,7 @@ const App: FC = ({ coreStart, deps, appMountParams }) => { data: deps.data, security: deps.security, licenseManagement: deps.licenseManagement, + lens: deps.lens, storage: localStorage, embeddable: deps.embeddable, maps: deps.maps, diff --git a/x-pack/plugins/ml/public/application/components/data_recognizer/data_recognizer.d.ts b/x-pack/plugins/ml/public/application/components/data_recognizer/data_recognizer.d.ts index c47e21222097d..ff6363ea2cc6e 100644 --- a/x-pack/plugins/ml/public/application/components/data_recognizer/data_recognizer.d.ts +++ b/x-pack/plugins/ml/public/application/components/data_recognizer/data_recognizer.d.ts @@ -7,10 +7,10 @@ import { FC } from 'react'; import { SavedSearchSavedObject } from '../../../../common/types/kibana'; -import { IndexPattern } from '../../../../../../../src/plugins/data/public'; +import type { IIndexPattern } from '../../../../../../../src/plugins/data/public'; declare const DataRecognizer: FC<{ - indexPattern: IndexPattern; + indexPattern: IIndexPattern; savedSearch: SavedSearchSavedObject | null; results: { count: number; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts index a8df8f8174bd3..1dd30d5d99335 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts @@ -17,7 +17,8 @@ import { SharePluginStart } from '../../../../../../../src/plugins/share/public' import { MlServicesContext } from '../../app'; import { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/public'; import type { EmbeddableStart } from '../../../../../../../src/plugins/embeddable/public'; -import { MapsStartApi } from '../../../../../maps/public'; +import type { MapsStartApi } from '../../../../../maps/public'; +import type { LensPublicStart } from '../../../../../lens/public'; interface StartPlugins { data: DataPublicPluginStart; @@ -26,6 +27,7 @@ interface StartPlugins { share: SharePluginStart; embeddable: EmbeddableStart; maps?: MapsStartApi; + lens?: LensPublicStart; } export type StartServices = CoreStart & StartPlugins & { diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/combined_query.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/combined_query.ts new file mode 100644 index 0000000000000..7723277959b1f --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/combined_query.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface CombinedQuery { + searchString: string | { [key: string]: any }; + searchQueryLanguage: string; +} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/index.ts index 50a67b946e525..fe99a63432793 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/index.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/index.ts @@ -6,3 +6,4 @@ */ export { FieldHistogramRequestConfig, FieldRequestConfig } from './request'; +export type { CombinedQuery } from './combined_query'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx index 9dd455427b747..255dfcc21ccab 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx @@ -5,23 +5,51 @@ * 2.0. */ -import React, { FC, useState } from 'react'; +import React, { FC, useState, useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EuiSpacer, EuiText, EuiTitle, EuiFlexGroup } from '@elastic/eui'; +import { + EuiSpacer, + EuiText, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiCard, + EuiIcon, +} from '@elastic/eui'; import { Link } from 'react-router-dom'; -import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; import { CreateJobLinkCard } from '../../../../components/create_job_link_card'; import { DataRecognizer } from '../../../../components/data_recognizer'; import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator'; +import { + DISCOVER_APP_URL_GENERATOR, + DiscoverUrlGeneratorState, +} from '../../../../../../../../../src/plugins/discover/public'; +import { useMlKibana } from '../../../../contexts/kibana'; +import { isFullLicense } from '../../../../license'; +import { checkPermission } from '../../../../capabilities/check_capabilities'; +import { mlNodesAvailable } from '../../../../ml_nodes_check'; +import { useUrlState } from '../../../../util/url_state'; +import type { IIndexPattern } from '../../../../../../../../../src/plugins/data/common'; interface Props { - indexPattern: IndexPattern; + indexPattern: IIndexPattern; + searchString?: string | { [key: string]: any }; + searchQueryLanguage?: string; } -export const ActionsPanel: FC = ({ indexPattern }) => { +export const ActionsPanel: FC = ({ indexPattern, searchString, searchQueryLanguage }) => { const [recognizerResultsCount, setRecognizerResultsCount] = useState(0); + const [discoverLink, setDiscoverLink] = useState(''); + const { + services: { + share: { + urlGenerators: { getUrlGenerator }, + }, + }, + } = useMlKibana(); + const [globalState] = useUrlState('_g'); const recognizerResults = { count: 0, @@ -29,63 +57,146 @@ export const ActionsPanel: FC = ({ indexPattern }) => { setRecognizerResultsCount(recognizerResults.count); }, }; + const showCreateJob = + isFullLicense() && + checkPermission('canCreateJob') && + mlNodesAvailable() && + indexPattern.timeFieldName !== undefined; const createJobLink = `/${ML_PAGES.ANOMALY_DETECTION_CREATE_JOB}/advanced?index=${indexPattern.id}`; + useEffect(() => { + let unmounted = false; + + const indexPatternId = indexPattern.id; + const getDiscoverUrl = async (): Promise => { + const state: DiscoverUrlGeneratorState = { + indexPatternId, + }; + if (searchString && searchQueryLanguage !== undefined) { + state.query = { query: searchString, language: searchQueryLanguage }; + } + if (globalState?.time) { + state.timeRange = globalState.time; + } + if (globalState?.refreshInterval) { + state.refreshInterval = globalState.refreshInterval; + } + + let discoverUrlGenerator; + try { + discoverUrlGenerator = getUrlGenerator(DISCOVER_APP_URL_GENERATOR); + } catch (error) { + // ignore error thrown when url generator is not available + return; + } + + const discoverUrl = await discoverUrlGenerator.createUrl(state); + if (!unmounted) { + setDiscoverLink(discoverUrl); + } + }; + getDiscoverUrl(); + return () => { + unmounted = true; + }; + }, [indexPattern, searchString, searchQueryLanguage, globalState]); + // Note we use display:none for the DataRecognizer section as it needs to be // passed the recognizerResults object, and then run the recognizer check which // controls whether the recognizer section is ultimately displayed. return (
    - -

    - -

    -
    - -
    - -

    - + +

    + +

    + + + + +

    + +

    +
    + + + + + + + )} + + {discoverLink && ( + <> + +

    + +

    +
    + + + } + description={i18n.translate( + 'xpack.ml.datavisualizer.actionsPanel.viewIndexInDiscoverDescription', + { + defaultMessage: 'Explore index in Discover', + } + )} + title={ + + } + href={discoverLink} /> -

    -
    - - - - - -
    - -

    - -

    -
    - - - - + + + )}
    ); }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/expanded_row.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/expanded_row.tsx index 96531de23fa4d..8a0656abe95cc 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/expanded_row.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; import { LoadingIndicator } from '../field_data_row/loading_indicator'; import { NotInDocsContent } from '../field_data_row/content_types'; -import { FieldVisConfig } from '../../../stats_table/types'; import { BooleanContent, DateContent, @@ -20,8 +19,10 @@ import { OtherContent, TextContent, } from '../../../stats_table/components/field_data_expanded_row'; -import { CombinedQuery, GeoPointContent } from './geo_point_content'; -import { IndexPattern } from '../../../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; +import { GeoPointContent } from './geo_point_content'; +import type { CombinedQuery } from '../../common'; +import type { IndexPattern } from '../../../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; +import type { FieldVisConfig } from '../../../stats_table/types'; export const IndexBasedDataVisualizerExpandedRow = ({ item, diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/geo_point_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/geo_point_content.tsx index cea65edbfb55a..33b347b4da805 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/geo_point_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/expanded_row/geo_point_content.tsx @@ -9,20 +9,17 @@ import React, { FC, useEffect, useState } from 'react'; import { EuiFlexItem } from '@elastic/eui'; import { ExamplesList } from '../../../index_based/components/field_data_row/examples_list'; -import { FieldVisConfig } from '../../../stats_table/types'; -import { IndexPattern } from '../../../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; import { MlEmbeddedMapComponent } from '../../../../components/ml_embedded_map'; import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; import { ES_GEO_FIELD_TYPE } from '../../../../../../../maps/common/constants'; -import { LayerDescriptor } from '../../../../../../../maps/common/descriptor_types'; import { useMlKibana } from '../../../../contexts/kibana'; import { DocumentStatsTable } from '../../../stats_table/components/field_data_expanded_row/document_stats'; import { ExpandedRowContent } from '../../../stats_table/components/field_data_expanded_row/expanded_row_content'; +import type { CombinedQuery } from '../../common'; +import type { IndexPattern } from '../../../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; +import type { LayerDescriptor } from '../../../../../../../maps/common/descriptor_types'; +import type { FieldVisConfig } from '../../../stats_table/types'; -export interface CombinedQuery { - searchString: string | { [key: string]: any }; - searchQueryLanguage: string; -} export const GeoPointContent: FC<{ config: FieldVisConfig; indexPattern: IndexPattern | undefined; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/actions.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/actions.ts new file mode 100644 index 0000000000000..57675927ce816 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/actions.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { Action } from '@elastic/eui/src/components/basic_table/action_types'; +import { getCompatibleLensDataType, getLensAttributes } from './lens_utils'; +import type { CombinedQuery } from '../../../common'; +import type { IIndexPattern } from '../../../../../../../../../../src/plugins/data/common/index_patterns'; +import type { LensPublicStart } from '../../../../../../../../lens/public'; +import type { FieldVisConfig } from '../../../../stats_table/types'; + +export function getActions( + indexPattern: IIndexPattern, + lensPlugin: LensPublicStart, + combinedQuery: CombinedQuery +): Array> { + const canUseLensEditor = lensPlugin.canUseEditor(); + return [ + { + name: i18n.translate('xpack.ml.dataVisualizer.indexBasedDataGrid.exploreInLensTitle', { + defaultMessage: 'Explore in Lens', + }), + description: i18n.translate( + 'xpack.ml.dataVisualizer.indexBasedDataGrid.exploreInLensDescription', + { + defaultMessage: 'Explore in Lens', + } + ), + type: 'icon', + icon: 'lensApp', + available: (item: FieldVisConfig) => + getCompatibleLensDataType(item.type) !== undefined && canUseLensEditor, + onClick: (item: FieldVisConfig) => { + const lensAttributes = getLensAttributes(indexPattern, combinedQuery, item); + if (lensAttributes) { + lensPlugin.navigateToPrefilledEditor({ + id: `ml-dataVisualizer-${item.fieldName}`, + attributes: lensAttributes, + }); + } + }, + 'data-test-subj': 'mlActionButtonViewInLens', + }, + ]; +} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/index.ts new file mode 100644 index 0000000000000..df36cc89ce911 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { getActions } from './actions'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts new file mode 100644 index 0000000000000..8d078b59ad778 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts @@ -0,0 +1,288 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { ML_JOB_FIELD_TYPES } from '../../../../../../../common/constants/field_types'; +import type { TypedLensByValueInput } from '../../../../../../../../lens/public'; +import type { FieldVisConfig } from '../../../../stats_table/types'; +import type { IndexPatternColumn, XYLayerConfig } from '../../../../../../../../lens/public'; +import type { CombinedQuery } from '../../../common'; +import type { IIndexPattern } from '../../../../../../../../../../src/plugins/data/common/index_patterns'; +interface ColumnsAndLayer { + columns: Record; + layer: XYLayerConfig; +} + +const TOP_VALUES_LABEL = i18n.translate('xpack.ml.dataVisualizer.lensChart.topValuesLabel', { + defaultMessage: 'Top values', +}); +const COUNT = i18n.translate('xpack.ml.dataVisualizer.lensChart.countLabel', { + defaultMessage: 'Count', +}); + +export function getNumberSettings(item: FieldVisConfig, defaultIndexPattern: IIndexPattern) { + // if index has no timestamp field + if (defaultIndexPattern.timeFieldName === undefined) { + const columns: Record = { + col1: { + label: item.fieldName!, + dataType: 'number', + isBucketed: true, + operationType: 'range', + params: { + type: 'histogram', + maxBars: 'auto', + ranges: [], + }, + sourceField: item.fieldName!, + }, + col2: { + label: COUNT, + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + }; + + const layer: XYLayerConfig = { + accessors: ['col2'], + layerId: 'layer1', + seriesType: 'bar', + xAccessor: 'col1', + }; + return { columns, layer }; + } + + const columns: Record = { + col2: { + dataType: 'number', + isBucketed: false, + label: i18n.translate('xpack.ml.dataVisualizer.lensChart.averageOfLabel', { + defaultMessage: 'Average of {fieldName}', + values: { fieldName: item.fieldName }, + }), + operationType: 'avg', + sourceField: item.fieldName!, + }, + col1: { + dataType: 'date', + isBucketed: true, + label: defaultIndexPattern.timeFieldName!, + operationType: 'date_histogram', + params: { interval: 'auto' }, + scale: 'interval', + sourceField: defaultIndexPattern.timeFieldName!, + }, + }; + + const layer: XYLayerConfig = { + accessors: ['col2'], + layerId: 'layer1', + seriesType: 'line', + xAccessor: 'col1', + }; + + return { columns, layer }; +} +export function getDateSettings(item: FieldVisConfig) { + const columns: Record = { + col2: { + dataType: 'number', + isBucketed: false, + label: COUNT, + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + col1: { + dataType: 'date', + isBucketed: true, + label: item.fieldName!, + operationType: 'date_histogram', + params: { interval: 'auto' }, + scale: 'interval', + sourceField: item.fieldName!, + }, + }; + const layer: XYLayerConfig = { + accessors: ['col2'], + layerId: 'layer1', + seriesType: 'line', + xAccessor: 'col1', + }; + + return { columns, layer }; +} + +export function getKeywordSettings(item: FieldVisConfig) { + const columns: Record = { + col1: { + label: TOP_VALUES_LABEL, + dataType: 'string', + isBucketed: true, + operationType: 'terms', + params: { + orderBy: { type: 'column', columnId: 'col2' }, + size: 10, + orderDirection: 'desc', + }, + sourceField: item.fieldName!, + }, + col2: { + label: COUNT, + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + }; + const layer: XYLayerConfig = { + accessors: ['col2'], + layerId: 'layer1', + seriesType: 'bar', + xAccessor: 'col1', + }; + + return { columns, layer }; +} + +export function getBooleanSettings(item: FieldVisConfig) { + const columns: Record = { + col1: { + label: TOP_VALUES_LABEL, + dataType: 'string', + isBucketed: true, + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + size: 2, + orderDirection: 'desc', + }, + sourceField: item.fieldName!, + }, + col2: { + label: COUNT, + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + }; + const layer: XYLayerConfig = { + accessors: ['col2'], + layerId: 'layer1', + seriesType: 'bar', + xAccessor: 'col1', + }; + + return { columns, layer }; +} + +export function getCompatibleLensDataType(type: FieldVisConfig['type']): string | undefined { + let lensType: string | undefined; + switch (type) { + case ML_JOB_FIELD_TYPES.KEYWORD: + lensType = 'string'; + break; + case ML_JOB_FIELD_TYPES.DATE: + lensType = 'date'; + break; + case ML_JOB_FIELD_TYPES.NUMBER: + lensType = 'number'; + break; + case ML_JOB_FIELD_TYPES.IP: + lensType = 'ip'; + break; + case ML_JOB_FIELD_TYPES.BOOLEAN: + lensType = 'string'; + break; + default: + lensType = undefined; + } + return lensType; +} + +function getColumnsAndLayer( + fieldType: FieldVisConfig['type'], + item: FieldVisConfig, + defaultIndexPattern: IIndexPattern +): ColumnsAndLayer | undefined { + if (item.fieldName === undefined) return; + + if (fieldType === ML_JOB_FIELD_TYPES.DATE) { + return getDateSettings(item); + } + if (fieldType === ML_JOB_FIELD_TYPES.NUMBER) { + return getNumberSettings(item, defaultIndexPattern); + } + if (fieldType === ML_JOB_FIELD_TYPES.IP || fieldType === ML_JOB_FIELD_TYPES.KEYWORD) { + return getKeywordSettings(item); + } + if (fieldType === ML_JOB_FIELD_TYPES.BOOLEAN) { + return getBooleanSettings(item); + } +} +// Get formatted Lens visualization format depending on field type +// currently only supports the following types: +// 'document' | 'string' | 'number' | 'date' | 'boolean' | 'ip' +export function getLensAttributes( + defaultIndexPattern: IIndexPattern | undefined, + combinedQuery: CombinedQuery, + item: FieldVisConfig +): TypedLensByValueInput['attributes'] | undefined { + if (defaultIndexPattern === undefined || item.type === undefined || item.fieldName === undefined) + return; + + const presets = getColumnsAndLayer(item.type, item, defaultIndexPattern); + + if (!presets) return; + + return { + visualizationType: 'lnsXY', + title: i18n.translate('xpack.ml.dataVisualizer.lensChart.chartTitle', { + defaultMessage: 'Lens for {fieldName}', + values: { fieldName: item.fieldName }, + }), + references: [ + { + id: defaultIndexPattern.id!, + name: 'indexpattern-datasource-current-indexpattern', + type: 'index-pattern', + }, + { + id: defaultIndexPattern.id!, + name: 'indexpattern-datasource-layer-layer1', + type: 'index-pattern', + }, + ], + state: { + datasourceStates: { + indexpattern: { + layers: { + layer1: { + columnOrder: ['col1', 'col2'], + columns: presets.columns, + }, + }, + }, + }, + filters: [], + query: { language: combinedQuery.searchQueryLanguage, query: combinedQuery.searchString }, + visualization: { + axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + fittingFunction: 'None', + gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + layers: [presets.layer], + legend: { isVisible: true, position: 'right' }, + preferredSeriesType: 'line', + tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, + valueLabels: 'hide', + }, + }, + }; +} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx index 6ea85c354d88b..6bc1970bc615b 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -19,6 +19,8 @@ import { EuiSpacer, EuiTitle, } from '@elastic/eui'; +import { EuiTableActionsColumnType } from '@elastic/eui/src/components/basic_table/table_types'; +import { FormattedMessage } from '@kbn/i18n/react'; import { IFieldType, KBN_FIELD_TYPES, @@ -32,9 +34,6 @@ import { NavigationMenu } from '../../components/navigation_menu'; import { DatePickerWrapper } from '../../components/navigation_menu/date_picker_wrapper'; import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '../../../../common/constants/search'; -import { isFullLicense } from '../../license'; -import { checkPermission } from '../../capabilities/check_capabilities'; -import { mlNodesAvailable } from '../../ml_nodes_check/check_ml_nodes'; import { FullTimeRangeSelector } from '../../components/full_time_range_selector'; import { mlTimefilterRefresh$ } from '../../services/timefilter_refresh_service'; import { useMlContext } from '../../contexts/ml'; @@ -63,6 +62,7 @@ import type { MetricFieldsStats, TotalFieldsStats, } from '../stats_table/components/field_count_stats'; +import { getActions } from './components/field_data_row/action_menu/actions'; interface DataVisualizerPageState { overallStats: OverallStats; @@ -116,6 +116,10 @@ export const getDefaultDataVisualizerListState = (): Required { const mlContext = useMlContext(); const restorableDefaults = getDefaultDataVisualizerListState(); + const { + services: { lens: lensPlugin, docLinks }, + } = useMlKibana(); + const [dataVisualizerListState, setDataVisualizerListState] = usePageUrlState( ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER, restorableDefaults @@ -167,12 +171,6 @@ export const Page: FC = () => { const defaults = getDefaultPageState(); - const showActionsPanel = - isFullLicense() && - checkPermission('canCreateJob') && - mlNodesAvailable() && - currentIndexPattern.timeFieldName !== undefined; - const { searchQueryLanguage, searchString, searchQuery } = useMemo(() => { const searchData = extractSearchData(currentSavedSearch); if (searchData === undefined || dataVisualizerListState.searchString !== '') { @@ -686,9 +684,27 @@ export const Page: FC = () => { [currentIndexPattern, searchQuery] ); - const { - services: { docLinks }, - } = useMlKibana(); + // Inject custom action column for the index based visualizer + const extendedColumns = useMemo(() => { + if (lensPlugin === undefined) { + // eslint-disable-next-line no-console + console.error('Lens plugin not available'); + return; + } + const actionColumn: EuiTableActionsColumnType = { + name: ( + + ), + actions: getActions(currentIndexPattern, lensPlugin, { searchQueryLanguage, searchString }), + width: '100px', + }; + + return [actionColumn]; + }, [currentIndexPattern, lensPlugin, searchQueryLanguage, searchString]); + const helpLink = docLinks.links.ml.guide; return ( @@ -766,14 +782,17 @@ export const Page: FC = () => { pageState={dataVisualizerListState} updatePageState={setDataVisualizerListState} getItemIdToExpandedRowMap={getItemIdToExpandedRowMap} + extendedColumns={extendedColumns} /> - {showActionsPanel === true && ( - - - - )} + + +
    diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/data_visualizer_stats_table.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/data_visualizer_stats_table.tsx index 82e807fa61e67..2a6a681c63210 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/data_visualizer_stats_table.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/data_visualizer_stats_table.tsx @@ -9,6 +9,7 @@ import React, { useMemo, useState } from 'react'; import { CENTER_ALIGNMENT, + EuiBasicTableColumn, EuiButtonIcon, EuiFlexItem, EuiIcon, @@ -52,6 +53,7 @@ interface DataVisualizerTableProps { update: Partial ) => void; getItemIdToExpandedRowMap: (itemIds: string[], items: T[]) => ItemIdToExpandedRowMap; + extendedColumns?: Array>; } export const DataVisualizerTable = ({ @@ -59,11 +61,12 @@ export const DataVisualizerTable = ({ pageState, updatePageState, getItemIdToExpandedRowMap, + extendedColumns, }: DataVisualizerTableProps) => { const [expandedRowItemIds, setExpandedRowItemIds] = useState([]); const [expandAll, toggleExpandAll] = useState(false); - const { onTableChange, pagination, sorting } = useTableSettings( + const { onTableChange, pagination, sorting } = useTableSettings( items, pageState, updatePageState @@ -136,7 +139,7 @@ export const DataVisualizerTable = ({ 'data-test-subj': 'mlDataVisualizerTableColumnDetailsToggle', }; - return [ + const baseColumns = [ expanderColumn, { field: 'type', @@ -236,7 +239,8 @@ export const DataVisualizerTable = ({ 'data-test-subj': 'mlDataVisualizerTableColumnDistribution', }, ]; - }, [expandAll, showDistributions, updatePageState]); + return extendedColumns ? [...baseColumns, ...extendedColumns] : baseColumns; + }, [expandAll, showDistributions, updatePageState, extendedColumns]); const itemIdToExpandedRowMap = useMemo(() => { let itemIds = expandedRowItemIds; @@ -248,7 +252,7 @@ export const DataVisualizerTable = ({ return ( - + className={'mlDataVisualizer'} items={items} itemId={FIELD_NAME} diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index bfbc04943273e..9fd245a7e16ba 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -46,6 +46,7 @@ import { registerFeature } from './register_feature'; // Not importing from `ml_url_generator/index` here to avoid importing unnecessary code import { registerUrlGenerator } from './ml_url_generator/ml_url_generator'; import type { MapsStartApi } from '../../maps/public'; +import { LensPublicStart } from '../../lens/public'; export interface MlStartDependencies { data: DataPublicPluginStart; @@ -55,6 +56,7 @@ export interface MlStartDependencies { spaces?: SpacesPluginStart; embeddable: EmbeddableStart; maps?: MapsStartApi; + lens?: LensPublicStart; } export interface MlSetupDependencies { security?: SecurityPluginSetup; @@ -106,6 +108,7 @@ export class MlPlugin implements Plugin { embeddable: { ...pluginsSetup.embeddable, ...pluginsStart.embeddable }, maps: pluginsStart.maps, uiActions: pluginsStart.uiActions, + lens: pluginsStart.lens, kibanaVersion, }, params diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json index 113bcbe71047f..2caf88de1b76a 100644 --- a/x-pack/plugins/ml/tsconfig.json +++ b/x-pack/plugins/ml/tsconfig.json @@ -28,6 +28,7 @@ { "path": "../license_management/tsconfig.json" }, { "path": "../licensing/tsconfig.json" }, { "path": "../maps/tsconfig.json" }, + { "path": "../lens/tsconfig.json" }, { "path": "../security/tsconfig.json" }, { "path": "../spaces/tsconfig.json" }, ] diff --git a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts index c09bb0c555322..65bc68db25aa1 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts @@ -222,6 +222,7 @@ export default function ({ getService }: FtrProviderContext) { fieldRow.fieldName, fieldRow.docCountFormatted, fieldRow.topValuesCount, + false, false ); } @@ -230,7 +231,8 @@ export default function ({ getService }: FtrProviderContext) { fieldRow.type, fieldRow.fieldName!, fieldRow.docCountFormatted, - fieldRow.exampleCount + fieldRow.exampleCount, + false ); } diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts index ffd22dd176ed5..609cf05dad541 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts @@ -13,11 +13,13 @@ interface MetricFieldVisConfig extends FieldVisConfig { statsMaxDecimalPlaces: number; docCountFormatted: string; topValuesCount: number; + viewableInLens: boolean; } interface NonMetricFieldVisConfig extends FieldVisConfig { docCountFormatted: string; exampleCount: number; + viewableInLens: boolean; } interface TestData { @@ -69,6 +71,7 @@ export default function ({ getService }: FtrProviderContext) { docCountFormatted: '5000 (100%)', statsMaxDecimalPlaces: 3, topValuesCount: 10, + viewableInLens: true, }, ], nonMetricFields: [ @@ -80,6 +83,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, docCountFormatted: '5000 (100%)', exampleCount: 2, + viewableInLens: true, }, { fieldName: '@version', @@ -89,6 +93,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 1, docCountFormatted: '', + viewableInLens: false, }, { fieldName: '@version.keyword', @@ -98,6 +103,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 1, docCountFormatted: '5000 (100%)', + viewableInLens: true, }, { fieldName: 'airline', @@ -107,6 +113,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 10, docCountFormatted: '5000 (100%)', + viewableInLens: true, }, { fieldName: 'type', @@ -116,6 +123,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 1, docCountFormatted: '', + viewableInLens: false, }, { fieldName: 'type.keyword', @@ -125,6 +133,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 1, docCountFormatted: '5000 (100%)', + viewableInLens: true, }, ], emptyFields: ['sourcetype'], @@ -158,6 +167,7 @@ export default function ({ getService }: FtrProviderContext) { docCountFormatted: '5000 (100%)', statsMaxDecimalPlaces: 3, topValuesCount: 10, + viewableInLens: true, }, ], nonMetricFields: [ @@ -169,6 +179,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, docCountFormatted: '5000 (100%)', exampleCount: 2, + viewableInLens: true, }, { fieldName: '@version', @@ -178,6 +189,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 1, docCountFormatted: '', + viewableInLens: false, }, { fieldName: '@version.keyword', @@ -187,6 +199,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 1, docCountFormatted: '5000 (100%)', + viewableInLens: true, }, { fieldName: 'airline', @@ -196,6 +209,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 5, docCountFormatted: '5000 (100%)', + viewableInLens: true, }, { fieldName: 'type', @@ -205,6 +219,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 1, docCountFormatted: '', + viewableInLens: false, }, { fieldName: 'type.keyword', @@ -214,6 +229,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 1, docCountFormatted: '5000 (100%)', + viewableInLens: true, }, ], emptyFields: ['sourcetype'], @@ -247,6 +263,7 @@ export default function ({ getService }: FtrProviderContext) { docCountFormatted: '5000 (100%)', statsMaxDecimalPlaces: 3, topValuesCount: 10, + viewableInLens: true, }, ], nonMetricFields: [ @@ -258,6 +275,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, docCountFormatted: '5000 (100%)', exampleCount: 2, + viewableInLens: true, }, { fieldName: '@version', @@ -267,6 +285,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 1, docCountFormatted: '', + viewableInLens: false, }, { fieldName: '@version.keyword', @@ -276,6 +295,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 1, docCountFormatted: '5000 (100%)', + viewableInLens: true, }, { fieldName: 'airline', @@ -285,6 +305,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 5, docCountFormatted: '5000 (100%)', + viewableInLens: true, }, { fieldName: 'type', @@ -294,6 +315,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 1, docCountFormatted: '', + viewableInLens: false, }, { fieldName: 'type.keyword', @@ -303,6 +325,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, exampleCount: 1, docCountFormatted: '5000 (100%)', + viewableInLens: true, }, ], emptyFields: ['sourcetype'], @@ -334,6 +357,7 @@ export default function ({ getService }: FtrProviderContext) { loading: false, docCountFormatted: '408 (100%)', exampleCount: 10, + viewableInLens: false, }, ], emptyFields: [], @@ -417,7 +441,8 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataVisualizerTable.assertNumberFieldContents( fieldRow.fieldName, fieldRow.docCountFormatted, - fieldRow.topValuesCount + fieldRow.topValuesCount, + fieldRow.viewableInLens ); } @@ -426,7 +451,8 @@ export default function ({ getService }: FtrProviderContext) { fieldRow.type, fieldRow.fieldName!, fieldRow.docCountFormatted, - fieldRow.exampleCount + fieldRow.exampleCount, + fieldRow.viewableInLens ); } diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts index 6e2e9cfb858c3..ce00ee79e9075 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts @@ -11,7 +11,7 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - describe('index based actions panel', function () { + describe('index based actions panel on trial license', function () { this.tags(['mlqa']); const indexPatternName = 'ft_farequote'; @@ -28,6 +28,7 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { await esArchiver.loadIfNeeded('ml/farequote'); await ml.testResources.createIndexPatternIfNeeded(indexPatternName, '@timestamp'); + await ml.testResources.createSavedSearchFarequoteKueryIfNeeded(); await ml.testResources.setKibanaTimeZoneToUTC(); await ml.securityUI.loginAsMlPowerUser(); @@ -59,5 +60,38 @@ export default function ({ getService }: FtrProviderContext) { await ml.jobWizardAdvanced.assertDatafeedQueryEditorValue(advancedJobWizardDatafeedQuery); }); }); + + describe('view in discover page action', function () { + const savedSearch = 'ft_farequote_kuery'; + const expectedQuery = 'airline: A* and responsetime > 5'; + const docCountFormatted = '34,415'; + + it('loads the source data in the data visualizer', async () => { + await ml.testExecution.logTestStep('loads the data visualizer selector page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToDataVisualizer(); + + await ml.testExecution.logTestStep('loads the saved search selection page'); + await ml.dataVisualizer.navigateToIndexPatternSelection(); + + await ml.testExecution.logTestStep('loads the index data visualizer page'); + await ml.jobSourceSelection.selectSourceForIndexBasedDataVisualizer(savedSearch); + + await ml.testExecution.logTestStep(`loads data for full time range`); + await ml.dataVisualizerIndexBased.assertTimeRangeSelectorSectionExists(); + await ml.dataVisualizerIndexBased.clickUseFullDataButton(docCountFormatted); + }); + + it('navigates to Discover page', async () => { + await ml.testExecution.logTestStep('displays the actions panel with view in Discover card'); + await ml.dataVisualizerIndexBased.assertActionsPanelExists(); + await ml.dataVisualizerIndexBased.assertViewInDiscoverCardExists(); + + await ml.testExecution.logTestStep('retains the query in Discover page'); + await ml.dataVisualizerIndexBased.clickViewInDiscoverButton(); + await ml.dataVisualizerIndexBased.assertDiscoverPageQuery(expectedQuery); + await ml.dataVisualizerIndexBased.assertDiscoverHitCount(docCountFormatted); + }); + }); }); } diff --git a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts index 261e0547210f1..7b4c646f379de 100644 --- a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts @@ -357,8 +357,13 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('should display the data visualizer table'); await ml.dataVisualizerIndexBased.assertDataVisualizerTableExist(); - await ml.testExecution.logTestStep('should display the actions panel with cards'); + await ml.testExecution.logTestStep( + 'should display the actions panel with Discover card' + ); await ml.dataVisualizerIndexBased.assertActionsPanelExists(); + await ml.dataVisualizerIndexBased.assertViewInDiscoverCardExists(); + + await ml.testExecution.logTestStep('should display job cards'); await ml.dataVisualizerIndexBased.assertCreateAdvancedJobCardExists(); await ml.dataVisualizerIndexBased.assertRecognizerCardExists(ecExpectedModuleId); }); diff --git a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts index 98b743192c160..69ae3961dfd4d 100644 --- a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts @@ -99,6 +99,7 @@ export default function ({ getService }: FtrProviderContext) { const ecIndexPattern = 'ft_module_sample_ecommerce'; const ecExpectedTotalCount = '287'; + const ecExpectedModuleId = 'sample_data_ecommerce'; const uploadFilePath = path.join( __dirname, @@ -349,8 +350,15 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('should display the data visualizer table'); await ml.dataVisualizerIndexBased.assertDataVisualizerTableExist(); - await ml.testExecution.logTestStep('should not display the actions panel'); - await ml.dataVisualizerIndexBased.assertActionsPanelNotExists(); + await ml.testExecution.logTestStep( + 'should display the actions panel with Discover card' + ); + await ml.dataVisualizerIndexBased.assertActionsPanelExists(); + await ml.dataVisualizerIndexBased.assertViewInDiscoverCardExists(); + + await ml.testExecution.logTestStep('should not display job cards'); + await ml.dataVisualizerIndexBased.assertCreateAdvancedJobCardNotExists(); + await ml.dataVisualizerIndexBased.assertRecognizerCardNotExists(ecExpectedModuleId); }); it('should display elements on File Data Visualizer page correctly', async () => { diff --git a/x-pack/test/functional/services/ml/data_visualizer_index_based.ts b/x-pack/test/functional/services/ml/data_visualizer_index_based.ts index 373b1aa20a4bb..d8ec8ed49f011 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_index_based.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_index_based.ts @@ -10,9 +10,12 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export function MachineLearningDataVisualizerIndexBasedProvider({ getService, + getPageObjects, }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); + const PageObjects = getPageObjects(['discover']); + const queryBar = getService('queryBar'); return { async assertTimeRangeSelectorSectionExists() { @@ -149,5 +152,42 @@ export function MachineLearningDataVisualizerIndexBasedProvider({ async clickCreateAdvancedJobButton() { await testSubjects.clickWhenNotDisabled('mlDataVisualizerCreateAdvancedJobCard'); }, + + async assertViewInDiscoverCardExists() { + await testSubjects.existOrFail('mlDataVisualizerViewInDiscoverCard'); + }, + + async assertViewInDiscoverCardNotExists() { + await testSubjects.missingOrFail('mlDataVisualizerViewInDiscoverCard'); + }, + + async clickViewInDiscoverButton() { + await retry.tryForTime(5000, async () => { + await testSubjects.clickWhenNotDisabled('mlDataVisualizerViewInDiscoverCard'); + await PageObjects.discover.waitForDiscoverAppOnScreen(); + }); + }, + + async assertDiscoverPageQuery(expectedQueryString: string) { + await PageObjects.discover.waitForDiscoverAppOnScreen(); + await retry.tryForTime(5000, async () => { + const queryString = await queryBar.getQueryString(); + expect(queryString).to.eql( + expectedQueryString, + `Expected Discover global query bar to have query '${expectedQueryString}', got '${queryString}'` + ); + }); + }, + + async assertDiscoverHitCount(expectedHitCountFormatted: string) { + await PageObjects.discover.waitForDiscoverAppOnScreen(); + await retry.tryForTime(5000, async () => { + const hitCount = await PageObjects.discover.getHitCount(); + expect(hitCount).to.eql( + expectedHitCountFormatted, + `Expected Discover hit count to be '${expectedHitCountFormatted}' (got '${hitCount}')` + ); + }); + }, }; } diff --git a/x-pack/test/functional/services/ml/data_visualizer_table.ts b/x-pack/test/functional/services/ml/data_visualizer_table.ts index 36f5b94dc52dd..3bd3b7e2e783a 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_table.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_table.ts @@ -133,6 +133,17 @@ export function MachineLearningDataVisualizerTableProvider( ); } + public async assertViewInLensActionEnabled(fieldName: string) { + const actionButton = this.rowSelector(fieldName, 'mlActionButtonViewInLens'); + await testSubjects.existOrFail(actionButton); + await testSubjects.isEnabled(actionButton); + } + + public async assertViewInLensActionNotExists(fieldName: string) { + const actionButton = this.rowSelector(fieldName, 'mlActionButtonViewInLens'); + await testSubjects.missingOrFail(actionButton); + } + public async assertFieldDistinctValuesExist(fieldName: string) { const selector = this.rowSelector(fieldName, 'mlDataVisualizerTableColumnDistinctValues'); await testSubjects.existOrFail(selector); @@ -249,6 +260,7 @@ export function MachineLearningDataVisualizerTableProvider( fieldName: string, docCountFormatted: string, topValuesCount: number, + viewableInLens: boolean, checkDistributionPreviewExist = true ) { await this.assertRowExists(fieldName); @@ -263,6 +275,11 @@ export function MachineLearningDataVisualizerTableProvider( if (checkDistributionPreviewExist) { await this.assertDistributionPreviewExist(fieldName); } + if (viewableInLens) { + await this.assertViewInLensActionEnabled(fieldName); + } else { + await this.assertViewInLensActionNotExists(fieldName); + } await this.ensureDetailsClosed(fieldName); } @@ -307,6 +324,7 @@ export function MachineLearningDataVisualizerTableProvider( ) { await this.assertRowExists(fieldName); await this.assertFieldDocCount(fieldName, docCountFormatted); + await this.ensureDetailsOpen(fieldName); await this.assertExamplesList(fieldName, expectedExamplesCount); @@ -320,6 +338,7 @@ export function MachineLearningDataVisualizerTableProvider( ) { await this.assertRowExists(fieldName); await this.assertFieldDocCount(fieldName, docCountFormatted); + await this.ensureDetailsOpen(fieldName); await this.assertExamplesList(fieldName, expectedExamplesCount); @@ -332,6 +351,7 @@ export function MachineLearningDataVisualizerTableProvider( public async assertUnknownFieldContents(fieldName: string, docCountFormatted: string) { await this.assertRowExists(fieldName); await this.assertFieldDocCount(fieldName, docCountFormatted); + await this.ensureDetailsOpen(fieldName); await testSubjects.existOrFail(this.detailsSelector(fieldName, 'mlDVDocumentStatsContent')); @@ -343,7 +363,8 @@ export function MachineLearningDataVisualizerTableProvider( fieldType: string, fieldName: string, docCountFormatted: string, - exampleCount: number + exampleCount: number, + viewableInLens: boolean ) { // Currently the data used in the data visualizer tests only contains these field types. if (fieldType === ML_JOB_FIELD_TYPES.DATE) { @@ -357,6 +378,12 @@ export function MachineLearningDataVisualizerTableProvider( } else if (fieldType === ML_JOB_FIELD_TYPES.UNKNOWN) { await this.assertUnknownFieldContents(fieldName, docCountFormatted); } + + if (viewableInLens) { + await this.assertViewInLensActionEnabled(fieldName); + } else { + await this.assertViewInLensActionNotExists(fieldName); + } } public async ensureNumRowsPerPage(n: 10 | 25 | 50) { diff --git a/x-pack/test/functional_basic/apps/ml/data_visualizer/index.ts b/x-pack/test/functional_basic/apps/ml/data_visualizer/index.ts index 007b8be272f5d..57a44a0b7952d 100644 --- a/x-pack/test/functional_basic/apps/ml/data_visualizer/index.ts +++ b/x-pack/test/functional_basic/apps/ml/data_visualizer/index.ts @@ -15,9 +15,10 @@ export default function ({ loadTestFile }: FtrProviderContext) { ); // The data visualizer should work the same as with a trial license, except the missing create actions - // That's why 'index_data_visualizer_actions_panel' is not loaded here + // That's why the 'basic' version of 'index_data_visualizer_actions_panel' is loaded here loadTestFile( require.resolve('../../../../functional/apps/ml/data_visualizer/index_data_visualizer') ); + loadTestFile(require.resolve('./index_data_visualizer_actions_panel')); }); } diff --git a/x-pack/test/functional_basic/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts b/x-pack/test/functional_basic/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts new file mode 100644 index 0000000000000..8a59d6ed3ce2a --- /dev/null +++ b/x-pack/test/functional_basic/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + describe('index based actions panel on basic license', function () { + this.tags(['mlqa']); + + const indexPatternName = 'ft_farequote'; + const savedSearch = 'ft_farequote_kuery'; + const expectedQuery = 'airline: A* and responsetime > 5'; + + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.createIndexPatternIfNeeded(indexPatternName, '@timestamp'); + await ml.testResources.createSavedSearchFarequoteKueryIfNeeded(); + await ml.testResources.setKibanaTimeZoneToUTC(); + + await ml.securityUI.loginAsMlPowerUser(); + }); + + describe('view in discover page action', function () { + it('loads the source data in the data visualizer', async () => { + await ml.testExecution.logTestStep('loads the data visualizer selector page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToDataVisualizer(); + + await ml.testExecution.logTestStep('loads the saved search selection page'); + await ml.dataVisualizer.navigateToIndexPatternSelection(); + + await ml.testExecution.logTestStep('loads the index data visualizer page'); + await ml.jobSourceSelection.selectSourceForIndexBasedDataVisualizer(savedSearch); + }); + + it('navigates to Discover page', async () => { + await ml.testExecution.logTestStep('should not display create job card'); + await ml.dataVisualizerIndexBased.assertCreateAdvancedJobCardNotExists(); + + await ml.testExecution.logTestStep('displays the actions panel with view in Discover card'); + await ml.dataVisualizerIndexBased.assertActionsPanelExists(); + await ml.dataVisualizerIndexBased.assertViewInDiscoverCardExists(); + + await ml.testExecution.logTestStep('retains the query in Discover page'); + await ml.dataVisualizerIndexBased.clickViewInDiscoverButton(); + await ml.dataVisualizerIndexBased.assertDiscoverPageQuery(expectedQuery); + }); + }); + }); +} diff --git a/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts index 36cc1b1771e8b..b09270b1d0f78 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts @@ -127,8 +127,11 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('should display the data visualizer table'); await ml.dataVisualizerIndexBased.assertDataVisualizerTableExist(); - await ml.testExecution.logTestStep('should not display the actions panel with cards'); - await ml.dataVisualizerIndexBased.assertActionsPanelNotExists(); + await ml.testExecution.logTestStep('should display the actions panel with Discover card'); + await ml.dataVisualizerIndexBased.assertActionsPanelExists(); + await ml.dataVisualizerIndexBased.assertViewInDiscoverCardExists(); + + await ml.testExecution.logTestStep('should not display job cards'); await ml.dataVisualizerIndexBased.assertCreateAdvancedJobCardNotExists(); await ml.dataVisualizerIndexBased.assertRecognizerCardNotExists(ecExpectedModuleId); }); diff --git a/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts index f302be40a0e98..14cc4e93b37ab 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts @@ -127,8 +127,11 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('should display the data visualizer table'); await ml.dataVisualizerIndexBased.assertDataVisualizerTableExist(); - await ml.testExecution.logTestStep('should not display the actions panel with cards'); - await ml.dataVisualizerIndexBased.assertActionsPanelNotExists(); + await ml.testExecution.logTestStep('should display the actions panel with Discover card'); + await ml.dataVisualizerIndexBased.assertActionsPanelExists(); + await ml.dataVisualizerIndexBased.assertViewInDiscoverCardExists(); + + await ml.testExecution.logTestStep('should not display job cards'); await ml.dataVisualizerIndexBased.assertCreateAdvancedJobCardNotExists(); await ml.dataVisualizerIndexBased.assertRecognizerCardNotExists(ecExpectedModuleId); }); From 0c0a74b364b89a59eecd5f5c8119f06dfe03ad5b Mon Sep 17 00:00:00 2001 From: Constance Date: Fri, 5 Feb 2021 10:38:37 -0800 Subject: [PATCH 11/44] [Enterprise Search] eslint rule override: catch unnecessary backticks (#90347) * Add eslint rule for linting unnecessary backticks This needs to be below the Prettier overrides at the bottom of the file to override Prettier * Run --fix Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .eslintrc.js | 12 ++++++++++++ .../documents/document_detail_logic.test.ts | 4 ++-- .../components/product_card/product_card.tsx | 2 +- .../server/routes/app_search/documents.ts | 6 +++--- .../server/routes/app_search/engines.ts | 4 ++-- .../server/routes/app_search/search_settings.ts | 8 ++++---- 6 files changed, 24 insertions(+), 12 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index e85792c4f4ba6..9430b9bf24466 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1112,6 +1112,8 @@ module.exports = { /** * Enterprise Search overrides + * NOTE: We also have a single rule at the bottom of the file that + * overrides Prettier's default of not linting unnecessary backticks */ { // All files @@ -1268,6 +1270,16 @@ module.exports = { ...require('eslint-config-prettier/@typescript-eslint').rules, }, }, + /** + * Enterprise Search Prettier override + * Lints unnecessary backticks - @see https://github.com/prettier/eslint-config-prettier/blob/main/README.md#forbid-unnecessary-backticks + */ + { + files: ['x-pack/plugins/enterprise_search/**/*.{ts,tsx}'], + rules: { + quotes: ['error', 'single', { avoidEscape: true, allowTemplateLiterals: false }], + }, + }, { files: [ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts index 21f6855d1abcf..ef5ebad3aea13 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts @@ -62,7 +62,7 @@ describe('DocumentDetailLogic', () => { DocumentDetailLogic.actions.getDocumentDetails('1'); - expect(http.get).toHaveBeenCalledWith(`/api/app_search/engines/engine1/documents/1`); + expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/engine1/documents/1'); await nextTick(); expect(DocumentDetailLogic.actions.setFields).toHaveBeenCalledWith(fields); }); @@ -96,7 +96,7 @@ describe('DocumentDetailLogic', () => { mount(); DocumentDetailLogic.actions.deleteDocument('1'); - expect(http.delete).toHaveBeenCalledWith(`/api/app_search/engines/engine1/documents/1`); + expect(http.delete).toHaveBeenCalledWith('/api/app_search/engines/engine1/documents/1'); await nextTick(); expect(setQueuedSuccessMessage).toHaveBeenCalledWith( 'Successfully marked document for deletion. It will be deleted momentarily.' diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx index 162ea7f427306..d31daeef54de9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx @@ -53,7 +53,7 @@ export const ProductCard: React.FC = ({ product, image }) => { className="productCard" titleElement="h2" title={i18n.translate('xpack.enterpriseSearch.overview.productCard.heading', { - defaultMessage: `Elastic {productName}`, + defaultMessage: 'Elastic {productName}', values: { productName: product.NAME }, })} image={ diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/documents.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.ts index 3a408b62bd540..78463fc8724ac 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/documents.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.ts @@ -26,7 +26,7 @@ export function registerDocumentsRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: `/as/engines/:engineName/documents/new`, + path: '/as/engines/:engineName/documents/new', }) ); } @@ -46,7 +46,7 @@ export function registerDocumentRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: `/as/engines/:engineName/documents/:documentId`, + path: '/as/engines/:engineName/documents/:documentId', }) ); router.delete( @@ -60,7 +60,7 @@ export function registerDocumentRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: `/as/engines/:engineName/documents/:documentId`, + path: '/as/engines/:engineName/documents/:documentId', }) ); } diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts index edf5d1f3855e3..49ff0353bef03 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts @@ -56,7 +56,7 @@ export function registerEnginesRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: `/as/engines/:name/details`, + path: '/as/engines/:name/details', }) ); router.get( @@ -69,7 +69,7 @@ export function registerEnginesRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: `/as/engines/:name/overview_metrics`, + path: '/as/engines/:name/overview_metrics', }) ); } diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.ts index c68c8e61d539b..82b0497cd0946 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.ts @@ -38,7 +38,7 @@ export function registerSearchSettingsRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: `/as/engines/:engineName/search_settings/details`, + path: '/as/engines/:engineName/search_settings/details', }) ); @@ -52,7 +52,7 @@ export function registerSearchSettingsRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: `/as/engines/:engineName/search_settings/reset`, + path: '/as/engines/:engineName/search_settings/reset', }) ); @@ -67,7 +67,7 @@ export function registerSearchSettingsRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: `/as/engines/:engineName/search_settings`, + path: '/as/engines/:engineName/search_settings', }) ); @@ -88,7 +88,7 @@ export function registerSearchSettingsRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: `/as/engines/:engineName/search_settings_search`, + path: '/as/engines/:engineName/search_settings_search', }) ); } From 3ba3131912d7032799f7bb9972e9c1078b7bda33 Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Fri, 5 Feb 2021 10:43:03 -0800 Subject: [PATCH 12/44] Accessibility test- unskipping a functional test (kibana_overview.ts) (#90395) * fixes https://github.com/elastic/kibana/issues/74449 * unskipping accessibility test --- x-pack/test/accessibility/apps/kibana_overview.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/test/accessibility/apps/kibana_overview.ts b/x-pack/test/accessibility/apps/kibana_overview.ts index 395da78f6049c..068b600d2adf2 100644 --- a/x-pack/test/accessibility/apps/kibana_overview.ts +++ b/x-pack/test/accessibility/apps/kibana_overview.ts @@ -11,8 +11,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'home']); const a11y = getService('a11y'); - // FLAKY: https://github.com/elastic/kibana/issues/82226 - describe.skip('Kibana overview', () => { + describe('Kibana overview', () => { const esArchiver = getService('esArchiver'); before(async () => { From 6c7c936e0086908299e710c01d276488f1dfebf1 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 5 Feb 2021 10:44:16 -0800 Subject: [PATCH 13/44] [DOCS] Update more installation details (#90469) --- docs/setup/docker.asciidoc | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/setup/docker.asciidoc b/docs/setup/docker.asciidoc index 2e70df09b5c37..75a9799d70fbd 100644 --- a/docs/setup/docker.asciidoc +++ b/docs/setup/docker.asciidoc @@ -11,12 +11,8 @@ A list of all published Docker images and tags is available at https://www.docker.elastic.co[www.docker.elastic.co]. The source code is in https://github.com/elastic/dockerfiles/tree/{branch}/kibana[GitHub]. -These images are free to use under the Elastic license. They contain open source -and free commercial features and access to paid commercial features. -<> to try out all of the -paid commercial features. See the -https://www.elastic.co/subscriptions[Subscriptions] page for information about -Elastic license levels. +These images contain both free and subscription features. +<> to try out all of the features. [float] [[pull-image]] From 095233d7278b15b9d501b2009bc6d46f33022ccf Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Fri, 5 Feb 2021 13:48:25 -0500 Subject: [PATCH 14/44] Fix Visualize Link Redirecting to Dashboard Linked Visualization (#90243) --- .../public/application/visualize_constants.ts | 1 + src/plugins/visualize/public/plugin.ts | 19 +++++++++++- .../apps/dashboard/edit_visualizations.js | 30 ++++++++++++++++--- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/plugins/visualize/public/application/visualize_constants.ts b/src/plugins/visualize/public/application/visualize_constants.ts index c74cabdb9fe82..7dbf5be77b74d 100644 --- a/src/plugins/visualize/public/application/visualize_constants.ts +++ b/src/plugins/visualize/public/application/visualize_constants.ts @@ -9,6 +9,7 @@ export const APP_NAME = 'visualize'; export const VisualizeConstants = { + VISUALIZE_BASE_PATH: '/app/visualize', LANDING_PAGE_PATH: '/', WIZARD_STEP_1_PAGE_PATH: '/new', WIZARD_STEP_2_PAGE_PATH: '/new/configure', diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 39074735e2aeb..1cad0ca7ca396 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -78,6 +78,7 @@ export class VisualizePlugin private appStateUpdater = new BehaviorSubject(() => ({})); private stopUrlTracking: (() => void) | undefined = undefined; private currentHistory: ScopedHistory | undefined = undefined; + private isLinkedToOriginatingApp: (() => boolean) | undefined = undefined; private readonly visEditorsRegistry = createVisEditorsRegistry(); @@ -94,7 +95,7 @@ export class VisualizePlugin setActiveUrl, restorePreviousUrl, } = createKbnUrlTracker({ - baseUrl: core.http.basePath.prepend('/app/visualize'), + baseUrl: core.http.basePath.prepend(VisualizeConstants.VISUALIZE_BASE_PATH), defaultSubUrl: '#/', storageKey: `lastUrl:${core.http.basePath.get()}:visualize`, navLinkUpdater$: this.appStateUpdater, @@ -114,6 +115,15 @@ export class VisualizePlugin }, ], getHistory: () => this.currentHistory!, + onBeforeNavLinkSaved: (urlToSave: string) => { + if ( + !urlToSave.includes(`${VisualizeConstants.EDIT_PATH}/`) && + this.isLinkedToOriginatingApp?.() + ) { + return core.http.basePath.prepend(VisualizeConstants.VISUALIZE_BASE_PATH); + } + return urlToSave; + }, }); this.stopUrlTracking = () => { stopUrlTracker(); @@ -134,6 +144,13 @@ export class VisualizePlugin const [coreStart, pluginsStart] = await core.getStartServices(); this.currentHistory = params.history; + // allows the urlTracker to only save URLs that are not linked to an originatingApp + this.isLinkedToOriginatingApp = () => { + return Boolean( + pluginsStart.embeddable.getStateTransfer().getIncomingEditorState()?.originatingApp + ); + }; + // make sure the index pattern list is up to date pluginsStart.data.indexPatterns.clearCache(); // make sure a default index pattern exists diff --git a/test/functional/apps/dashboard/edit_visualizations.js b/test/functional/apps/dashboard/edit_visualizations.js index ab8de37122bb7..0996fbe7cf0d7 100644 --- a/test/functional/apps/dashboard/edit_visualizations.js +++ b/test/functional/apps/dashboard/edit_visualizations.js @@ -12,6 +12,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'common', 'visEditor']); const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); const kibanaServer = getService('kibanaServer'); const dashboardPanelActions = getService('dashboardPanelActions'); const dashboardVisualizations = getService('dashboardVisualizations'); @@ -25,10 +26,14 @@ export default function ({ getService, getPageObjects }) { await PageObjects.visualize.clickMarkdownWidget(); await PageObjects.visEditor.setMarkdownTxt(originalMarkdownText); await PageObjects.visEditor.clickGo(); - await PageObjects.visualize.saveVisualizationExpectSuccess(title, { - saveAsNew: true, - redirectToOrigin: true, - }); + if (title) { + await PageObjects.visualize.saveVisualizationExpectSuccess(title, { + saveAsNew: true, + redirectToOrigin: true, + }); + } else { + await PageObjects.visualize.saveVisualizationAndReturn(); + } }; const editMarkdownVis = async () => { @@ -86,5 +91,22 @@ export default function ({ getService, getPageObjects }) { const markdownText = await testSubjects.find('markdownBody'); expect(await markdownText.getVisibleText()).to.eql(originalMarkdownText); }); + + it('visualize app menu navigates to the visualize listing page if the last opened visualization was by value', async () => { + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.clickNewDashboard(); + + // Create markdown by value. + await createMarkdownVis(); + + // Edit then save and return + await editMarkdownVis(); + await PageObjects.visualize.saveVisualizationAndReturn(); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await appsMenu.clickLink('Visualize'); + await PageObjects.common.clickConfirmOnModal(); + expect(await testSubjects.exists('visualizationLandingPage')).to.be(true); + }); }); } From b4248465cd6ec63932ba774928c00dd2616aed83 Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Fri, 5 Feb 2021 13:57:42 -0500 Subject: [PATCH 15/44] [Security Solution][Endpoint][Admin] Locked ransomware card (#90210) * [Security Solution][Endpoint][Admin] Locked card for ransomware policy --- .../pages/policy/view/policy_details.test.tsx | 5 ++ .../pages/policy/view/policy_details_form.tsx | 3 +- .../policy/view/policy_forms/locked_card.tsx | 82 +++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/locked_card.tsx diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx index 154e26dd0f380..1ae4144a26835 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx @@ -336,6 +336,11 @@ describe('Policy Details', () => { const ransomware = policyView.find('EuiPanel[data-test-subj="ransomwareProtectionsForm"]'); expect(ransomware).toHaveLength(0); }); + + it('shows the locked card in place of 1 paid feature', () => { + const lockedCard = policyView.find('EuiCard[data-test-subj="lockedPolicyCard"]'); + expect(lockedCard).toHaveLength(1); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx index aa1d62c2e1430..528f3afc1e64a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx @@ -13,6 +13,7 @@ import { LinuxEvents, MacEvents, WindowsEvents } from './policy_forms/events'; import { AdvancedPolicyForms } from './policy_advanced'; import { AntivirusRegistrationForm } from './components/antivirus_registration_form'; import { Ransomware } from './policy_forms/protections/ransomware'; +import { LockedPolicyCard } from './policy_forms/locked_card'; import { useLicense } from '../../../../common/hooks/use_license'; export const PolicyDetailsForm = memo(() => { @@ -36,7 +37,7 @@ export const PolicyDetailsForm = memo(() => { - {isPlatinumPlus && } + {isPlatinumPlus ? : } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/locked_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/locked_card.tsx new file mode 100644 index 0000000000000..5c19a10307608 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/locked_card.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { EuiCard, EuiIcon, EuiTextColor, EuiLink, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; + +const LockedPolicyDiv = styled.div` + .euiCard__betaBadgeWrapper { + .euiCard__betaBadge { + width: auto; + } + } + .lockedCardDescription { + padding: 0 ${(props) => props.theme.eui.fractions.thirds.percentage}; + } +`; + +export const LockedPolicyCard = memo(() => { + return ( + + } + title={ +

    + + + +

    + } + description={ + + +

    + + + +

    +
    + +

    + + + + ), + }} + /> +

    +
    +
    + } + /> +
    + ); +}); +LockedPolicyCard.displayName = 'LockedPolicyCard'; From e202ceab299c7fcf3fa13ea7f74124d658e18df8 Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Fri, 5 Feb 2021 12:15:44 -0700 Subject: [PATCH 16/44] [Security Solution] [Timeline] Endpoint row renderers (1st batch) (#89810) ## [Security Solution] [Timeline] Endpoint row renderers (1st batch) This PR implements the 1st batch of Endpoint (`event.module: "endpoint"`) row renderers by updating and enhancing some of the existing "Endgame" (`event.module: "endgame"`) row renderers to use the latest [ECS fields](https://www.elastic.co/guide/en/ecs/current/ecs-field-reference.html). The following Endpoint events will be rendered via row renderers in Timeline: | event.dataset | event.action | |--------------------------|---------------------| | endpoint.events.file | creation | | endpoint.events.file | deletion | | endpoint.events.process | start | | endpoint.events.process | end | | endpoint.events.network | lookup_requested | | endpoint.events.network | lookup_result | | endpoint.events.network | connection_accepted | | endpoint.events.network | disconnect_received | | endpoint.events.security | log_on | | endpoint.events.security | log_off | ## File (FIM) Creation events Endpoint File (FIM) Creation events with the following `event.dataset` and `event.action` will be rendered in Timeline via row renderers: ``` event.dataset: endpoint.events.file and event.action: creation ``` ### Sample rendered File (FIM) Creation event ![endpoint_file_creation](https://user-images.githubusercontent.com/4459398/106036793-ff522f80-6092-11eb-9e3b-c24538129bea.png) Each field with `this formatting` is draggable (to pivot a search) in the row-rendered event: `SYSTEM` \ `NT AUTHORITY` @ `win2019-endpoint` created a file `WimProvider.dll` in `C:\Windows\TEMP\F590BACBAE94\WimProvider.dll` via `MsMpEng.exe` `(2424)` ### Fields in a File (FIM) Creation event `user.name` \ `user.domain` @ `host.name` created a file `file.name` in `file.path` via `process.name` `(process.pid)` ## File (FIM) Deletion events Endpoint File (FIM) Deletion events with the following `event.dataset` and `event.action` will be rendered in Timeline via row renderers: ``` event.dataset: endpoint.events.file and event.action: deletion ``` ### Sample rendered File (FIM) Deletion event ![endpoint_file_deletion](https://user-images.githubusercontent.com/4459398/106037520-088fcc00-6094-11eb-985d-ba8cead9fec9.png) `SYSTEM` \ `NT AUTHORITY` @ `windows-endpoint-1` deleted a file `AM_Delta_Patch_1.329.2793.0.exe` in `C:\Windows\SoftwareDistribution\Download\Install\AM_Delta_Patch_1.329.2793.0.exe` via `svchost.exe` `(1728)` ### Fields in a File (FIM) Deletion event `user.name` \ `user.domain` @ `host.name` deleted a file `file.name` in `file.path` via `process.name` `(process.pid)` ## Process Start events Endpoint Process Start events with the following `event.dataset` and `event.action` will be rendered in Timeline via row renderers: ``` event.dataset: endpoint.events.process and event.action: start ``` ### Sample rendered Process Start event ![creation-event](https://user-images.githubusercontent.com/4459398/106061579-c7f37b00-60b2-11eb-9bc4-224e671baa4a.png) `SYSTEM` \ `NT AUTHORITY` @ `win2019-endpoint` started process `conhost.exe` (`376`) `C:\Windows\system32\conhost.exe` `0xffffffff` `-ForceV1` via parent process `sshd.exe` (`6460`) `sha256 697334c236cce7d4c9e223146ee683a1219adced9729d4ae771fd6a1502a6b63` `sha1 e19da2c35ba1c38adf12d1a472c1fcf1f1a811a7` `md5 1b0e9b5fcb62de0787235ecca560b610` ### Fields in a Process Start event The following fields will be used to render a Process Start event: `user.name` \ `user.domain` @ `host.name` started process `process.name` (`process.pid`) `process.args` via parent process `process.parent.name` (`process.parent.pid`) `process.hash.sha256` `process.hash.sha1` `process.hash.md5` ## Process End events Endpoint Process End events with the following `event.dataset` and `event.action` will be rendered in Timeline via row renderers: ``` event.dataset: endpoint.events.process and event.action: end ``` ### Sample rendered Process End event ![endpoint_process_end](https://user-images.githubusercontent.com/4459398/106076527-f1b99b80-60cc-11eb-8ff8-2da78a1fcb8f.png) `SYSTEM` \ `NT AUTHORITY` @ `win2019-endpoint` terminated process `svchost.exe` (`10392`) `C:\Windows\System32\svchost.exe` `-k` `netsvcs` `-p` `-s` `NetSetupSvc` with exit code `0` via parent process `services.exe` `(568)` `7fd065bac18c5278777ae44908101cdfed72d26fa741367f0ad4d02020787ab6` `a1385ce20ad79f55df235effd9780c31442aa234` `8a0a29438052faed8a2532da50455756` ### Fields in a Process End event The following fields will be used to render a Process End event: `user.name` \ `user.domain` @ `host.name` terminated process `process.name` (`process.pid`) with exit code `process.exit_code` via parent process `process.parent.name` (`process.parent.pid`) `process.hash.sha256` `process.hash.sha1` `process.hash.md5` ## Network (DNS) Lookup Requested events Endpoint Network (DNS) Lookup Requested events with the following `event.dataset` and `event.action` will be rendered in Timeline via row renderers: ``` event.dataset: endpoint.events.network and event.action: lookup_requested ``` ### Runtime matching criteria All Network Lookup Requested events, including Endpoint and non-Endpoint DNS events matching the following criteria will be rendered: ``` dns.question.type: * and dns.question.name: * ``` ### Sample rendered Network Lookup Requested event ![network_lookup_requested](https://user-images.githubusercontent.com/4459398/106191208-cdf76380-6167-11eb-9be7-aaf78e4cfdd3.png) `SYSTEM` \ `NT AUTHORITY` @ `windows-endpoint-1` asked for `logging.googleapis.com` with question type `A` via `google_osconfig_agent.exe` `(4064)` `dns` ### Fields in a Network Lookup Requested event The following fields will be used to render a Network Lookup Request event: `user.name` \ `user.domain` @ `host.name` asked for `dns.question.name` with question type `dns.question.type` via `process.name` `(process.pid)` `network.protocol` ## Network Lookup Result events Endpoint Network (DNS) Lookup Result events with the following `event.dataset` and `event.action` will be rendered in Timeline via row renderers: ``` event.dataset: endpoint.events.network and event.action: lookup_result ``` ### Runtime matching criteria All Network Lookup Result events, including Endpoint and non-Endpoint DNS events matching the following criteria will be rendered: ``` dns.question.type: * and dns.question.name: * ``` ### Sample rendered Network Lookup Result event ![network_lookup_result](https://user-images.githubusercontent.com/4459398/106192595-a43f3c00-6169-11eb-95bc-4ebe331f1231.png) `SYSTEM` \ `NT AUTHORITY` @ `windows-endpoint-1` asked for `logging.googleapis.com` with question type `AAAA` via `GCEWindowsAgent.exe` `(684)` `dns` ### Fields in a Network Lookup Result event The following fields will be used to render a Network Lookup Result event: `user.name` \ `user.domain` @ `host.name` asked for `dns.question.name` with question type `dns.question.type` via `process.name` `(process.pid)` `network.protocol` ## Network Connection Accepted events Endpoint Network Connection Accepted events with the following `event.dataset` and `event.action` will be rendered in Timeline via row renderers: ``` event.dataset: endpoint.events.network and event.action: connection_accepted ```` ### Sample rendered Network Connection Accepted event ![network_connection_accepted](https://user-images.githubusercontent.com/4459398/106200497-4f54f300-6174-11eb-8879-06b7bfc88edf.png) Network Connection Accepted events, like the one in the screenshot above, are also rendered by the _Netflow_ row renderer, which displays information that includes the directionality of the connection, protocol, and source / destination details. `NETWORK SERVICE` \ `NT AUTHORITY` @ `windows-endpoint-1` accepted a connection via `svchost.exe` `(328)` with result `success` ### Fields in a Network Connection Accepted event `user.name` \ `user.domain` @ `host.name` accepted a connection via `process.name` `(process.pid)` with result `event.outcome` ## Network Disconnect Received events Endpoint Network Disconnect Received events with the following `event.dataset` and `event.action` will be rendered in Timeline via row renderers: ``` event.dataset: endpoint.events.network and event.action: disconnect_received ```` ### Sample rendered Network Disconnect Received event ![network_disconnect_received](https://user-images.githubusercontent.com/4459398/106205196-56cbca80-617b-11eb-83d3-26aa9670f114.png) Network Disconnect Received events, like the one in the screenshot above, are also rendered by the _Netflow_ row renderer, which displays information that includes the directionality of the connection, protocol, and source / destination details. `NETWORK SERVICE` \ `NT AUTHORITY` @ `windows-endpoint-1` disconnected via `svchost.exe` `(328)` ### Fields in a Network Disconnect Received event `user.name` \ `user.domain` @ `host.name` disconnected via `process.name` `(process.pid)` ## Security Log On events Endpoint Security Log On events with the following `event.dataset` and `event.action` will be rendered in Timeline via row renderers: ``` event.dataset: endpoint.events.security and event.action: log_on ``` ### `event.outcome: "success"` vs `event.outcome: "failure"` The row renderer for Security Log On events uses the `event.outcome` field to display different results for events matching: ``` event.dataset: endpoint.events.security and event.action: log_on and event.outcome: success ``` vs events matching: ``` event.dataset: endpoint.events.security and event.action: log_on and event.outcome: failure ``` ### Sample rendered Security Log On / `event.outcome: "success"` event ![security_log_on_success](https://user-images.githubusercontent.com/4459398/106210917-fcd00280-6184-11eb-9c1c-564cfb375539.png) `SYSTEM` \ `NT AUTHORITY` @ `win2019-endpoint` successfully logged in via `C:\Program Files\OpenSSH-Win64\sshd.exe` ### Fields in an Security Log On / `event.outcome: "success"` event `user.name` \ `user.domain` @ `host.name` successfully logged in via `process.name` (`process.pid`) ### Sample rendered Security Log On / `event.outcome: "failure"` event ![security_log_on_failure](https://user-images.githubusercontent.com/4459398/106211893-b2e81c00-6186-11eb-9c34-43227c15a1f0.png) `SYSTEM` \ `NT AUTHORITY` @ `win2019-endpoint` failed to log in via `C:\Program Files\OpenSSH-Win64\sshd.exe` ### Fields in an Security Log On / `event.outcome: "failure"` event `user.name` \ `user.domain` @ `host.name` failed to log in via `process.name` (`process.pid`) ## Security Log Off events Endpoint Security Log Off events with the following `event.dataset` and `event.action` will be rendered in Timeline via row renderers: ``` event.dataset: endpoint.events.security and event.action: log_off ``` ### Sample rendered Security Log Off event ![security_log_off](https://user-images.githubusercontent.com/4459398/106212499-0018bd80-6188-11eb-9e91-971f360ee87a.png) `SYSTEM` \ `NT AUTHORITY` @ `win2019-endpoint` logged off via `C:\Program Files\OpenSSH-Win64\sshd.exe` ### Fields in a Security Log Off event `user.name` \ `user.domain` @ `host.name` logged off via `process.name` (`process.pid`) --- .../common/ecs/process/index.ts | 6 + .../common/mock/mock_endgame_ecs_data.ts | 594 ++++++++++++++++++ .../process_draggable.test.tsx.snap | 43 +- .../endgame_security_event_details.tsx | 2 + ...dgame_security_event_details_line.test.tsx | 19 + .../endgame_security_event_details_line.tsx | 4 +- .../body/renderers/endgame/helpers.test.tsx | 112 +++- .../body/renderers/endgame/helpers.ts | 15 +- .../body/renderers/endgame/translations.ts | 14 + .../renderers/exit_code_draggable.test.tsx | 101 ++- .../body/renderers/exit_code_draggable.tsx | 34 +- .../timeline/body/renderers/helpers.tsx | 7 +- .../parent_process_draggable.test.tsx | 48 +- .../renderers/parent_process_draggable.tsx | 40 +- .../body/renderers/process_draggable.tsx | 85 +-- .../system/generic_file_details.test.tsx | 285 +++++++-- .../renderers/system/generic_file_details.tsx | 15 + .../system/generic_row_renderer.test.tsx | 264 ++++++++ .../renderers/system/generic_row_renderer.tsx | 65 +- .../timeline/factory/events/all/constants.ts | 3 + 20 files changed, 1590 insertions(+), 166 deletions(-) diff --git a/x-pack/plugins/security_solution/common/ecs/process/index.ts b/x-pack/plugins/security_solution/common/ecs/process/index.ts index cc4a961a5b528..3a8ccc309aecb 100644 --- a/x-pack/plugins/security_solution/common/ecs/process/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/process/index.ts @@ -7,7 +7,9 @@ export interface ProcessEcs { entity_id?: string[]; + exit_code?: number[]; hash?: ProcessHashData; + parent?: ProcessParentData; pid?: number[]; name?: string[]; ppid?: number[]; @@ -24,6 +26,10 @@ export interface ProcessHashData { sha256?: string[]; } +export interface ProcessParentData { + name?: string[]; +} + export interface Thread { id?: number[]; start?: string[]; diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_endgame_ecs_data.ts b/x-pack/plugins/security_solution/public/common/mock/mock_endgame_ecs_data.ts index 98bedbb08028b..1082b5f9474e5 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_endgame_ecs_data.ts +++ b/x-pack/plugins/security_solution/public/common/mock/mock_endgame_ecs_data.ts @@ -58,6 +58,121 @@ export const mockEndgameDnsRequest: Ecs = { }, }; +export const mockEndpointNetworkLookupRequestedEvent: Ecs = { + host: { + os: { + full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1697)'], + name: ['Windows'], + version: ['1809 (10.0.17763.1697)'], + family: ['windows'], + kernel: ['1809 (10.0.17763.1697)'], + platform: ['windows'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + name: ['win2019-endpoint'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['d8ad572e-d224-4044-a57d-f5a84c0dfe5d'], + }, + event: { + category: ['network'], + kind: ['event'], + created: ['2021-01-25T16:44:40.788Z'], + module: ['endpoint'], + action: ['lookup_requested'], + type: ['protocol,info'], + id: ['LzzWB9jjGmCwGMvk++++6FZj'], + dataset: ['endpoint.events.network'], + }, + process: { + name: ['google_osconfig_agent.exe'], + pid: [3272], + entity_id: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTMyNzItMTMyNTUwNzg4NjguNjUzODkxNTAw', + ], + executable: ['C:\\Program Files\\Google\\OSConfig\\google_osconfig_agent.exe'], + }, + dns: { + question: { + name: ['logging.googleapis.com'], + type: ['A'], + }, + }, + agent: { + type: ['endpoint'], + }, + user: { + name: ['SYSTEM'], + domain: ['NT AUTHORITY'], + }, + network: { + protocol: ['dns'], + }, + message: [ + 'DNS query is completed for the name logging.googleapis.com, type 1, query options 1073766400 with status 87 Results', + ], + timestamp: '2021-01-25T16:44:40.788Z', + _id: 'sUNzOncBPmkOXwyN9VbT', +}; + +export const mockEndpointNetworkLookupResultEvent: Ecs = { + host: { + os: { + full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1697)'], + name: ['Windows'], + version: ['1809 (10.0.17763.1697)'], + family: ['windows'], + kernel: ['1809 (10.0.17763.1697)'], + platform: ['windows'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + name: ['win2019-endpoint'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['d8ad572e-d224-4044-a57d-f5a84c0dfe5d'], + }, + event: { + category: ['network'], + kind: ['event'], + outcome: ['success'], + created: ['2021-01-25T16:44:40.789Z'], + module: ['endpoint'], + action: ['lookup_result'], + type: ['protocol,info'], + id: ['LzzWB9jjGmCwGMvk++++6FZq'], + dataset: ['endpoint.events.network'], + }, + process: { + name: ['google_osconfig_agent.exe'], + pid: [3272], + entity_id: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTMyNzItMTMyNTUwNzg4NjguNjUzODkxNTAw', + ], + executable: ['C:\\Program Files\\Google\\OSConfig\\google_osconfig_agent.exe'], + }, + agent: { + type: ['endpoint'], + }, + dns: { + question: { + name: ['logging.googleapis.com'], + type: ['AAAA'], + }, + }, + user: { + name: ['SYSTEM'], + domain: ['NT AUTHORITY'], + }, + network: { + protocol: ['dns'], + }, + message: [ + 'DNS query is completed for the name logging.googleapis.com, type 28, query options 2251800887582720 with status 0 Results', + ], + timestamp: '2021-01-25T16:44:40.789Z', + _id: 'skNzOncBPmkOXwyN9VbT', +}; + export const mockEndgameFileCreateEvent: Ecs = { _id: '98jPcG0BOpWiDweSouzg', user: { @@ -91,6 +206,59 @@ export const mockEndgameFileCreateEvent: Ecs = { }, }; +export const mockEndpointFileCreationEvent: Ecs = { + file: { + path: ['C:\\Windows\\TEMP\\E38FD162-B6E6-4799-B52D-F590BACBAE94\\WimProvider.dll'], + extension: ['dll'], + name: ['WimProvider.dll'], + }, + host: { + os: { + full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1697)'], + name: ['Windows'], + version: ['1809 (10.0.17763.1697)'], + family: ['windows'], + kernel: ['1809 (10.0.17763.1697)'], + platform: ['windows'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + name: ['win2019-endpoint'], + architecture: ['x86_64'], + ip: ['10.9.8.7'], + id: ['d8ad572e-d224-4044-a57d-f5a84c0dfe5d'], + }, + event: { + category: ['file'], + kind: ['event'], + created: ['2021-01-25T16:21:56.832Z'], + module: ['endpoint'], + action: ['creation'], + type: ['creation'], + id: ['LzzWB9jjGmCwGMvk++++6FEM'], + dataset: ['endpoint.events.file'], + }, + process: { + name: ['MsMpEng.exe'], + pid: [2424], + entity_id: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTI0MjQtMTMyNTUwNzg2OTAuNDQ1MzY0NzAw', + ], + executable: [ + 'C:\\ProgramData\\Microsoft\\Windows Defender\\Platform\\4.18.2011.6-0\\MsMpEng.exe', + ], + }, + agent: { + type: ['endpoint'], + }, + user: { + name: ['SYSTEM'], + domain: ['NT AUTHORITY'], + }, + message: ['Endpoint file event'], + timestamp: '2021-01-25T16:21:56.832Z', + _id: 'eSdbOncBLJMagDUQ3YFs', +}; + export const mockEndgameFileDeleteEvent: Ecs = { _id: 'OMjPcG0BOpWiDweSeuW9', user: { @@ -123,6 +291,58 @@ export const mockEndgameFileDeleteEvent: Ecs = { }, }; +export const mockEndpointFileDeletionEvent: Ecs = { + file: { + path: ['C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.329.2793.0.exe'], + extension: ['exe'], + name: ['AM_Delta_Patch_1.329.2793.0.exe'], + }, + host: { + os: { + full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1697)'], + name: ['Windows'], + version: ['1809 (10.0.17763.1697)'], + family: ['windows'], + kernel: ['1809 (10.0.17763.1697)'], + platform: ['windows'], + }, + mac: ['11:22:33:44:55:66'], + name: ['windows-endpoint-1'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['ce6fa3c3-fda1-4984-9bce-f6d602a5bd1a'], + }, + event: { + category: ['file'], + kind: ['event'], + created: ['2021-01-25T22:50:36.783Z'], + module: ['endpoint'], + action: ['deletion'], + type: ['deletion'], + id: ['Lzty2lsJxA05IUWg++++CBsc'], + dataset: ['endpoint.events.file'], + }, + process: { + name: ['svchost.exe'], + pid: [1728], + entity_id: [ + 'YjUwNDNiMTMtYTdjNi0xZGFlLTEyZWQtODQ1ZDlhNTRhZmQyLTE3MjgtMTMyNTQ5ODc2MjYuNjg3OTg0MDAw', + ], + executable: ['C:\\Windows\\System32\\svchost.exe'], + }, + user: { + id: ['S-1-5-18'], + name: ['SYSTEM'], + domain: ['NT AUTHORITY'], + }, + agent: { + type: ['endpoint'], + }, + message: ['Endpoint file event'], + timestamp: '2021-01-25T22:50:36.783Z', + _id: 'mnXHO3cBPmkOXwyNlyv_', +}; + export const mockEndgameIpv4ConnectionAcceptEvent: Ecs = { _id: 'LsjPcG0BOpWiDweSCNfu', user: { @@ -213,6 +433,74 @@ export const mockEndgameIpv6ConnectionAcceptEvent: Ecs = { }, }; +export const mockEndpointNetworkConnectionAcceptedEvent: Ecs = { + host: { + os: { + full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1697)'], + name: ['Windows'], + version: ['1809 (10.0.17763.1697)'], + family: ['windows'], + kernel: ['1809 (10.0.17763.1697)'], + platform: ['windows'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + name: ['windows-endpoint-1'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['ce6fa3c3-fda1-4984-9bce-f6d602a5bd1a'], + }, + event: { + category: ['network'], + kind: ['event'], + outcome: ['success'], + created: ['2021-01-25T16:44:45.048Z'], + module: ['endpoint'], + action: ['connection_accepted'], + type: ['start'], + id: ['Lzty2lsJxA05IUWg++++C1CY'], + dataset: ['endpoint.events.network'], + }, + process: { + name: ['svchost.exe'], + pid: [328], + entity_id: [ + 'YjUwNDNiMTMtYTdjNi0xZGFlLTEyZWQtODQ1ZDlhNTRhZmQyLTMyOC0xMzI1NDk4NzUwNS45OTYxMjUzMDA=', + ], + executable: ['C:\\Windows\\System32\\svchost.exe'], + }, + source: { + geo: { + region_name: ['North Carolina'], + region_iso_code: ['US-NC'], + city_name: ['Concord'], + country_iso_code: ['US'], + continent_name: ['North America'], + country_name: ['United States'], + }, + ip: ['10.1.2.3'], + port: [64557], + }, + destination: { + port: [3389], + ip: ['10.50.60.70'], + }, + user: { + id: ['S-1-5-20'], + name: ['NETWORK SERVICE'], + domain: ['NT AUTHORITY'], + }, + agent: { + type: ['endpoint'], + }, + network: { + direction: ['incoming'], + transport: ['tcp'], + }, + message: ['Endpoint network event'], + timestamp: '2021-01-25T16:44:45.048Z', + _id: 'tUN0OncBPmkOXwyNOGPV', +}; + export const mockEndgameIpv4DisconnectReceivedEvent: Ecs = { _id: 'hMjPcG0BOpWiDweSoOin', user: { @@ -309,6 +597,75 @@ export const mockEndgameIpv6DisconnectReceivedEvent: Ecs = { }, }; +export const mockEndpointDisconnectReceivedEvent: Ecs = { + host: { + os: { + full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1697)'], + name: ['Windows'], + version: ['1809 (10.0.17763.1697)'], + family: ['windows'], + kernel: ['1809 (10.0.17763.1697)'], + platform: ['windows'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + name: ['windows-endpoint-1'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['ce6fa3c3-fda1-4984-9bce-f6d602a5bd1a'], + }, + event: { + category: ['network'], + kind: ['event'], + created: ['2021-01-25T16:44:47.004Z'], + module: ['endpoint'], + action: ['disconnect_received'], + type: ['end'], + id: ['Lzty2lsJxA05IUWg++++C1Ch'], + dataset: ['endpoint.events.network'], + }, + process: { + name: ['svchost.exe'], + pid: [328], + entity_id: [ + 'YjUwNDNiMTMtYTdjNi0xZGFlLTEyZWQtODQ1ZDlhNTRhZmQyLTMyOC0xMzI1NDk4NzUwNS45OTYxMjUzMDA=', + ], + executable: ['C:\\Windows\\System32\\svchost.exe'], + }, + source: { + geo: { + region_name: ['North Carolina'], + region_iso_code: ['US-NC'], + city_name: ['Concord'], + country_iso_code: ['US'], + continent_name: ['North America'], + country_name: ['United States'], + }, + ip: ['10.20.30.40'], + port: [64557], + bytes: [1192], + }, + destination: { + bytes: [1615], + port: [3389], + ip: ['10.11.12.13'], + }, + user: { + id: ['S-1-5-20'], + name: ['NETWORK SERVICE'], + domain: ['NT AUTHORITY'], + }, + agent: { + type: ['endpoint'], + }, + network: { + direction: ['incoming'], + transport: ['tcp'], + }, + message: ['Endpoint network event'], + timestamp: '2021-01-25T16:44:47.004Z', + _id: 'uUN0OncBPmkOXwyNOGPV', +}; + export const mockEndgameUserLogon: Ecs = { _id: 'QsjPcG0BOpWiDweSeuRE', user: { @@ -357,6 +714,92 @@ export const mockEndgameUserLogon: Ecs = { }, }; +export const mockEndpointSecurityLogOnSuccessEvent: Ecs = { + host: { + os: { + full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1697)'], + name: ['Windows'], + version: ['1809 (10.0.17763.1697)'], + family: ['windows'], + kernel: ['1809 (10.0.17763.1697)'], + platform: ['windows'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + name: ['win2019-endpoint'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['d8ad572e-d224-4044-a57d-f5a84c0dfe5d'], + }, + event: { + category: ['authentication', 'session'], + kind: ['event'], + outcome: ['success'], + created: ['2021-01-25T16:24:51.761Z'], + module: ['endpoint'], + action: ['log_on'], + type: ['start'], + id: ['LzzWB9jjGmCwGMvk++++6FKC'], + dataset: ['endpoint.events.security'], + }, + process: { + name: ['C:\\Program Files\\OpenSSH-Win64\\sshd.exe'], + entity_id: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQzNDQtMTMyNTYwNjU0ODYuMzIwNDI3MDAw', + ], + executable: ['C:\\Program Files\\OpenSSH-Win64\\sshd.exe'], + pid: [90210], + }, + agent: { + type: ['endpoint'], + }, + user: { + name: ['SYSTEM'], + domain: ['NT AUTHORITY'], + }, + message: ['Endpoint security event'], + timestamp: '2021-01-25T16:24:51.761Z', + _id: 'eSlgOncBLJMagDUQ-yBL', +}; + +export const mockEndpointSecurityLogOnFailureEvent: Ecs = { + host: { + os: { + full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1637)'], + name: ['Windows'], + version: ['1809 (10.0.17763.1637)'], + kernel: ['1809 (10.0.17763.1637)'], + platform: ['windows'], + family: ['windows'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + ip: ['10.1.2.3'], + name: ['win2019-endpoint'], + id: ['d8ad572e-d224-4044-a57d-f5a84c0dfe5d'], + architecture: ['x86_64'], + }, + event: { + category: ['authentication', 'session'], + module: ['endpoint'], + kind: ['event'], + outcome: ['failure'], + action: ['log_on'], + created: ['2020-12-28T04:05:01.409Z'], + type: ['start'], + id: ['Ly1AjdVRChqy2iq3++++3jlX'], + dataset: ['endpoint.events.security'], + }, + process: { + name: ['C:\\Program Files\\OpenSSH-Win64\\sshd.exe'], + pid: [90210], + }, + agent: { + type: ['endpoint'], + }, + message: ['Endpoint security event'], + timestamp: '2020-12-28T04:05:01.409Z', + _id: 's8GIp3YBN9Y7_e914Upz', +}; + export const mockEndgameAdminLogon: Ecs = { _id: 'psjPcG0BOpWiDweSoelR', user: { @@ -488,6 +931,49 @@ export const mockEndgameUserLogoff: Ecs = { }, }; +export const mockEndpointSecurityLogOffEvent: Ecs = { + host: { + os: { + full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1697)'], + name: ['Windows'], + version: ['1809 (10.0.17763.1697)'], + family: ['windows'], + kernel: ['1809 (10.0.17763.1697)'], + platform: ['windows'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + name: ['win2019-endpoint'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['d8ad572e-d224-4044-a57d-f5a84c0dfe5d'], + }, + event: { + category: ['authentication,session'], + kind: ['event'], + outcome: ['success'], + created: ['2021-01-26T23:27:27.610Z'], + module: ['endpoint'], + action: ['log_off'], + type: ['end'], + id: ['LzzWB9jjGmCwGMvk++++6l0y'], + dataset: ['endpoint.events.security'], + }, + process: { + entity_id: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU4MC0xMzI1NTA3ODY2Ny45MTg5Njc1MDA=', + ], + executable: ['C:\\Windows\\System32\\lsass.exe'], + pid: [90210], + }, + user: { + name: ['SYSTEM'], + domain: ['NT AUTHORITY'], + }, + message: ['Endpoint security event'], + timestamp: '2021-01-26T23:27:27.610Z', + _id: 'ZesLQXcBPmkOXwyNdT1a', +}; + export const mockEndgameCreationEvent: Ecs = { _id: 'BcjPcG0BOpWiDweSou3g', user: { @@ -537,6 +1023,58 @@ export const mockEndgameCreationEvent: Ecs = { }, }; +export const mockEndpointProcessStartEvent: Ecs = { + process: { + hash: { + md5: ['1b0e9b5fcb62de0787235ecca560b610'], + sha256: ['697334c236cce7d4c9e223146ee683a1219adced9729d4ae771fd6a1502a6b63'], + sha1: ['e19da2c35ba1c38adf12d1a472c1fcf1f1a811a7'], + }, + name: ['conhost.exe'], + pid: [3636], + entity_id: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTM2MzYtMTMyNTYwODU1OTguMTA3NTA3MDAw', + ], + executable: ['C:\\Windows\\System32\\conhost.exe'], + args: ['C:\\Windows\\system32\\conhost.exe,0xffffffff,-ForceV1'], + }, + host: { + os: { + full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1697)'], + name: ['Windows'], + version: ['1809 (10.0.17763.1697)'], + family: ['windows'], + kernel: ['1809 (10.0.17763.1697)'], + platform: ['windows'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + name: ['win2019-endpoint-1'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['d8ad572e-d224-4044-a57d-f5a84c0dfe5d'], + }, + event: { + category: ['process'], + kind: ['event'], + created: ['2021-01-25T21:59:58.107Z'], + module: ['endpoint'], + action: ['start'], + type: ['start'], + id: ['LzzWB9jjGmCwGMvk++++6Kw+'], + dataset: ['endpoint.events.process'], + }, + agent: { + type: ['endpoint'], + }, + user: { + name: ['SYSTEM'], + domain: ['NT AUTHORITY'], + }, + message: ['Endpoint process event'], + timestamp: '2021-01-25T21:59:58.107Z', + _id: 't5KSO3cB8l64wN2iQ8V9', +}; + export const mockEndgameTerminationEvent: Ecs = { _id: '2MjPcG0BOpWiDweSoutC', user: { @@ -578,3 +1116,59 @@ export const mockEndgameTerminationEvent: Ecs = { exit_code: [0], }, }; + +export const mockEndpointProcessEndEvent: Ecs = { + process: { + hash: { + md5: ['8a0a29438052faed8a2532da50455756'], + sha256: ['7fd065bac18c5278777ae44908101cdfed72d26fa741367f0ad4d02020787ab6'], + sha1: ['a1385ce20ad79f55df235effd9780c31442aa234'], + }, + name: ['svchost.exe'], + parent: { + name: ['services.exe'], + }, + pid: [10392], + entity_id: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTEwMzkyLTEzMjU2MjY2OTkwLjcwMzgzMDgwMA==', + ], + executable: ['C:\\Windows\\System32\\svchost.exe'], + exit_code: [-1], + args: ['C:\\Windows\\System32\\svchost.exe,-k,netsvcs,-p,-s,NetSetupSvc'], + }, + host: { + os: { + full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1697)'], + name: ['Windows'], + version: ['1809 (10.0.17763.1697)'], + family: ['windows'], + kernel: ['1809 (10.0.17763.1697)'], + platform: ['windows'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + name: ['win2019-endpoint'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['d8ad572e-d224-4044-a57d-f5a84c0dfe5d'], + }, + event: { + category: ['process'], + kind: ['event'], + created: ['2021-01-28T00:24:05.929Z'], + module: ['endpoint'], + action: ['end'], + type: ['end'], + id: ['LzzWB9jjGmCwGMvk++++77mE'], + dataset: ['endpoint.events.process'], + }, + agent: { + type: ['endpoint'], + }, + user: { + name: ['SYSTEM'], + domain: ['NT AUTHORITY'], + }, + message: ['Endpoint process event'], + timestamp: '2021-01-28T00:24:05.929Z', + _id: 'quloRncBX5UUcOOYo2ZS', +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/process_draggable.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/process_draggable.test.tsx.snap index 494a2b2b7732b..84aea591337ee 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/process_draggable.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/process_draggable.test.tsx.snap @@ -1,20 +1,31 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ProcessDraggable rendering it renders against shallow snapshot 1`] = ` -
    - - -
    + + + + + + + + `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx index 819c77343fc14..515db45e9fcd4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx @@ -42,6 +42,7 @@ export const EndgameSecurityEventDetails = React.memo(({ data, contextId, const endgameTargetUserName: string | null | undefined = get('endgame.target_user_name[0]', data); const eventAction: string | null | undefined = get('event.action[0]', data); const eventCode: string | null | undefined = get('event.code[0]', data); + const eventOutcome: string | null | undefined = get('event.outcome[0]', data); const hostName: string | null | undefined = get('host.name[0]', data); const id = data._id; const processExecutable: string | null | undefined = get('process.executable[0]', data); @@ -64,6 +65,7 @@ export const EndgameSecurityEventDetails = React.memo(({ data, contextId, endgameTargetUserName={endgameTargetUserName} eventAction={eventAction} eventCode={eventCode} + eventOutcome={eventOutcome} hostName={hostName} id={id} processExecutable={processExecutable} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx index a502180edfcf1..5d08898789821 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx @@ -39,6 +39,7 @@ describe('EndgameSecurityEventDetailsLine', () => { endgameTargetUserName="[endgameTargetUserName]" eventAction="admin_logon" eventCode="[eventCode]" + eventOutcome={undefined} hostName="[hostName]" id="1" processExecutable="[processExecutable]" @@ -69,6 +70,7 @@ describe('EndgameSecurityEventDetailsLine', () => { endgameTargetUserName="[endgameTargetUserName]" eventAction="explicit_user_logon" eventCode="[eventCode]" + eventOutcome={undefined} hostName="[hostName]" id="1" processExecutable="[processExecutable]" @@ -99,6 +101,7 @@ describe('EndgameSecurityEventDetailsLine', () => { endgameTargetUserName="[endgameTargetUserName]" eventAction="explicit_user_logon" eventCode="[eventCode]" + eventOutcome={undefined} hostName="[hostName]" id="1" processExecutable="[processExecutable]" @@ -129,6 +132,7 @@ describe('EndgameSecurityEventDetailsLine', () => { endgameTargetUserName="[endgameTargetUserName]" eventAction="explicit_user_logon" eventCode="[eventCode]" + eventOutcome={undefined} hostName="[hostName]" id="1" processExecutable="[processExecutable]" @@ -159,6 +163,7 @@ describe('EndgameSecurityEventDetailsLine', () => { endgameTargetUserName="[endgameTargetUserName]" eventAction="explicit_user_logon" eventCode="[eventCode]" + eventOutcome={undefined} hostName="[hostName]" id="1" processExecutable="[processExecutable]" @@ -189,6 +194,7 @@ describe('EndgameSecurityEventDetailsLine', () => { endgameTargetUserName="[endgameTargetUserName]" eventAction="explicit_user_logon" eventCode="[eventCode]" + eventOutcome={undefined} hostName="[hostName]" id="1" processExecutable="[processExecutable]" @@ -219,6 +225,7 @@ describe('EndgameSecurityEventDetailsLine', () => { endgameTargetUserName="[endgameTargetUserName]" eventAction="explicit_user_logon" eventCode="[eventCode]" + eventOutcome={undefined} hostName="[hostName]" id="1" processExecutable="[processExecutable]" @@ -249,6 +256,7 @@ describe('EndgameSecurityEventDetailsLine', () => { endgameTargetUserName="[endgameTargetUserName]" eventAction="explicit_user_logon" eventCode="[eventCode]" + eventOutcome={undefined} hostName="[hostName]" id="1" processExecutable="[processExecutable]" @@ -279,6 +287,7 @@ describe('EndgameSecurityEventDetailsLine', () => { endgameTargetUserName={undefined} eventAction="explicit_user_logon" eventCode="[eventCode]" + eventOutcome={undefined} hostName="[hostName]" id="1" processExecutable="[processExecutable]" @@ -309,6 +318,7 @@ describe('EndgameSecurityEventDetailsLine', () => { endgameTargetUserName="[endgameTargetUserName]" eventAction={undefined} eventCode="[eventCode]" + eventOutcome={undefined} hostName="[hostName]" id="1" processExecutable="[processExecutable]" @@ -339,6 +349,7 @@ describe('EndgameSecurityEventDetailsLine', () => { endgameTargetUserName="[endgameTargetUserName]" eventAction="explicit_user_logon" eventCode={undefined} + eventOutcome={undefined} hostName="[hostName]" id="1" processExecutable="[processExecutable]" @@ -369,6 +380,7 @@ describe('EndgameSecurityEventDetailsLine', () => { endgameTargetUserName="[endgameTargetUserName]" eventAction="explicit_user_logon" eventCode="[eventCode]" + eventOutcome={undefined} hostName={undefined} id="1" processExecutable="[processExecutable]" @@ -399,6 +411,7 @@ describe('EndgameSecurityEventDetailsLine', () => { endgameTargetUserName="[endgameTargetUserName]" eventAction="explicit_user_logon" eventCode="[eventCode]" + eventOutcome={undefined} hostName="[hostName]" id="1" processExecutable={undefined} @@ -429,6 +442,7 @@ describe('EndgameSecurityEventDetailsLine', () => { endgameTargetUserName="[endgameTargetUserName]" eventAction="explicit_user_logon" eventCode="[eventCode]" + eventOutcome={undefined} hostName="[hostName]" id="1" processExecutable="[processExecutable]" @@ -459,6 +473,7 @@ describe('EndgameSecurityEventDetailsLine', () => { endgameTargetUserName="[endgameTargetUserName]" eventAction="explicit_user_logon" eventCode="[eventCode]" + eventOutcome={undefined} hostName="[hostName]" id="1" processExecutable="[processExecutable]" @@ -489,6 +504,7 @@ describe('EndgameSecurityEventDetailsLine', () => { endgameTargetUserName="[endgameTargetUserName]" eventAction="admin_logon" eventCode="[eventCode]" + eventOutcome={undefined} hostName="[hostName]" id="1" processExecutable="[processExecutable]" @@ -519,6 +535,7 @@ describe('EndgameSecurityEventDetailsLine', () => { endgameTargetUserName="[endgameTargetUserName]" eventAction="admin_logon" eventCode="[eventCode]" + eventOutcome={undefined} hostName="[hostName]" id="1" processExecutable="[processExecutable]" @@ -549,6 +566,7 @@ describe('EndgameSecurityEventDetailsLine', () => { endgameTargetUserName="[endgameTargetUserName]" eventAction="admin_logon" eventCode="[eventCode]" + eventOutcome={undefined} hostName="[hostName]" id="1" processExecutable="[processExecutable]" @@ -579,6 +597,7 @@ describe('EndgameSecurityEventDetailsLine', () => { endgameTargetUserName="[endgameTargetUserName]" eventAction="admin_logon" eventCode={undefined} + eventOutcome={undefined} hostName="[hostName]" id="1" processExecutable="[processExecutable]" diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx index 9d3e74435852a..aba6f7346271d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx @@ -35,6 +35,7 @@ interface Props { endgameTargetUserName: string | null | undefined; eventAction: string | null | undefined; eventCode: string | null | undefined; + eventOutcome: string | null | undefined; hostName: string | null | undefined; id: string; processExecutable: string | null | undefined; @@ -57,6 +58,7 @@ export const EndgameSecurityEventDetailsLine = React.memo( endgameTargetUserName, eventAction, eventCode, + eventOutcome, hostName, id, processExecutable, @@ -67,7 +69,7 @@ export const EndgameSecurityEventDetailsLine = React.memo( winlogEventId, }) => { const domain = getTargetUserAndTargetDomain(eventAction) ? endgameTargetDomainName : userDomain; - const eventDetails = getEventDetails(eventAction); + const eventDetails = getEventDetails({ eventAction, eventOutcome }); const hostNameSeparator = getHostNameSeparator(eventAction); const user = getTargetUserAndTargetDomain(eventAction) ? endgameTargetUserName : userName; const userDomainField = getUserDomainField(eventAction); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/helpers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/helpers.test.tsx index a8955ccf22fec..5efc1e0b15673 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/helpers.test.tsx @@ -182,28 +182,116 @@ describe('helpers', () => { }); describe('#getEventDetails', () => { - test('it returns successfully logged in when eventAction is undefined', () => { - expect(getEventDetails(undefined)).toEqual('successfully logged in'); + test('it returns an empty string when eventAction is "explicit_user_logon"', () => { + expect( + getEventDetails({ eventAction: 'explicit_user_logon', eventOutcome: undefined }) + ).toEqual(''); }); - test('it returns successfully logged in when eventAction is null', () => { - expect(getEventDetails(null)).toEqual('successfully logged in'); + test('it returns logged off when eventAction is "log_off" and eventOutcome is null', () => { + expect(getEventDetails({ eventAction: 'log_off', eventOutcome: null })).toEqual('logged off'); }); - test('it returns successfully logged in when eventAction is an empty string', () => { - expect(getEventDetails('')).toEqual('successfully logged in'); + test('it returns logged off when eventAction is "log_off" and eventOutcome is undefined', () => { + expect(getEventDetails({ eventAction: 'log_off', eventOutcome: undefined })).toEqual( + 'logged off' + ); }); - test('it returns successfully logged in when eventAction is a random value', () => { - expect(getEventDetails('a random value')).toEqual('successfully logged in'); + test('it returns failed to log off when eventAction is "log_off" and eventOutcome is failure', () => { + expect(getEventDetails({ eventAction: 'log_off', eventOutcome: 'failure' })).toEqual( + 'failed to log off' + ); }); - test('it returns an empty string when eventAction is "explicit_user_logon"', () => { - expect(getEventDetails('explicit_user_logon')).toEqual(''); + test('it returns failed to log off when eventAction is "log_off" and eventOutcome is fAiLuRe', () => { + expect(getEventDetails({ eventAction: 'log_off', eventOutcome: 'fAiLuRe' })).toEqual( + 'failed to log off' + ); + }); + + test('it returns logged off when eventAction is "log_off" and eventOutcome is anything_else', () => { + expect(getEventDetails({ eventAction: 'log_off', eventOutcome: 'anything_else' })).toEqual( + 'logged off' + ); + }); + + test('it returns logged off when eventAction is "user_logoff" and eventOutcome is null', () => { + expect(getEventDetails({ eventAction: 'user_logoff', eventOutcome: null })).toEqual( + 'logged off' + ); + }); + + test('it returns logged off when eventAction is "user_logoff" and eventOutcome is undefined', () => { + expect(getEventDetails({ eventAction: 'user_logoff', eventOutcome: undefined })).toEqual( + 'logged off' + ); + }); + + test('it returns failed to log off when eventAction is "user_logoff" and eventOutcome is failure', () => { + expect(getEventDetails({ eventAction: 'user_logoff', eventOutcome: 'failure' })).toEqual( + 'failed to log off' + ); + }); + + test('it returns failed to log off when eventAction is "user_logoff" and eventOutcome is fAiLuRe', () => { + expect(getEventDetails({ eventAction: 'user_logoff', eventOutcome: 'fAiLuRe' })).toEqual( + 'failed to log off' + ); + }); + + test('it returns logged off when eventAction is "user_logoff" and eventOutcome is anything_else', () => { + expect( + getEventDetails({ eventAction: 'user_logoff', eventOutcome: 'anything_else' }) + ).toEqual('logged off'); + }); + + test('it returns successfully logged in when eventAction is null and eventOutcome is undefined', () => { + expect(getEventDetails({ eventAction: null, eventOutcome: undefined })).toEqual( + 'successfully logged in' + ); + }); + + test('it returns successfully logged in when eventAction is null and eventOutcome is null', () => { + expect(getEventDetails({ eventAction: null, eventOutcome: null })).toEqual( + 'successfully logged in' + ); + }); + + test('it returns successfully logged in when eventAction is undefined and eventOutcome is null', () => { + expect(getEventDetails({ eventAction: undefined, eventOutcome: null })).toEqual( + 'successfully logged in' + ); + }); + + test('it returns successfully logged in when eventAction is undefined and eventOutcome is undefined', () => { + expect(getEventDetails({ eventAction: undefined, eventOutcome: undefined })).toEqual( + 'successfully logged in' + ); + }); + + test('it returns successfully logged in when eventAction is anything_else and eventOutcome is undefined', () => { + expect(getEventDetails({ eventAction: 'anything_else', eventOutcome: undefined })).toEqual( + 'successfully logged in' + ); + }); + + test('it returns successfully logged in when eventAction is anything_else and eventOutcome is null', () => { + expect(getEventDetails({ eventAction: 'anything_else', eventOutcome: null })).toEqual( + 'successfully logged in' + ); + }); + + test('it returns failed to log in when eventAction is anything_else and eventOutcome is failure', () => { + expect(getEventDetails({ eventAction: 'anything_else', eventOutcome: 'failure' })).toEqual( + 'failed to log in' + ); }); - test('it returns logged off when eventAction is "user_logoff"', () => { - expect(getEventDetails('user_logoff')).toEqual('logged off'); + test('it returns failed to log in when eventAction is anything_else and eventOutcome is fAiLuRe', () => { + expect(getEventDetails({ eventAction: 'anything_else', eventOutcome: 'fAiLuRe' })).toEqual( + 'failed to log in' + ); }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/helpers.ts index 86785c3986270..87c0ed2782f9d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/helpers.ts @@ -50,13 +50,22 @@ export const getUserDomainField = (eventAction: string | null | undefined): stri export const getUserNameField = (eventAction: string | null | undefined): string => getTargetUserAndTargetDomain(eventAction) ? 'endgame.target_user_name' : 'user.name'; -export const getEventDetails = (eventAction: string | null | undefined): string => { +export const getEventDetails = ({ + eventAction, + eventOutcome, +}: { + eventAction: string | null | undefined; + eventOutcome: string | null | undefined; +}): string => { switch (eventAction) { case 'explicit_user_logon': return ''; // no details + case 'log_off': // fall through case 'user_logoff': - return i18n.LOGGED_OFF; + return eventOutcome?.toLowerCase() === 'failure' ? i18n.FAILED_TO_LOG_OFF : i18n.LOGGED_OFF; default: - return i18n.SUCCESSFULLY_LOGGED_IN; + return eventOutcome?.toLowerCase() === 'failure' + ? i18n.FAILED_TO_LOG_IN + : i18n.SUCCESSFULLY_LOGGED_IN; } }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/translations.ts index e7dfefb2b570c..859fc8ead332a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/translations.ts @@ -21,6 +21,20 @@ export const AS_REQUESTED_BY_SUBJECT = i18n.translate( } ); +export const FAILED_TO_LOG_IN = i18n.translate( + 'xpack.securitySolution.timeline.body.renderers.endpoint.failedToLogInDescription', + { + defaultMessage: 'failed to log in', + } +); + +export const FAILED_TO_LOG_OFF = i18n.translate( + 'xpack.securitySolution.timeline.body.renderers.endpoint.failedToLogOffDescription', + { + defaultMessage: 'failed to log off', + } +); + export const LOGGED_OFF = i18n.translate( 'xpack.securitySolution.timeline.body.renderers.endgame.loggedOffDescription', { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.test.tsx index 2d502f1195995..a6f15a9f79f4e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.test.tsx @@ -25,22 +25,29 @@ jest.mock('@elastic/eui', () => { describe('ExitCodeDraggable', () => { const mount = useMountAppended(); - test('it renders the expected text and exit code, when both text and an endgameExitCode are provided', () => { + test('it renders the expected text and exit codes, when text, processExitCode, and an endgameExitCode are provided', () => { const wrapper = mount( - + ); - expect(wrapper.text()).toEqual('with exit code0'); + expect(wrapper.text()).toEqual('with exit code-10'); }); - test('it returns an empty string when text is provided, but endgameExitCode is undefined', () => { + test('it returns an empty string when text is provided, but processExitCode and endgameExitCode are undefined', () => { const wrapper = mount( @@ -48,13 +55,14 @@ describe('ExitCodeDraggable', () => { expect(wrapper.text()).toEqual(''); }); - test('it returns an empty string when text is provided, but endgameExitCode is null', () => { + test('it returns an empty string when text is provided, but processExitCode and endgameExitCode are null', () => { const wrapper = mount( @@ -65,36 +73,105 @@ describe('ExitCodeDraggable', () => { test('it returns an empty string when text is provided, but endgameExitCode is an empty string', () => { const wrapper = mount( - + ); expect(wrapper.text()).toEqual(''); }); - test('it renders just the exit code when text is undefined', () => { + test('it renders just the endgameExitCode code when text is undefined', () => { const wrapper = mount( - + ); expect(wrapper.text()).toEqual('1'); }); - test('it renders just the exit code when text is null', () => { + test('it renders just the processExitCode code when text is undefined', () => { const wrapper = mount( - + + + ); + expect(wrapper.text()).toEqual('-1'); + }); + + test('it renders just the endgameExitCode code when text is null', () => { + const wrapper = mount( + + ); expect(wrapper.text()).toEqual('1'); }); - test('it renders just the exit code when text is an empty string', () => { + test('it renders just the processExitCode code when text is null', () => { const wrapper = mount( - + + + ); + expect(wrapper.text()).toEqual('-1'); + }); + + test('it renders just the endgameExitCode code when text is an empty string', () => { + const wrapper = mount( + + ); expect(wrapper.text()).toEqual('1'); }); + + test('it renders just the processExitCode code when text is an empty string', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual('-1'); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.tsx index 7d680aeb2ea76..7ac9fe290893f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.tsx @@ -15,12 +15,13 @@ interface Props { contextId: string; endgameExitCode: string | null | undefined; eventId: string; + processExitCode: number | null | undefined; text: string | null | undefined; } export const ExitCodeDraggable = React.memo( - ({ contextId, endgameExitCode, eventId, text }) => { - if (isNillEmptyOrNotFinite(endgameExitCode)) { + ({ contextId, endgameExitCode, eventId, processExitCode, text }) => { + if (isNillEmptyOrNotFinite(processExitCode) && isNillEmptyOrNotFinite(endgameExitCode)) { return null; } @@ -32,14 +33,27 @@ export const ExitCodeDraggable = React.memo( )} - - - + {!isNillEmptyOrNotFinite(processExitCode) && ( + + + + )} + + {!isNillEmptyOrNotFinite(endgameExitCode) && ( + + + + )} ); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/helpers.tsx index 1dfff526dcce6..ea84dc19908f0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/helpers.tsx @@ -51,14 +51,15 @@ export const isFileEvent = ({ eventCategory: string | null | undefined; eventDataset: string | null | undefined; }) => - (eventCategory != null && eventCategory.toLowerCase() === 'file') || - (eventDataset != null && eventDataset.toLowerCase() === 'file'); + eventCategory?.toLowerCase() === 'file' || + eventDataset?.toLowerCase() === 'file' || + eventDataset?.toLowerCase() === 'endpoint.events.file'; export const isProcessStoppedOrTerminationEvent = ( eventAction: string | null | undefined ): boolean => ['process_stopped', 'termination_event'].includes(`${eventAction}`.toLowerCase()); export const showVia = (eventAction: string | null | undefined): boolean => - ['file_create_event', 'created', 'file_delete_event', 'deleted'].includes( + ['file_create_event', 'created', 'creation', 'file_delete_event', 'deleted', 'deletion'].includes( `${eventAction}`.toLowerCase() ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/parent_process_draggable.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/parent_process_draggable.test.tsx index 19fd5eee0e230..2402be88dea18 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/parent_process_draggable.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/parent_process_draggable.test.tsx @@ -25,28 +25,34 @@ jest.mock('@elastic/eui', () => { describe('ParentProcessDraggable', () => { const mount = useMountAppended(); - test('displays the text, endgameParentProcessName, and processPpid when they are all provided', () => { + test('displays the text, endgameParentProcessName, processParentName, processParentPid, and processPpid when they are all provided', () => { const wrapper = mount( ); - expect(wrapper.text()).toEqual('via parent process[endgameParentProcessName](456)'); + expect(wrapper.text()).toEqual( + 'via parent process[processParentName][endgameParentProcessName](789)(456)' + ); }); - test('displays nothing when the text is provided, but endgameParentProcessName and processPpid are both undefined', () => { + test('displays nothing when the text is provided, but endgameParentProcessName and processParentName are both undefined', () => { const wrapper = mount( @@ -55,63 +61,71 @@ describe('ParentProcessDraggable', () => { expect(wrapper.text()).toEqual(''); }); - test('displays the text and processPpid when endgameParentProcessName is undefined', () => { + test('displays the text and endgameParentProcessName when processPpid is undefined', () => { const wrapper = mount( ); - expect(wrapper.text()).toEqual('via parent process(456)'); + expect(wrapper.text()).toEqual('via parent process[endgameParentProcessName]'); }); - test('displays the processPpid when both endgameParentProcessName and text are undefined', () => { + test('displays the text and processParentName when processParentPid is undefined', () => { const wrapper = mount( ); - expect(wrapper.text()).toEqual('(456)'); + expect(wrapper.text()).toEqual('via parent process[processParentName]'); }); - test('displays the text and endgameParentProcessName when processPpid is undefined', () => { + test('displays the endgameParentProcessName when both processPpid and text are undefined', () => { const wrapper = mount( ); - expect(wrapper.text()).toEqual('via parent process[endgameParentProcessName]'); + expect(wrapper.text()).toEqual('[endgameParentProcessName]'); }); - test('displays the endgameParentProcessName when both processPpid and text are undefined', () => { + test('displays the processParentName when both processParentPid and text are undefined', () => { const wrapper = mount( ); - expect(wrapper.text()).toEqual('[endgameParentProcessName]'); + expect(wrapper.text()).toEqual('[processParentName]'); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/parent_process_draggable.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/parent_process_draggable.tsx index 816b2c8ddae78..f0a63404feeb7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/parent_process_draggable.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/parent_process_draggable.tsx @@ -15,13 +15,26 @@ interface Props { contextId: string; endgameParentProcessName: string | null | undefined; eventId: string; + processParentPid: number | null | undefined; + processParentName: string | null | undefined; processPpid: number | undefined | null; text: string | null | undefined; } export const ParentProcessDraggable = React.memo( - ({ contextId, endgameParentProcessName, eventId, processPpid, text }) => { - if (isNillEmptyOrNotFinite(endgameParentProcessName) && isNillEmptyOrNotFinite(processPpid)) { + ({ + contextId, + endgameParentProcessName, + eventId, + processParentName, + processParentPid, + processPpid, + text, + }) => { + if ( + isNillEmptyOrNotFinite(processParentName) && + isNillEmptyOrNotFinite(endgameParentProcessName) + ) { return null; } @@ -37,6 +50,17 @@ export const ParentProcessDraggable = React.memo( )} + {!isNillEmptyOrNotFinite(processParentName) && ( + + + + )} + {!isNillEmptyOrNotFinite(endgameParentProcessName) && ( ( )} + {!isNillEmptyOrNotFinite(processParentPid) && ( + + + + )} + {!isNillEmptyOrNotFinite(processPpid) && ( ( } return ( -
    + {!isNillEmptyOrNotFinite(processName) ? ( - + + + ) : !isNillEmptyOrNotFinite(processExecutable) ? ( - + + + ) : !isNillEmptyOrNotFinite(endgameProcessName) ? ( - + + + ) : null} {!isNillEmptyOrNotFinite(processPid) ? ( - + + + ) : !isNillEmptyOrNotFinite(endgamePid) ? ( - + + + ) : null} -
    + ); } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx index 536de70a712a8..a3932fde44c1d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx @@ -116,10 +116,13 @@ describe('SystemGenericFileDetails', () => { packageName="[packageName-123]" packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" - processExecutable="[packageExecutable=123]" + processExecutable="[processExecutable=123]" + processExitCode={-1} processHashMd5="[processHashMd5-123]" processHashSha1="[processHashSha1-123]" processHashSha256="[processHashSha256-123]" + processParentName="[processParentName-123]" + processParentPid={789} processPid={123} processPpid={456} processName="[processName-123]" @@ -135,7 +138,7 @@ describe('SystemGenericFileDetails', () => {
    ); expect(wrapper.text()).toEqual( - '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][generic-text-123][fileName-123]in[filePath-123][processName-123](123)[arg-1][arg-2][arg-3][some-title-123]with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][generic-text-123][fileName-123]in[filePath-123][processName-123](123)[arg-1][arg-2][arg-3][some-title-123]with exit code-1[endgameExitCode-123]via parent process[processParentName-123][endgameParentProcessName-123](789)(456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' ); }); @@ -162,9 +165,12 @@ describe('SystemGenericFileDetails', () => { packageSummary={null} packageVersion={null} processExecutable={null} + processExitCode={null} processHashMd5={null} processHashSha1={null} processHashSha256={null} + processParentName={null} + processParentPid={null} processPid={null} processPpid={null} processName={null} @@ -207,9 +213,12 @@ describe('SystemGenericFileDetails', () => { packageSummary={null} packageVersion={null} processExecutable={null} + processExitCode={null} processHashMd5={null} processHashSha1={null} processHashSha256={null} + processParentName={null} + processParentPid={null} processPid={null} processPpid={null} processName={null} @@ -252,9 +261,12 @@ describe('SystemGenericFileDetails', () => { packageSummary={null} packageVersion={null} processExecutable={null} + processExitCode={null} processHashMd5={null} processHashSha1={null} processHashSha256={null} + processParentName={null} + processParentPid={null} processPid={null} processPpid={null} processName={null} @@ -297,9 +309,12 @@ describe('SystemGenericFileDetails', () => { packageSummary={null} packageVersion={null} processExecutable={null} + processExitCode={null} processHashMd5={null} processHashSha1={null} processHashSha256={null} + processParentName={null} + processParentPid={null} processPid={null} processPpid={null} processName={null} @@ -344,9 +359,12 @@ describe('SystemGenericFileDetails', () => { packageSummary={null} packageVersion={null} processExecutable={null} + processExitCode={null} processHashMd5={null} processHashSha1={null} processHashSha256={null} + processParentName={null} + processParentPid={null} processPid={null} processPpid={null} processName={null} @@ -391,9 +409,12 @@ describe('SystemGenericFileDetails', () => { packageSummary="[packageSummary-123]" packageVersion={null} processExecutable={null} + processExitCode={null} processHashMd5={null} processHashSha1={null} processHashSha256={null} + processParentName={null} + processParentPid={null} processPid={null} processPpid={null} processName={null} @@ -438,9 +459,12 @@ describe('SystemGenericFileDetails', () => { packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" processExecutable={null} + processExitCode={null} processHashMd5={null} processHashSha1={null} processHashSha256={null} + processParentName={null} + processParentPid={null} processPid={null} processPpid={null} processName={null} @@ -462,7 +486,7 @@ describe('SystemGenericFileDetails', () => { ); }); - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable', () => { + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processExitCode', () => { const wrapper = mount(
    @@ -484,10 +508,13 @@ describe('SystemGenericFileDetails', () => { packageName="[packageName-123]" packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" - processExecutable="[packageVersion-123]" + processExecutable="[processExecutable-123]" + processExitCode={-1} processHashMd5={null} processHashSha1={null} processHashSha256={null} + processParentName={null} + processParentPid={null} processPid={null} processPpid={null} processName={null} @@ -505,11 +532,11 @@ describe('SystemGenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - '[hostname-123][packageVersion-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + '[hostname-123][processExecutable-123]with exit code-1with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' ); }); - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5', () => { + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, processExecutable, processExitCode, processHashMd5', () => { const wrapper = mount(
    @@ -531,10 +558,13 @@ describe('SystemGenericFileDetails', () => { packageName="[packageName-123]" packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" - processExecutable="[packageVersion-123]" + processExecutable="[processExecutable-123]" + processExitCode={-1} processHashMd5="[processHashMd5-123]" processHashSha1={null} processHashSha256={null} + processParentName={null} + processParentPid={null} processPid={null} processPpid={null} processName={null} @@ -552,11 +582,11 @@ describe('SystemGenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - '[hostname-123][packageVersion-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashMd5-123][message-123]' + '[hostname-123][processExecutable-123]with exit code-1with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashMd5-123][message-123]' ); }); - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1', () => { + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, processExecutable, processExitCode, processHashMd5, processHashSha1', () => { const wrapper = mount(
    @@ -578,10 +608,63 @@ describe('SystemGenericFileDetails', () => { packageName="[packageName-123]" packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" - processExecutable="[packageVersion-123]" + processExecutable="[processExecutable-123]" + processExitCode={-1} processHashMd5="[processHashMd5-123]" processHashSha1="[processHashSha1-123]" processHashSha256={null} + processParentName={null} + processParentPid={null} + processPid={null} + processPpid={null} + processName={null} + showMessage={true} + sshMethod={null} + sshSignature={null} + text={null} + userDomain={null} + userName={null} + workingDirectory={null} + processTitle={null} + args={null} + /> +
    +
    + ); + expect(wrapper.text()).toEqual( + '[hostname-123][processExecutable-123]with exit code-1with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha1-123][processHashMd5-123][message-123]' + ); + }); + + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, processExecutable, processExitCode, processHashMd5, processHashSha1, processHashSha256', () => { + const wrapper = mount( + +
    + { ); expect(wrapper.text()).toEqual( - '[hostname-123][packageVersion-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha1-123][processHashMd5-123][message-123]' + '[hostname-123][processExecutable-123]with exit code-1with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' ); }); - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256', () => { + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, processExecutable, processExitCode, processHashMd5, processHashSha1, processHashSha256, processParentName', () => { const wrapper = mount(
    @@ -625,10 +708,13 @@ describe('SystemGenericFileDetails', () => { packageName="[packageName-123]" packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" - processExecutable="[packageVersion-123]" + processExecutable="[processExecutable-123]" + processExitCode={-1} processHashMd5="[processHashMd5-123]" processHashSha1="[processHashSha1-123]" processHashSha256="[processHashSha256-123]" + processParentName="[processParentName-123]" + processParentPid={null} processPid={null} processPpid={null} processName={null} @@ -646,11 +732,11 @@ describe('SystemGenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - '[hostname-123][packageVersion-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + '[hostname-123][processExecutable-123]with exit code-1via parent process[processParentName-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' ); }); - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid', () => { + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, processExecutable, processExitCode, processHashMd5, processHashSha1, processHashSha256, processParentName, processParentPid', () => { const wrapper = mount(
    @@ -673,9 +759,62 @@ describe('SystemGenericFileDetails', () => { packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" processExecutable="[processExecutable-123]" + processExitCode={-1} processHashMd5="[processHashMd5-123]" processHashSha1="[processHashSha1-123]" processHashSha256="[processHashSha256-123]" + processParentName="[processParentName-123]" + processParentPid={789} + processPid={null} + processPpid={null} + processName={null} + showMessage={true} + sshMethod={null} + sshSignature={null} + text={null} + userDomain={null} + userName={null} + workingDirectory={null} + processTitle={null} + args={null} + /> +
    +
    + ); + expect(wrapper.text()).toEqual( + '[hostname-123][processExecutable-123]with exit code-1via parent process[processParentName-123](789)with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + ); + }); + + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, processExecutable, processExitCode, processHashMd5, processHashSha1, processHashSha256, processParentName, processParentPid, processPid', () => { + const wrapper = mount( + +
    + { ); expect(wrapper.text()).toEqual( - '[hostname-123][processExecutable-123](123)with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + '[hostname-123][processExecutable-123](123)with exit code-1via parent process[processParentName-123](789)with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' ); }); - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName', () => { + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, processExecutable, processExitCode, processHashMd5, processHashSha1, processHashSha256, processParentName, processParentPid, processPid, processPpid, processName', () => { const wrapper = mount(
    @@ -719,10 +858,13 @@ describe('SystemGenericFileDetails', () => { packageName="[packageName-123]" packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" - processExecutable="[packageVersion-123]" + processExecutable="[processExecutable-123]" + processExitCode={-1} processHashMd5="[processHashMd5-123]" processHashSha1="[processHashSha1-123]" processHashSha256="[processHashSha256-123]" + processParentName="[processParentName-123]" + processParentPid={789} processPid={123} processPpid={456} processName="[processName-123]" @@ -740,11 +882,11 @@ describe('SystemGenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - '[hostname-123][processName-123](123)via parent process(456)with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + '[hostname-123][processName-123](123)with exit code-1via parent process[processParentName-123](789)(456)with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' ); }); - test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod', () => { + test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, processExecutable, processExitCode, processHashMd5, processHashSha1, processHashSha256, processParentName, processParentPid, processPid, processPpid, processName, sshMethod', () => { const wrapper = mount(
    @@ -766,10 +908,13 @@ describe('SystemGenericFileDetails', () => { packageName="[packageName-123]" packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" - processExecutable="[packageVersion-123]" + processExecutable="[processExecutable-123]" + processExitCode={-1} processHashMd5="[processHashMd5-123]" processHashSha1="[processHashSha1-123]" processHashSha256="[processHashSha256-123]" + processParentName="[processParentName-123]" + processParentPid={789} processPid={123} processPpid={456} processName="[processName-123]" @@ -787,11 +932,11 @@ describe('SystemGenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - '[hostname-123][processName-123](123)with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + '[hostname-123][processName-123](123)with exit code-1[endgameExitCode-123]via parent process[processParentName-123][endgameParentProcessName-123](789)(456)with result[outcome-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' ); }); - test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature', () => { + test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, processExecutable, processExitCode, processHashMd5, processHashSha1, processHashSha256, processParentName, processParentPid, processPid, processPpid, processName, sshMethod, sshSignature', () => { const wrapper = mount(
    @@ -813,10 +958,13 @@ describe('SystemGenericFileDetails', () => { packageName="[packageName-123]" packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" - processExecutable="[packageVersion-123]" + processExecutable="[processExecutable-123]" + processExitCode={-1} processHashMd5="[processHashMd5-123]" processHashSha1="[processHashSha1-123]" processHashSha256="[processHashSha256-123]" + processParentName="[processParentName-123]" + processParentPid={789} processPid={123} processPpid={456} processName="[processName-123]" @@ -834,11 +982,11 @@ describe('SystemGenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - '[hostname-123][processName-123](123)with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + '[hostname-123][processName-123](123)with exit code-1[endgameExitCode-123]via parent process[processParentName-123][endgameParentProcessName-123](789)(456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' ); }); - test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text', () => { + test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, processExecutable, processExitCode, processHashMd5, processHashSha1, processHashSha256, processParentName, processParentPid, processPid, processPpid, processName, sshMethod, sshSignature, text', () => { const wrapper = mount(
    @@ -860,10 +1008,13 @@ describe('SystemGenericFileDetails', () => { packageName="[packageName-123]" packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" - processExecutable="[packageVersion-123]" + processExecutable="[processExecutable-123]" + processExitCode={-1} processHashMd5="[processHashMd5-123]" processHashSha1="[processHashSha1-123]" processHashSha256="[processHashSha256-123]" + processParentName="[processParentName-123]" + processParentPid={789} processPid={123} processPpid={456} processName="[processName-123]" @@ -881,11 +1032,11 @@ describe('SystemGenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - '[hostname-123][text-123][processName-123](123)with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + '[hostname-123][text-123][processName-123](123)with exit code-1[endgameExitCode-123]via parent process[processParentName-123][endgameParentProcessName-123](789)(456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' ); }); - test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain', () => { + test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, processExecutable, processExitCode, processHashMd5, processHashSha1, processHashSha256, processParentName, processParentPid, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain', () => { const wrapper = mount(
    @@ -907,10 +1058,13 @@ describe('SystemGenericFileDetails', () => { packageName="[packageName-123]" packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" - processExecutable="[packageVersion-123]" + processExecutable="[processExecutable-123]" + processExitCode={-1} processHashMd5="[processHashMd5-123]" processHashSha1="[processHashSha1-123]" processHashSha256="[processHashSha256-123]" + processParentName="[processParentName-123]" + processParentPid={789} processPid={123} processPpid={456} processName="[processName-123]" @@ -928,11 +1082,11 @@ describe('SystemGenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - '\\[userDomain-123][hostname-123][text-123][processName-123](123)with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + '\\[userDomain-123][hostname-123][text-123][processName-123](123)with exit code-1[endgameExitCode-123]via parent process[processParentName-123][endgameParentProcessName-123](789)(456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' ); }); - test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username', () => { + test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, processExecutable, processExitCode, processHashMd5, processHashSha1, processHashSha256, processParentName, processParentPid, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username', () => { const wrapper = mount(
    @@ -954,10 +1108,13 @@ describe('SystemGenericFileDetails', () => { packageName="[packageName-123]" packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" - processExecutable="[packageVersion-123]" + processExecutable="[processExecutable-123]" + processExitCode={-1} processHashMd5="[processHashMd5-123]" processHashSha1="[processHashSha1-123]" processHashSha256="[processHashSha256-123]" + processParentName="[processParentName-123]" + processParentPid={789} processPid={123} processPpid={456} processName="[processName-123]" @@ -975,11 +1132,11 @@ describe('SystemGenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - '[username-123]\\[userDomain-123]@[hostname-123][text-123][processName-123](123)with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + '[username-123]\\[userDomain-123]@[hostname-123][text-123][processName-123](123)with exit code-1[endgameExitCode-123]via parent process[processParentName-123][endgameParentProcessName-123](789)(456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' ); }); - test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory', () => { + test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, processExecutable, processExitCode, processHashMd5, processHashSha1, processHashSha256, processParentName, processParentPid, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory', () => { const wrapper = mount(
    @@ -1001,10 +1158,13 @@ describe('SystemGenericFileDetails', () => { packageName="[packageName-123]" packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" - processExecutable="[packageVersion-123]" + processExecutable="[processExecutable-123]" + processExitCode={-1} processHashMd5="[processHashMd5-123]" processHashSha1="[processHashSha1-123]" processHashSha256="[processHashSha256-123]" + processParentName="[processParentName-123]" + processParentPid={789} processPid={123} processPpid={456} processName="[processName-123]" @@ -1022,11 +1182,11 @@ describe('SystemGenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][text-123][processName-123](123)with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][text-123][processName-123](123)with exit code-1[endgameExitCode-123]via parent process[processParentName-123][endgameParentProcessName-123](789)(456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' ); }); - test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory, process-title', () => { + test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, processExecutable, processExitCode, processHashMd5, processHashSha1, processHashSha256, processParentName, processParentPid, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory, process-title', () => { const wrapper = mount(
    @@ -1048,10 +1208,13 @@ describe('SystemGenericFileDetails', () => { packageName="[packageName-123]" packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" - processExecutable="[packageVersion-123]" + processExecutable="[processExecutable-123]" + processExitCode={-1} processHashMd5="[processHashMd5-123]" processHashSha1="[processHashSha1-123]" processHashSha256="[processHashSha256-123]" + processParentName="[processParentName-123]" + processParentPid={789} processPid={123} processPpid={456} processName="[processName-123]" @@ -1069,11 +1232,11 @@ describe('SystemGenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][text-123][processName-123](123)[process-title-123]with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][text-123][processName-123](123)[process-title-123]with exit code-1[endgameExitCode-123]via parent process[processParentName-123][endgameParentProcessName-123](789)(456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' ); }); - test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory, process-title, args', () => { + test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, processExecutable, processExitCode, processHashMd5, processHashSha1, processHashSha256, processParentName, processParentPid, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory, process-title, args', () => { const wrapper = mount(
    @@ -1095,10 +1258,13 @@ describe('SystemGenericFileDetails', () => { packageName="[packageName-123]" packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" - processExecutable="[packageVersion-123]" + processExecutable="[processExecutable-123]" + processExitCode={-1} processHashMd5="[processHashMd5-123]" processHashSha1="[processHashSha1-123]" processHashSha256="[processHashSha256-123]" + processParentName="[processParentName-123]" + processParentPid={789} processPid={123} processPpid={456} processName="[processName-123]" @@ -1116,7 +1282,7 @@ describe('SystemGenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][text-123][processName-123](123)[arg-1][arg-2][arg-3][process-title-123]with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][text-123][processName-123](123)[arg-1][arg-2][arg-3][process-title-123]with exit code-1[endgameExitCode-123]via parent process[processParentName-123][endgameParentProcessName-123](789)(456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' ); }); @@ -1143,9 +1309,12 @@ describe('SystemGenericFileDetails', () => { packageSummary={undefined} packageVersion={undefined} processExecutable={undefined} + processExitCode={undefined} processHashMd5={undefined} processHashSha1={undefined} processHashSha256={undefined} + processParentName={undefined} + processParentPid={undefined} processPid={undefined} processPpid={undefined} processName={undefined} @@ -1188,9 +1357,12 @@ describe('SystemGenericFileDetails', () => { packageSummary={undefined} packageVersion={undefined} processExecutable={undefined} + processExitCode={undefined} processHashMd5={undefined} processHashSha1={undefined} processHashSha256={undefined} + processParentName={undefined} + processParentPid={undefined} processPid={undefined} processPpid={undefined} processName={undefined} @@ -1235,9 +1407,12 @@ describe('SystemGenericFileDetails', () => { packageSummary={undefined} packageVersion={undefined} processExecutable={undefined} + processExitCode={undefined} processHashMd5={undefined} processHashSha1={undefined} processHashSha256={undefined} + processParentName={undefined} + processParentPid={undefined} processPid={undefined} processPpid={undefined} processName={undefined} @@ -1284,9 +1459,12 @@ describe('SystemGenericFileDetails', () => { packageSummary={undefined} packageVersion={undefined} processExecutable={undefined} + processExitCode={undefined} processHashMd5={undefined} processHashSha1={undefined} processHashSha256={undefined} + processParentName={undefined} + processParentPid={undefined} processPid={undefined} processPpid={undefined} processName={undefined} @@ -1332,9 +1510,12 @@ describe('SystemGenericFileDetails', () => { packageSummary={undefined} packageVersion={undefined} processExecutable={undefined} + processExitCode={undefined} processHashMd5={undefined} processHashSha1={undefined} processHashSha256={undefined} + processParentName={undefined} + processParentPid={undefined} processPid={undefined} processPpid={456} processName={undefined} @@ -1382,9 +1563,12 @@ describe('SystemGenericFileDetails', () => { packageSummary={undefined} packageVersion={undefined} processExecutable={undefined} + processExitCode={undefined} processHashMd5={undefined} processHashSha1={undefined} processHashSha256={undefined} + processParentName={undefined} + processParentPid={undefined} processPid={undefined} processPpid={456} processName={undefined} @@ -1430,9 +1614,12 @@ describe('SystemGenericFileDetails', () => { packageSummary={undefined} packageVersion={undefined} processExecutable={undefined} + processExitCode={undefined} processHashMd5={undefined} processHashSha1={undefined} processHashSha256={undefined} + processParentName={undefined} + processParentPid={undefined} processPid={undefined} processPpid={456} processName={undefined} @@ -1476,9 +1663,12 @@ describe('SystemGenericFileDetails', () => { packageSummary={undefined} packageVersion={undefined} processExecutable={undefined} + processExitCode={undefined} processHashMd5={undefined} processHashSha1={undefined} processHashSha256={undefined} + processParentName={undefined} + processParentPid={undefined} processPid={undefined} processPpid={undefined} processName={undefined} @@ -1522,9 +1712,12 @@ describe('SystemGenericFileDetails', () => { packageSummary={undefined} packageVersion={undefined} processExecutable={undefined} + processExitCode={undefined} processHashMd5={undefined} processHashSha1={undefined} processHashSha256={undefined} + processParentName={undefined} + processParentPid={undefined} processPid={undefined} processPpid={undefined} processName={undefined} @@ -1568,9 +1761,12 @@ describe('SystemGenericFileDetails', () => { packageSummary={undefined} packageVersion={undefined} processExecutable={undefined} + processExitCode={undefined} processHashMd5={undefined} processHashSha1={undefined} processHashSha256={undefined} + processParentName={undefined} + processParentPid={undefined} processPid={undefined} processPpid={undefined} processName={undefined} @@ -1614,9 +1810,12 @@ describe('SystemGenericFileDetails', () => { packageSummary={undefined} packageVersion={undefined} processExecutable={undefined} + processExitCode={undefined} processHashMd5={undefined} processHashSha1={undefined} processHashSha256={undefined} + processParentName={undefined} + processParentPid={undefined} processPid={123} processPpid={undefined} processName="[processName]" diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.tsx index e0e0743fb3043..4026613ff7d0a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.tsx @@ -48,6 +48,9 @@ interface Props { packageSummary: string | null | undefined; packageVersion: string | null | undefined; processName: string | null | undefined; + processParentName: string | null | undefined; + processParentPid: number | null | undefined; + processExitCode: number | null | undefined; processPid: number | null | undefined; processPpid: number | null | undefined; processExecutable: string | null | undefined; @@ -82,6 +85,9 @@ export const SystemGenericFileLine = React.memo( message, outcome, packageName, + processParentName, + processParentPid, + processExitCode, packageSummary, packageVersion, processExecutable, @@ -142,6 +148,7 @@ export const SystemGenericFileLine = React.memo( contextId={contextId} endgameExitCode={endgameExitCode} eventId={id} + processExitCode={processExitCode} text={i18n.WITH_EXIT_CODE} /> {!isProcessStoppedOrTerminationEvent(eventAction) && ( @@ -149,6 +156,8 @@ export const SystemGenericFileLine = React.memo( contextId={contextId} endgameParentProcessName={endgameParentProcessName} eventId={id} + processParentName={processParentName} + processParentPid={processParentPid} processPpid={processPpid} text={i18n.VIA_PARENT_PROCESS} /> @@ -239,6 +248,9 @@ export const SystemGenericFileDetails = React.memo( const packageName: string | null | undefined = get('system.audit.package.name[0]', data); const packageSummary: string | null | undefined = get('system.audit.package.summary[0]', data); const packageVersion: string | null | undefined = get('system.audit.package.version[0]', data); + const processExitCode: number | null | undefined = get('process.exit_code[0]', data); + const processParentName: string | null | undefined = get('process.parent.name[0]', data); + const processParentPid: number | null | undefined = get('process.parent.pid[0]', data); const processHashMd5: string | null | undefined = get('process.hash.md5[0]', data); const processHashSha1: string | null | undefined = get('process.hash.sha1[0]', data); const processHashSha256: string | null | undefined = get('process.hash.sha256[0]', data); @@ -271,6 +283,9 @@ export const SystemGenericFileDetails = React.memo( userDomain={userDomain} userName={userName} message={message} + processExitCode={processExitCode} + processParentName={processParentName} + processParentPid={processParentPid} processTitle={processTitle} workingDirectory={workingDirectory} args={args} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx index 23c7770d1f25b..4de9bcbdd9c18 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx @@ -36,6 +36,17 @@ import { mockEndgameTerminationEvent, mockEndgameUserLogoff, mockEndgameUserLogon, + mockEndpointDisconnectReceivedEvent, + mockEndpointFileCreationEvent, + mockEndpointFileDeletionEvent, + mockEndpointNetworkConnectionAcceptedEvent, + mockEndpointNetworkLookupRequestedEvent, + mockEndpointNetworkLookupResultEvent, + mockEndpointProcessStartEvent, + mockEndpointProcessEndEvent, + mockEndpointSecurityLogOnSuccessEvent, + mockEndpointSecurityLogOnFailureEvent, + mockEndpointSecurityLogOffEvent, } from '../../../../../../common/mock/mock_endgame_ecs_data'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { RowRenderer } from '../row_renderer'; @@ -187,6 +198,31 @@ describe('GenericRowRenderer', () => { }); describe('#createEndgameProcessRowRenderer', () => { + test('it renders an endpoint process start event', () => { + const actionName = 'start'; + const text = i18n.PROCESS_STARTED; + + const endpointProcessStartRowRenderer = createEndgameProcessRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endpointProcessStartRowRenderer.isInstance(mockEndpointProcessStartEvent) && + endpointProcessStartRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: mockEndpointProcessStartEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'SYSTEM\\NT AUTHORITY@win2019-endpoint-1started processconhost.exe(3636)C:\\Windows\\system32\\conhost.exe,0xffffffff,-ForceV1697334c236cce7d4c9e223146ee683a1219adced9729d4ae771fd6a1502a6b63e19da2c35ba1c38adf12d1a472c1fcf1f1a811a71b0e9b5fcb62de0787235ecca560b610' + ); + }); + test('it renders an endgame process creation_event', () => { const actionName = 'creation_event'; const text = i18n.PROCESS_STARTED; @@ -215,6 +251,31 @@ describe('GenericRowRenderer', () => { ); }); + test('it renders an endpoint process end event', () => { + const actionName = 'end'; + const text = i18n.TERMINATED_PROCESS; + + const endpointProcessEndRowRenderer = createEndgameProcessRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endpointProcessEndRowRenderer.isInstance(mockEndpointProcessEndEvent) && + endpointProcessEndRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: mockEndpointProcessEndEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'SYSTEM\\NT AUTHORITY@win2019-endpointterminated processsvchost.exe(10392)C:\\Windows\\System32\\svchost.exe,-k,netsvcs,-p,-s,NetSetupSvcwith exit code-1via parent processservices.exe7fd065bac18c5278777ae44908101cdfed72d26fa741367f0ad4d02020787ab6a1385ce20ad79f55df235effd9780c31442aa2348a0a29438052faed8a2532da50455756' + ); + }); + test('it renders an endgame process termination_event', () => { const actionName = 'termination_event'; const text = i18n.TERMINATED_PROCESS; @@ -331,6 +392,31 @@ describe('GenericRowRenderer', () => { }); describe('#createFimRowRenderer', () => { + test('it renders an endpoint file creation event', () => { + const actionName = 'creation'; + const text = i18n.CREATED_FILE; + + const endpointFileCreationRowRenderer = createFimRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endpointFileCreationRowRenderer.isInstance(mockEndpointFileCreationEvent) && + endpointFileCreationRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: mockEndpointFileCreationEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'SYSTEM\\NT AUTHORITY@win2019-endpointcreated a fileWimProvider.dllinC:\\Windows\\TEMP\\E38FD162-B6E6-4799-B52D-F590BACBAE94\\WimProvider.dllviaMsMpEng.exe(2424)' + ); + }); + test('it renders an endgame file_create_event', () => { const actionName = 'file_create_event'; const text = i18n.CREATED_FILE; @@ -359,6 +445,31 @@ describe('GenericRowRenderer', () => { ); }); + test('it renders an endpoint file deletion event', () => { + const actionName = 'deletion'; + const text = i18n.DELETED_FILE; + + const endpointFileDeletionRowRenderer = createFimRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endpointFileDeletionRowRenderer.isInstance(mockEndpointFileDeletionEvent) && + endpointFileDeletionRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: mockEndpointFileDeletionEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'SYSTEM\\NT AUTHORITY@windows-endpoint-1deleted a fileAM_Delta_Patch_1.329.2793.0.exeinC:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_1.329.2793.0.exeviasvchost.exe(1728)' + ); + }); + test('it renders an endgame file_delete_event', () => { const actionName = 'file_delete_event'; const text = i18n.DELETED_FILE; @@ -529,6 +640,33 @@ describe('GenericRowRenderer', () => { }); describe('#createSocketRowRenderer', () => { + test('it renders an Endpoint network connection_accepted event', () => { + const actionName = 'connection_accepted'; + const text = i18n.ACCEPTED_A_CONNECTION_VIA; + + const endpointConnectionAcceptedRowRenderer = createSocketRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endpointConnectionAcceptedRowRenderer.isInstance( + mockEndpointNetworkConnectionAcceptedEvent + ) && + endpointConnectionAcceptedRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: mockEndpointNetworkConnectionAcceptedEvent, + timelineId: 'test', + })} + + ); + + expect(removeExternalLinkText(wrapper.text())).toEqual( + 'NETWORK SERVICE\\NT AUTHORITY@windows-endpoint-1accepted a connection viasvchost.exe(328)with resultsuccessEndpoint network eventincomingtcpSource10.1.2.3:64557North AmericaUnited States🇺🇸USNorth CarolinaConcordDestination10.50.60.70:3389' + ); + }); + test('it renders an Endgame ipv4_connection_accept_event', () => { const actionName = 'ipv4_connection_accept_event'; const text = i18n.ACCEPTED_A_CONNECTION_VIA; @@ -585,6 +723,31 @@ describe('GenericRowRenderer', () => { ); }); + test('it renders an endpoint network disconnect_received event', () => { + const actionName = 'disconnect_received'; + const text = i18n.DISCONNECTED_VIA; + + const endpointDisconnectReceivedRowRenderer = createSocketRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endpointDisconnectReceivedRowRenderer.isInstance(mockEndpointDisconnectReceivedEvent) && + endpointDisconnectReceivedRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: mockEndpointDisconnectReceivedEvent, + timelineId: 'test', + })} + + ); + + expect(removeExternalLinkText(wrapper.text())).toEqual( + 'NETWORK SERVICE\\NT AUTHORITY@windows-endpoint-1disconnected viasvchost.exe(328)Endpoint network eventincomingtcpSource10.20.30.40:64557North AmericaUnited States🇺🇸USNorth CarolinaConcord(42.47%)1.2KB(57.53%)1.6KBDestination10.11.12.13:3389' + ); + }); + test('it renders an Endgame ipv4_disconnect_received_event', () => { const actionName = 'ipv4_disconnect_received_event'; const text = i18n.DISCONNECTED_VIA; @@ -725,6 +888,48 @@ describe('GenericRowRenderer', () => { }); describe('#createSecurityEventRowRenderer', () => { + test('it renders an endpoint security log_on event with event.outcome: success', () => { + const actionName = 'log_on'; + + const securityLogOnRowRenderer = createSecurityEventRowRenderer({ actionName }); + + const wrapper = mount( + + {securityLogOnRowRenderer.isInstance(mockEndpointSecurityLogOnSuccessEvent) && + securityLogOnRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: mockEndpointSecurityLogOnSuccessEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'SYSTEM\\NT AUTHORITY@win2019-endpointsuccessfully logged inviaC:\\Program Files\\OpenSSH-Win64\\sshd.exe(90210)' + ); + }); + + test('it renders an endpoint security log_on event with event.outcome: failure', () => { + const actionName = 'log_on'; + + const securityLogOnRowRenderer = createSecurityEventRowRenderer({ actionName }); + + const wrapper = mount( + + {securityLogOnRowRenderer.isInstance(mockEndpointSecurityLogOnFailureEvent) && + securityLogOnRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: mockEndpointSecurityLogOnFailureEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'win2019-endpointfailed to log inviaC:\\Program Files\\OpenSSH-Win64\\sshd.exe(90210)' + ); + }); + test('it renders an Endgame user_logon event', () => { const actionName = 'user_logon'; const userLogonEvent = { @@ -797,6 +1002,27 @@ describe('GenericRowRenderer', () => { ); }); + test('it renders an endpoint security log_off event', () => { + const actionName = 'log_off'; + + const securityLogOffRowRenderer = createSecurityEventRowRenderer({ actionName }); + + const wrapper = mount( + + {securityLogOffRowRenderer.isInstance(mockEndpointSecurityLogOffEvent) && + securityLogOffRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: mockEndpointSecurityLogOffEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'SYSTEM\\NT AUTHORITY@win2019-endpointlogged offviaC:\\Windows\\System32\\lsass.exe(90210)' + ); + }); + test('it renders an Endgame user_logoff event', () => { const actionName = 'user_logoff'; const userLogoffEvent = { @@ -845,6 +1071,44 @@ describe('GenericRowRenderer', () => { }); describe('#createDnsRowRenderer', () => { + test('it renders an endpoint network lookup_requested event', () => { + const dnsRowRenderer = createDnsRowRenderer(); + + const wrapper = mount( + + {dnsRowRenderer.isInstance(mockEndpointNetworkLookupRequestedEvent) && + dnsRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: mockEndpointNetworkLookupRequestedEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'SYSTEM\\NT AUTHORITY@win2019-endpointasked forlogging.googleapis.comwith question typeAviagoogle_osconfig_agent.exe(3272)dns' + ); + }); + + test('it renders an endpoint network lookup_result event', () => { + const dnsRowRenderer = createDnsRowRenderer(); + + const wrapper = mount( + + {dnsRowRenderer.isInstance(mockEndpointNetworkLookupResultEvent) && + dnsRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: mockEndpointNetworkLookupResultEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'SYSTEM\\NT AUTHORITY@win2019-endpointasked forlogging.googleapis.comwith question typeAAAAviagoogle_osconfig_agent.exe(3272)dns' + ); + }); + test('it renders an Endgame DNS request_event', () => { const requestEvent = { ...mockEndgameDnsRequest, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx index 431fc2592c8d1..69a6317ebcd11 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx @@ -63,11 +63,11 @@ export const createEndgameProcessRowRenderer = ({ isInstance: (ecs) => { const action: string | null | undefined = get('event.action[0]', ecs); const category: string | null | undefined = get('event.category[0]', ecs); + const dataset: string | null | undefined = get('event.dataset[0]', ecs); return ( - category != null && - category.toLowerCase() === 'process' && - action != null && - action.toLowerCase() === actionName + (category?.toLowerCase() === 'process' || + dataset?.toLowerCase() === 'endpoint.events.process') && + action?.toLowerCase() === actionName ); }, renderRow: ({ browserFields, data, timelineId }) => ( @@ -98,8 +98,7 @@ export const createFimRowRenderer = ({ const dataset: string | null | undefined = get('event.dataset[0]', ecs); return ( isFileEvent({ eventCategory: category, eventDataset: dataset }) && - action != null && - action.toLowerCase() === actionName + action?.toLowerCase() === actionName ); }, renderRow: ({ browserFields, data, timelineId }) => ( @@ -181,11 +180,11 @@ export const createSecurityEventRowRenderer = ({ isInstance: (ecs) => { const category: string | null | undefined = get('event.category[0]', ecs); const action: string | null | undefined = get('event.action[0]', ecs); + const dataset: string | null | undefined = get('event.dataset[0]', ecs); return ( - category != null && - category.toLowerCase() === 'authentication' && - action != null && - action.toLowerCase() === actionName + (category?.toLowerCase() === 'authentication' || + dataset?.toLowerCase() === 'endpoint.events.security') && + action?.toLowerCase() === actionName ); }, renderRow: ({ browserFields, data, timelineId }) => ( @@ -234,6 +233,11 @@ const endgameProcessStartedRowRenderer = createEndgameProcessRowRenderer({ text: i18n.PROCESS_STARTED, }); +const endpointProcessStartRowRenderer = createEndgameProcessRowRenderer({ + actionName: 'start', + text: i18n.PROCESS_STARTED, +}); + const systemProcessStoppedRowRenderer = createGenericFileRowRenderer({ actionName: 'process_stopped', text: i18n.PROCESS_STOPPED, @@ -244,11 +248,21 @@ const endgameProcessTerminationRowRenderer = createEndgameProcessRowRenderer({ text: i18n.TERMINATED_PROCESS, }); +const endpointProcessEndRowRenderer = createEndgameProcessRowRenderer({ + actionName: 'end', + text: i18n.TERMINATED_PROCESS, +}); + const endgameFileCreateEventRowRenderer = createFimRowRenderer({ actionName: 'file_create_event', text: i18n.CREATED_FILE, }); +const endpointFileCreationEventRowRenderer = createFimRowRenderer({ + actionName: 'creation', + text: i18n.CREATED_FILE, +}); + const fimFileCreateEventRowRenderer = createFimRowRenderer({ actionName: 'created', text: i18n.CREATED_FILE, @@ -259,6 +273,11 @@ const endgameFileDeleteEventRowRenderer = createFimRowRenderer({ text: i18n.DELETED_FILE, }); +const endpointFileDeletionEventRowRenderer = createFimRowRenderer({ + actionName: 'deletion', + text: i18n.DELETED_FILE, +}); + const fimFileDeletedEventRowRenderer = createFimRowRenderer({ actionName: 'deleted', text: i18n.DELETED_FILE, @@ -284,6 +303,11 @@ const endgameIpv4ConnectionAcceptEventRowRenderer = createSocketRowRenderer({ text: i18n.ACCEPTED_A_CONNECTION_VIA, }); +const endpointConnectionAcceptedEventRowRenderer = createSocketRowRenderer({ + actionName: 'connection_accepted', + text: i18n.ACCEPTED_A_CONNECTION_VIA, +}); + const endgameIpv6ConnectionAcceptEventRowRenderer = createSocketRowRenderer({ actionName: 'ipv6_connection_accept_event', text: i18n.ACCEPTED_A_CONNECTION_VIA, @@ -294,6 +318,11 @@ const endgameIpv4DisconnectReceivedEventRowRenderer = createSocketRowRenderer({ text: i18n.DISCONNECTED_VIA, }); +const endpointDisconnectReceivedEventRowRenderer = createSocketRowRenderer({ + actionName: 'disconnect_received', + text: i18n.DISCONNECTED_VIA, +}); + const endgameIpv6DisconnectReceivedEventRowRenderer = createSocketRowRenderer({ actionName: 'ipv6_disconnect_received_event', text: i18n.DISCONNECTED_VIA, @@ -315,6 +344,10 @@ const endgameUserLogonRowRenderer = createSecurityEventRowRenderer({ actionName: 'user_logon', }); +const endpointUserLogOnRowRenderer = createSecurityEventRowRenderer({ + actionName: 'log_on', +}); + const dnsRowRenderer = createDnsRowRenderer(); const systemExistingUserRowRenderer = createGenericSystemRowRenderer({ @@ -357,6 +390,10 @@ const systemLogoutRowRenderer = createGenericSystemRowRenderer({ text: i18n.LOGGED_OUT, }); +const endpointUserLogOffRowRenderer = createSecurityEventRowRenderer({ + actionName: 'log_off', +}); + const systemProcessErrorRowRenderer = createGenericFileRowRenderer({ actionName: 'process_error', text: i18n.PROCESS_ERROR, @@ -408,15 +445,22 @@ export const systemRowRenderers: RowRenderer[] = [ endgameAdminLogonRowRenderer, endgameExplicitUserLogonRowRenderer, endgameFileCreateEventRowRenderer, + endpointFileCreationEventRowRenderer, endgameFileDeleteEventRowRenderer, + endpointFileDeletionEventRowRenderer, endgameIpv4ConnectionAcceptEventRowRenderer, + endpointConnectionAcceptedEventRowRenderer, endgameIpv6ConnectionAcceptEventRowRenderer, endgameIpv4DisconnectReceivedEventRowRenderer, + endpointDisconnectReceivedEventRowRenderer, endgameIpv6DisconnectReceivedEventRowRenderer, endgameProcessStartedRowRenderer, + endpointProcessStartRowRenderer, endgameProcessTerminationRowRenderer, + endpointProcessEndRowRenderer, endgameUserLogoffRowRenderer, endgameUserLogonRowRenderer, + endpointUserLogOnRowRenderer, fimFileCreateEventRowRenderer, fimFileDeletedEventRowRenderer, systemAcceptedRowRenderer, @@ -431,6 +475,7 @@ export const systemRowRenderers: RowRenderer[] = [ systemInvalidRowRenderer, systemLoginRowRenderer, systemLogoutRowRenderer, + endpointUserLogOffRowRenderer, systemPackageInstalledRowRenderer, systemPackageUpdatedRowRenderer, systemPackageRemovedRowRenderer, diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts index 649a36d01d47d..5e9391df5b8a4 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts @@ -162,9 +162,12 @@ export const TIMELINE_EVENTS_FIELDS = [ 'tls.server_certificate.fingerprint.sha1', 'user.domain', 'winlog.event_id', + 'process.exit_code', 'process.hash.md5', 'process.hash.sha1', 'process.hash.sha256', + 'process.parent.name', + 'process.parent.pid', 'process.pid', 'process.name', 'process.ppid', From 83e866d62d68101487dd4aec97f871ed138dea9b Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Fri, 5 Feb 2021 14:17:22 -0500 Subject: [PATCH 17/44] skip flaky suite (#90135) --- x-pack/test/api_integration/apis/security_solution/users.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/security_solution/users.ts b/x-pack/test/api_integration/apis/security_solution/users.ts index 178bb3810b087..45e06ab72adbb 100644 --- a/x-pack/test/api_integration/apis/security_solution/users.ts +++ b/x-pack/test/api_integration/apis/security_solution/users.ts @@ -22,7 +22,8 @@ const IP = '0.0.0.0'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); - describe('Users', () => { + // Failing: See https://github.com/elastic/kibana/issues/90135 + describe.skip('Users', () => { describe('With auditbeat', () => { before(() => esArchiver.load('auditbeat/default')); after(() => esArchiver.unload('auditbeat/default')); From eff9d4381f320d5f472476a5afbce8cf78531d59 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Fri, 5 Feb 2021 13:48:14 -0600 Subject: [PATCH 18/44] [ML] Fix incorrect behaviors for Anomaly Detection jobs when resetting or converting to advanced job (#90078) --- .../jobs/jobs_list/components/utils.js | 9 ++++++--- .../common/job_creator/util/general.ts | 19 ++++++++----------- .../jobs/new_job/pages/new_job/page.tsx | 7 ++++++- .../application/services/job_service.d.ts | 8 ++++---- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js index 2b88a9cbcaaf5..98d8b5eaf912a 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js @@ -373,11 +373,14 @@ export function filterJobs(jobs, clauses) { // start datafeed modal. export function checkForAutoStartDatafeed() { const job = mlJobService.tempJobCloningObjects.job; + const datafeed = mlJobService.tempJobCloningObjects.datafeed; if (job !== undefined) { mlJobService.tempJobCloningObjects.job = undefined; - const hasDatafeed = - typeof job.datafeed_config === 'object' && Object.keys(job.datafeed_config).length > 0; - const datafeedId = hasDatafeed ? job.datafeed_config.datafeed_id : ''; + mlJobService.tempJobCloningObjects.datafeed = undefined; + mlJobService.tempJobCloningObjects.createdBy = undefined; + + const hasDatafeed = typeof datafeed === 'object' && Object.keys(datafeed).length > 0; + const datafeedId = hasDatafeed ? datafeed.datafeed_id : ''; return { id: job.job_id, hasDatafeed, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts index d24b1cbf22de3..9ae8585933ca8 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts @@ -229,17 +229,14 @@ export function isSparseDataJob(job: Job, datafeed: Datafeed): boolean { return false; } -function stashCombinedJob( +function stashJobForCloning( jobCreator: JobCreatorType, skipTimeRangeStep: boolean = false, includeTimeRange: boolean = false ) { - const combinedJob = { - ...jobCreator.jobConfig, - datafeed_config: jobCreator.datafeedConfig, - }; - - mlJobService.tempJobCloningObjects.job = combinedJob; + mlJobService.tempJobCloningObjects.job = jobCreator.jobConfig; + mlJobService.tempJobCloningObjects.datafeed = jobCreator.datafeedConfig; + mlJobService.tempJobCloningObjects.createdBy = jobCreator.createdBy ?? undefined; // skip over the time picker step of the wizard mlJobService.tempJobCloningObjects.skipTimeRangeStep = skipTimeRangeStep; @@ -259,21 +256,21 @@ export function convertToMultiMetricJob( ) { jobCreator.createdBy = CREATED_BY_LABEL.MULTI_METRIC; jobCreator.modelPlot = false; - stashCombinedJob(jobCreator, true, true); + stashJobForCloning(jobCreator, true, true); navigateToPath(`jobs/new_job/${JOB_TYPE.MULTI_METRIC}`, true); } export function convertToAdvancedJob(jobCreator: JobCreatorType, navigateToPath: NavigateToPath) { jobCreator.createdBy = null; - stashCombinedJob(jobCreator, true, true); + stashJobForCloning(jobCreator, true, true); navigateToPath(`jobs/new_job/${JOB_TYPE.ADVANCED}`, true); } export function resetJob(jobCreator: JobCreatorType, navigateToPath: NavigateToPath) { jobCreator.jobId = ''; - stashCombinedJob(jobCreator, true, true); + stashJobForCloning(jobCreator, true, true); navigateToPath('/jobs/new_job'); } @@ -282,7 +279,7 @@ export function advancedStartDatafeed( navigateToPath: NavigateToPath ) { if (jobCreator !== null) { - stashCombinedJob(jobCreator, false, false); + stashJobForCloning(jobCreator, false, false); } navigateToPath('/jobs'); } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx index 7b9d79a2cfb2f..c8dbb90804444 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx @@ -72,7 +72,10 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => { let autoSetTimeRange = false; - if (mlJobService.tempJobCloningObjects.job !== undefined) { + if ( + mlJobService.tempJobCloningObjects.job !== undefined && + mlJobService.tempJobCloningObjects.datafeed !== undefined + ) { // cloning a job const clonedJob = mlJobService.tempJobCloningObjects.job; const clonedDatafeed = mlJobService.cloneDatafeed(mlJobService.tempJobCloningObjects.datafeed); @@ -89,6 +92,8 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => { mlJobService.tempJobCloningObjects.skipTimeRangeStep = false; mlJobService.tempJobCloningObjects.job = undefined; + mlJobService.tempJobCloningObjects.datafeed = undefined; + mlJobService.tempJobCloningObjects.createdBy = undefined; if ( mlJobService.tempJobCloningObjects.start !== undefined && diff --git a/x-pack/plugins/ml/public/application/services/job_service.d.ts b/x-pack/plugins/ml/public/application/services/job_service.d.ts index b954f1d344b45..544d346341591 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.d.ts +++ b/x-pack/plugins/ml/public/application/services/job_service.d.ts @@ -7,7 +7,7 @@ import { SearchResponse } from 'elasticsearch'; import { TimeRange } from 'src/plugins/data/common/query/timefilter/types'; -import { CombinedJob, Datafeed } from '../../../common/types/anomaly_detection_jobs'; +import { CombinedJob, Datafeed, Job } from '../../../common/types/anomaly_detection_jobs'; import { Calendar } from '../../../common/types/calendars'; export interface ExistingJobsAndGroups { @@ -21,15 +21,15 @@ declare interface JobService { tempJobCloningObjects: { createdBy?: string; datafeed?: Datafeed; - job: any; + job?: Job; skipTimeRangeStep: boolean; start?: number; end?: number; calendars: Calendar[] | undefined; }; skipTimeRangeStep: boolean; - saveNewJob(job: any): Promise; - cloneDatafeed(datafeed: any): Datafeed; + saveNewJob(job: Job): Promise; + cloneDatafeed(Datafeed: Datafeed): Datafeed; openJob(jobId: string): Promise; saveNewDatafeed(datafeedConfig: any, jobId: string): Promise; startDatafeed( From a7b46a975d2a9bac7fe2ed40f9af18972e6e2784 Mon Sep 17 00:00:00 2001 From: Constance Date: Fri, 5 Feb 2021 12:26:19 -0800 Subject: [PATCH 19/44] Update eslint-plugin-import to latest (#90483) -to grab fixes, case-sensitivity, etc. --- package.json | 2 +- yarn.lock | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index fc5cd02a03253..4afe7a579ad45 100644 --- a/package.json +++ b/package.json @@ -636,7 +636,7 @@ "eslint-plugin-ban": "^1.4.0", "eslint-plugin-cypress": "^2.11.2", "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-import": "^2.19.1", + "eslint-plugin-import": "^2.22.1", "eslint-plugin-jest": "^24.0.2", "eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-mocha": "^6.2.2", diff --git a/yarn.lock b/yarn.lock index 24fe6463fa41c..e4f5fc2e1a8e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5968,6 +5968,11 @@ resolved "https://registry.yarnpkg.com/@types/json-stable-stringify/-/json-stable-stringify-1.0.32.tgz#121f6917c4389db3923640b2e68de5fa64dda88e" integrity sha512-q9Q6+eUEGwQkv4Sbst3J4PNgDOvpuVuKj79Hl/qnmBMEIPzB5QoFRUtjcgcg2xNUZyYUGXBk5wYIBKHt0A+Mxw== +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + "@types/json5@^0.0.30": version "0.0.30" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.30.tgz#44cb52f32a809734ca562e685c6473b5754a7818" @@ -13614,6 +13619,14 @@ eslint-import-resolver-node@0.3.2, eslint-import-resolver-node@^0.3.2: debug "^2.6.9" resolve "^1.5.0" +eslint-import-resolver-node@^0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" + integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA== + dependencies: + debug "^2.6.9" + resolve "^1.13.1" + eslint-import-resolver-webpack@0.11.1: version "0.11.1" resolved "https://registry.yarnpkg.com/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.11.1.tgz#fcf1fd57a775f51e18f442915f85dd6ba45d2f26" @@ -13638,6 +13651,14 @@ eslint-module-utils@2.5.0, eslint-module-utils@^2.4.1: debug "^2.6.9" pkg-dir "^2.0.0" +eslint-module-utils@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz#579ebd094f56af7797d19c9866c9c9486629bfa6" + integrity sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA== + dependencies: + debug "^2.6.9" + pkg-dir "^2.0.0" + eslint-plugin-babel@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/eslint-plugin-babel/-/eslint-plugin-babel-5.3.1.tgz#75a2413ffbf17e7be57458301c60291f2cfbf560" @@ -13693,6 +13714,25 @@ eslint-plugin-import@^2.19.1: read-pkg-up "^2.0.0" resolve "^1.12.0" +eslint-plugin-import@^2.22.1: + version "2.22.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz#0896c7e6a0cf44109a2d97b95903c2bb689d7702" + integrity sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw== + dependencies: + array-includes "^3.1.1" + array.prototype.flat "^1.2.3" + contains-path "^0.1.0" + debug "^2.6.9" + doctrine "1.5.0" + eslint-import-resolver-node "^0.3.4" + eslint-module-utils "^2.6.0" + has "^1.0.3" + minimatch "^3.0.4" + object.values "^1.1.1" + read-pkg-up "^2.0.0" + resolve "^1.17.0" + tsconfig-paths "^3.9.0" + eslint-plugin-jest@^24.0.2: version "24.0.2" resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-24.0.2.tgz#4bf0fcdc86289d702a7dacb430b4363482af773b" @@ -25534,7 +25574,7 @@ resolve@1.8.1: dependencies: path-parse "^1.0.5" -resolve@^1.1.10, resolve@^1.1.4, resolve@^1.1.5, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.7.1, resolve@^1.8.1: +resolve@^1.1.10, resolve@^1.1.4, resolve@^1.1.5, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.7.1, resolve@^1.8.1: version "1.19.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== @@ -28427,6 +28467,16 @@ ts-pnp@^1.1.6: resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== +tsconfig-paths@^3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" + integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.0" + strip-bom "^3.0.0" + tsd@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/tsd/-/tsd-0.13.1.tgz#d2a8baa80b8319dafea37fbeb29fef3cec86e92b" From fc516bacbd367baea0b06447c3710f693ebab06d Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 5 Feb 2021 14:13:51 -0700 Subject: [PATCH 20/44] [index patterns] Add pattern validation method to index patterns fetcher (#90170) --- ...lugins-data-server.indexpatternsfetcher.md | 1 + ...tternsfetcher.validatepatternlistactive.md | 24 ++++ .../fetcher/index_patterns_fetcher.test.ts | 72 ++++++++++++ .../fetcher/index_patterns_fetcher.ts | 40 ++++++- src/plugins/data/server/server.api.md | 1 + .../fields_for_wildcard_route/response.js | 110 ++++++++++-------- 6 files changed, 197 insertions(+), 51 deletions(-) create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.validatepatternlistactive.md create mode 100644 src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.test.ts diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md index 3ba3c862bf16a..608d738676bcf 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md @@ -22,4 +22,5 @@ export declare class IndexPatternsFetcher | --- | --- | --- | | [getFieldsForTimePattern(options)](./kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsfortimepattern.md) | | Get a list of field objects for a time pattern | | [getFieldsForWildcard(options)](./kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md) | | Get a list of field objects for an index pattern that may contain wildcards | +| [validatePatternListActive(patternList)](./kibana-plugin-plugins-data-server.indexpatternsfetcher.validatepatternlistactive.md) | | Returns an index pattern list of only those index pattern strings in the given list that return indices | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.validatepatternlistactive.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.validatepatternlistactive.md new file mode 100644 index 0000000000000..8944c41204323 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.validatepatternlistactive.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsFetcher](./kibana-plugin-plugins-data-server.indexpatternsfetcher.md) > [validatePatternListActive](./kibana-plugin-plugins-data-server.indexpatternsfetcher.validatepatternlistactive.md) + +## IndexPatternsFetcher.validatePatternListActive() method + +Returns an index pattern list of only those index pattern strings in the given list that return indices + +Signature: + +```typescript +validatePatternListActive(patternList: string[]): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| patternList | string[] | | + +Returns: + +`Promise` + diff --git a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.test.ts b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.test.ts new file mode 100644 index 0000000000000..ffdd47e5cdf49 --- /dev/null +++ b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.test.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IndexPatternsFetcher } from '.'; +import { ElasticsearchClient } from 'kibana/server'; +import * as indexNotFoundException from '../../../common/search/test_data/index_not_found_exception.json'; + +describe('Index Pattern Fetcher - server', () => { + let indexPatterns: IndexPatternsFetcher; + let esClient: ElasticsearchClient; + const emptyResponse = { + body: { + count: 0, + }, + }; + const response = { + body: { + count: 1115, + }, + }; + const patternList = ['a', 'b', 'c']; + beforeEach(() => { + esClient = ({ + count: jest.fn().mockResolvedValueOnce(emptyResponse).mockResolvedValue(response), + } as unknown) as ElasticsearchClient; + indexPatterns = new IndexPatternsFetcher(esClient); + }); + + it('Removes pattern without matching indices', async () => { + const result = await indexPatterns.validatePatternListActive(patternList); + expect(result).toEqual(['b', 'c']); + }); + + it('Returns all patterns when all match indices', async () => { + esClient = ({ + count: jest.fn().mockResolvedValue(response), + } as unknown) as ElasticsearchClient; + indexPatterns = new IndexPatternsFetcher(esClient); + const result = await indexPatterns.validatePatternListActive(patternList); + expect(result).toEqual(patternList); + }); + it('Removes pattern when "index_not_found_exception" error is thrown', async () => { + class ServerError extends Error { + public body?: Record; + constructor( + message: string, + public readonly statusCode: number, + errBody?: Record + ) { + super(message); + this.body = errBody; + } + } + + esClient = ({ + count: jest + .fn() + .mockResolvedValueOnce(response) + .mockRejectedValue( + new ServerError('index_not_found_exception', 404, indexNotFoundException) + ), + } as unknown) as ElasticsearchClient; + indexPatterns = new IndexPatternsFetcher(esClient); + const result = await indexPatterns.validatePatternListActive(patternList); + expect(result).toEqual([patternList[0]]); + }); +}); diff --git a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts index cc8bfe28bbc9a..3acdde33f599e 100644 --- a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts +++ b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts @@ -58,9 +58,16 @@ export class IndexPatternsFetcher { rollupIndex?: string; }): Promise { const { pattern, metaFields, fieldCapsOptions, type, rollupIndex } = options; + const patternList = Array.isArray(pattern) ? pattern : pattern.split(','); + let patternListActive: string[] = patternList; + // if only one pattern, don't bother with validation. We let getFieldCapabilities fail if the single pattern is bad regardless + if (patternList.length > 1) { + patternListActive = await this.validatePatternListActive(patternList); + } const fieldCapsResponse = await getFieldCapabilities( this.elasticsearchClient, - pattern, + // if none of the patterns are active, pass the original list to get an error + patternListActive.length > 0 ? patternListActive : patternList, metaFields, { allow_no_indices: fieldCapsOptions @@ -68,6 +75,7 @@ export class IndexPatternsFetcher { : this.allowNoIndices, } ); + if (type === 'rollup' && rollupIndex) { const rollupFields: FieldDescriptor[] = []; const rollupIndexCapabilities = getCapabilitiesForRollupIndices( @@ -118,4 +126,34 @@ export class IndexPatternsFetcher { } return await getFieldCapabilities(this.elasticsearchClient, indices, metaFields); } + + /** + * Returns an index pattern list of only those index pattern strings in the given list that return indices + * + * @param patternList string[] + * @return {Promise} + */ + async validatePatternListActive(patternList: string[]) { + const result = await Promise.all( + patternList + .map((pattern) => + this.elasticsearchClient.count({ + index: pattern, + }) + ) + .map((p) => + p.catch((e) => { + if (e.body.error.type === 'index_not_found_exception') { + return { body: { count: 0 } }; + } + throw e; + }) + ) + ); + return result.reduce( + (acc: string[], { body: { count } }, patternListIndex) => + count > 0 ? [...acc, patternList[patternListIndex]] : acc, + [] + ); + } } diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 68582a9d877e9..3b1440f211bfe 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -885,6 +885,7 @@ export class IndexPatternsFetcher { type?: string; rollupIndex?: string; }): Promise; + validatePatternListActive(patternList: string[]): Promise; } // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceStart" needs to be exported by the entry point index.d.ts diff --git a/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js b/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js index e84052e58dac4..87c5aa535ccd9 100644 --- a/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js +++ b/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js @@ -17,6 +17,55 @@ export default function ({ getService }) { expect(resp.body.fields).to.eql(sortBy(resp.body.fields, 'name')); }; + const testFields = [ + { + type: 'boolean', + esTypes: ['boolean'], + searchable: true, + aggregatable: true, + name: 'bar', + readFromDocValues: true, + }, + { + type: 'string', + esTypes: ['text'], + searchable: true, + aggregatable: false, + name: 'baz', + readFromDocValues: false, + }, + { + type: 'string', + esTypes: ['keyword'], + searchable: true, + aggregatable: true, + name: 'baz.keyword', + readFromDocValues: true, + subType: { multi: { parent: 'baz' } }, + }, + { + type: 'number', + esTypes: ['long'], + searchable: true, + aggregatable: true, + name: 'foo', + readFromDocValues: true, + }, + { + aggregatable: true, + esTypes: ['keyword'], + name: 'nestedField.child', + readFromDocValues: true, + searchable: true, + subType: { + nested: { + path: 'nestedField', + }, + }, + type: 'string', + }, + ]; + describe('fields_for_wildcard_route response', () => { before(() => esArchiver.load('index_patterns/basic_index')); after(() => esArchiver.unload('index_patterns/basic_index')); @@ -26,54 +75,7 @@ export default function ({ getService }) { .get('/api/index_patterns/_fields_for_wildcard') .query({ pattern: 'basic_index' }) .expect(200, { - fields: [ - { - type: 'boolean', - esTypes: ['boolean'], - searchable: true, - aggregatable: true, - name: 'bar', - readFromDocValues: true, - }, - { - type: 'string', - esTypes: ['text'], - searchable: true, - aggregatable: false, - name: 'baz', - readFromDocValues: false, - }, - { - type: 'string', - esTypes: ['keyword'], - searchable: true, - aggregatable: true, - name: 'baz.keyword', - readFromDocValues: true, - subType: { multi: { parent: 'baz' } }, - }, - { - type: 'number', - esTypes: ['long'], - searchable: true, - aggregatable: true, - name: 'foo', - readFromDocValues: true, - }, - { - aggregatable: true, - esTypes: ['keyword'], - name: 'nestedField.child', - readFromDocValues: true, - searchable: true, - subType: { - nested: { - path: 'nestedField', - }, - }, - type: 'string', - }, - ], + fields: testFields, }) .then(ensureFieldsAreSorted); }); @@ -162,11 +164,19 @@ export default function ({ getService }) { .then(ensureFieldsAreSorted); }); - it('returns 404 when the pattern does not exist', async () => { + it('returns fields when one pattern exists and the other does not', async () => { + await supertest + .get('/api/index_patterns/_fields_for_wildcard') + .query({ pattern: 'bad_index,basic_index' }) + .expect(200, { + fields: testFields, + }); + }); + it('returns 404 when no patterns exist', async () => { await supertest .get('/api/index_patterns/_fields_for_wildcard') .query({ - pattern: '[non-existing-pattern]its-invalid-*', + pattern: 'bad_index', }) .expect(404); }); From feda8a07854ca18ce5d932c4ab990adfa3a752fd Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 5 Feb 2021 21:55:09 +0000 Subject: [PATCH 21/44] chore(NA): build bazel projects all at once in the distributable build process (#90328) * chore(NA): build bazel projects all at once in the distributable build process * chore(NA): make sure bazelisk is installed * chore(NA): install bazelisk using npm * chore(NA): remove extra spac * chore(NA): test yarn path exports * chore(NA): add direct global dir * chore(NA): some more debug steps * chore(NA): remove one statement * chore(NA): comment one more line out for testing purposes * chore(NA): export the correct yarn bin location into the PATH * chore(NA): cleaning implementation * chore(NA): move installation process of bazelisk into npm * chore(NA): add missing type --- packages/kbn-pm/dist/index.js | 60 ++++++++++++------- .../build_bazel_production_projects.ts | 7 ++- .../kbn-pm/src/utils/bazel/install_tools.ts | 32 +++++++--- src/dev/ci_setup/setup.sh | 5 -- src/dev/ci_setup/setup_env.sh | 11 ++++ 5 files changed, 76 insertions(+), 39 deletions(-) diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 4d065411f91b6..abb941d211713 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -48106,23 +48106,34 @@ async function isBazelBinAvailable() { } } +async function isBazeliskInstalled(bazeliskVersion) { + try { + const { + stdout: bazeliskPkgInstallStdout + } = await Object(_child_process__WEBPACK_IMPORTED_MODULE_2__["spawn"])('npm', ['ls', '--global', '--parseable', '--long', `@bazel/bazelisk@${bazeliskVersion}`], { + stdio: 'pipe' + }); + return bazeliskPkgInstallStdout.includes(`@bazel/bazelisk@${bazeliskVersion}`); + } catch { + return false; + } +} + async function installBazelTools(repoRootPath) { _log__WEBPACK_IMPORTED_MODULE_4__["log"].debug(`[bazel_tools] reading bazel tools versions from version files`); const bazeliskVersion = await readBazelToolsVersionFile(repoRootPath, '.bazeliskversion'); const bazelVersion = await readBazelToolsVersionFile(repoRootPath, '.bazelversion'); // Check what globals are installed - _log__WEBPACK_IMPORTED_MODULE_4__["log"].debug(`[bazel_tools] verify if bazelisk is installed`); - const { - stdout: bazeliskPkgInstallStdout - } = await Object(_child_process__WEBPACK_IMPORTED_MODULE_2__["spawn"])('yarn', ['global', 'list'], { - stdio: 'pipe' - }); + _log__WEBPACK_IMPORTED_MODULE_4__["log"].debug(`[bazel_tools] verify if bazelisk is installed`); // Test if bazelisk is already installed in the correct version + + const isBazeliskPkgInstalled = await isBazeliskInstalled(bazeliskVersion); // Test if bazel bin is available + const isBazelBinAlreadyAvailable = await isBazelBinAvailable(); // Install bazelisk if not installed - if (!bazeliskPkgInstallStdout.includes(`@bazel/bazelisk@${bazeliskVersion}`) || !isBazelBinAlreadyAvailable) { + if (!isBazeliskPkgInstalled || !isBazelBinAlreadyAvailable) { _log__WEBPACK_IMPORTED_MODULE_4__["log"].info(`[bazel_tools] installing Bazel tools`); _log__WEBPACK_IMPORTED_MODULE_4__["log"].debug(`[bazel_tools] bazelisk is not installed. Installing @bazel/bazelisk@${bazeliskVersion} and bazel@${bazelVersion}`); - await Object(_child_process__WEBPACK_IMPORTED_MODULE_2__["spawn"])('yarn', ['global', 'add', `@bazel/bazelisk@${bazeliskVersion}`], { + await Object(_child_process__WEBPACK_IMPORTED_MODULE_2__["spawn"])('npm', ['install', '--global', `@bazel/bazelisk@${bazeliskVersion}`], { env: { USE_BAZEL_VERSION: bazelVersion }, @@ -48132,7 +48143,7 @@ async function installBazelTools(repoRootPath) { if (!isBazelBinAvailableAfterInstall) { throw new Error(dedent__WEBPACK_IMPORTED_MODULE_0___default.a` - [bazel_tools] an error occurred when installing the Bazel tools. Please make sure 'yarn global bin' is on your $PATH, otherwise just add it there + [bazel_tools] an error occurred when installing the Bazel tools. Please make sure you have access to npm globally installed modules on your $PATH `); } } @@ -59771,10 +59782,11 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(745); -/* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(131); -/* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(246); -/* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(251); -/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(248); +/* harmony import */ var _utils_bazel_run__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(374); +/* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(131); +/* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(246); +/* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(251); +/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(248); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License @@ -59790,17 +59802,19 @@ __webpack_require__.r(__webpack_exports__); + async function buildBazelProductionProjects({ kibanaRoot, buildRoot, onlyOSS }) { - const projects = await Object(_utils_projects__WEBPACK_IMPORTED_MODULE_7__["getBazelProjectsOnly"])(await Object(_build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__["getProductionProjects"])(kibanaRoot, onlyOSS)); + const projects = await Object(_utils_projects__WEBPACK_IMPORTED_MODULE_8__["getBazelProjectsOnly"])(await Object(_build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__["getProductionProjects"])(kibanaRoot, onlyOSS)); const projectNames = [...projects.values()].map(project => project.name); - _utils_log__WEBPACK_IMPORTED_MODULE_5__["log"].info(`Preparing Bazel projects production build for [${projectNames.join(', ')}]`); + _utils_log__WEBPACK_IMPORTED_MODULE_6__["log"].info(`Preparing Bazel projects production build for [${projectNames.join(', ')}]`); + await Object(_utils_bazel_run__WEBPACK_IMPORTED_MODULE_4__["runBazel"])(['build', '//packages:build']); + _utils_log__WEBPACK_IMPORTED_MODULE_6__["log"].info(`All Bazel projects production builds for [${projectNames.join(', ')}] are complete}]`); for (const project of projects.values()) { - await Object(_build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__["buildProject"])(project); await copyToBuild(project, kibanaRoot, buildRoot); await applyCorrectPermissions(project, kibanaRoot, buildRoot); } @@ -59835,9 +59849,9 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { // the intermediate build, we fall back to using the project's already defined // `package.json`. - const packageJson = (await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_4__["isFile"])(Object(path__WEBPACK_IMPORTED_MODULE_2__["join"])(buildProjectPath, 'package.json'))) ? await Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_6__["readPackageJson"])(buildProjectPath) : project.json; - const preparedPackageJson = Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_6__["createProductionPackageJson"])(packageJson); - await Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_6__["writePackageJson"])(buildProjectPath, preparedPackageJson); + const packageJson = (await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["isFile"])(Object(path__WEBPACK_IMPORTED_MODULE_2__["join"])(buildProjectPath, 'package.json'))) ? await Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_7__["readPackageJson"])(buildProjectPath) : project.json; + const preparedPackageJson = Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_7__["createProductionPackageJson"])(packageJson); + await Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_7__["writePackageJson"])(buildProjectPath, preparedPackageJson); } async function applyCorrectPermissions(project, kibanaRoot, buildRoot) { @@ -59852,12 +59866,12 @@ async function applyCorrectPermissions(project, kibanaRoot, buildRoot) { for (const pluginPath of allPluginPaths) { const resolvedPluginPath = Object(path__WEBPACK_IMPORTED_MODULE_2__["resolve"])(buildRoot, pluginPath); - if (await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_4__["isFile"])(resolvedPluginPath)) { - await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_4__["chmod"])(resolvedPluginPath, 0o644); + if (await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["isFile"])(resolvedPluginPath)) { + await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["chmod"])(resolvedPluginPath, 0o644); } - if (await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_4__["isDirectory"])(resolvedPluginPath)) { - await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_4__["chmod"])(resolvedPluginPath, 0o755); + if (await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["isDirectory"])(resolvedPluginPath)) { + await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["chmod"])(resolvedPluginPath, 0o755); } } } diff --git a/packages/kbn-pm/src/production/build_bazel_production_projects.ts b/packages/kbn-pm/src/production/build_bazel_production_projects.ts index cd40653a6b54c..a54d6c753d8d7 100644 --- a/packages/kbn-pm/src/production/build_bazel_production_projects.ts +++ b/packages/kbn-pm/src/production/build_bazel_production_projects.ts @@ -10,7 +10,8 @@ import copy from 'cpy'; import globby from 'globby'; import { basename, join, relative, resolve } from 'path'; -import { buildProject, getProductionProjects } from './build_non_bazel_production_projects'; +import { getProductionProjects } from './build_non_bazel_production_projects'; +import { runBazel } from '../utils/bazel/run'; import { chmod, isFile, isDirectory } from '../utils/fs'; import { log } from '../utils/log'; import { @@ -35,8 +36,10 @@ export async function buildBazelProductionProjects({ const projectNames = [...projects.values()].map((project) => project.name); log.info(`Preparing Bazel projects production build for [${projectNames.join(', ')}]`); + await runBazel(['build', '//packages:build']); + log.info(`All Bazel projects production builds for [${projectNames.join(', ')}] are complete}]`); + for (const project of projects.values()) { - await buildProject(project); await copyToBuild(project, kibanaRoot, buildRoot); await applyCorrectPermissions(project, kibanaRoot, buildRoot); } diff --git a/packages/kbn-pm/src/utils/bazel/install_tools.ts b/packages/kbn-pm/src/utils/bazel/install_tools.ts index dfd20f5974d67..cee6eff317afa 100644 --- a/packages/kbn-pm/src/utils/bazel/install_tools.ts +++ b/packages/kbn-pm/src/utils/bazel/install_tools.ts @@ -36,6 +36,22 @@ async function isBazelBinAvailable() { } } +async function isBazeliskInstalled(bazeliskVersion: string) { + try { + const { stdout: bazeliskPkgInstallStdout } = await spawn( + 'npm', + ['ls', '--global', '--parseable', '--long', `@bazel/bazelisk@${bazeliskVersion}`], + { + stdio: 'pipe', + } + ); + + return bazeliskPkgInstallStdout.includes(`@bazel/bazelisk@${bazeliskVersion}`); + } catch { + return false; + } +} + export async function installBazelTools(repoRootPath: string) { log.debug(`[bazel_tools] reading bazel tools versions from version files`); const bazeliskVersion = await readBazelToolsVersionFile(repoRootPath, '.bazeliskversion'); @@ -43,23 +59,21 @@ export async function installBazelTools(repoRootPath: string) { // Check what globals are installed log.debug(`[bazel_tools] verify if bazelisk is installed`); - const { stdout: bazeliskPkgInstallStdout } = await spawn('yarn', ['global', 'list'], { - stdio: 'pipe', - }); + // Test if bazelisk is already installed in the correct version + const isBazeliskPkgInstalled = await isBazeliskInstalled(bazeliskVersion); + + // Test if bazel bin is available const isBazelBinAlreadyAvailable = await isBazelBinAvailable(); // Install bazelisk if not installed - if ( - !bazeliskPkgInstallStdout.includes(`@bazel/bazelisk@${bazeliskVersion}`) || - !isBazelBinAlreadyAvailable - ) { + if (!isBazeliskPkgInstalled || !isBazelBinAlreadyAvailable) { log.info(`[bazel_tools] installing Bazel tools`); log.debug( `[bazel_tools] bazelisk is not installed. Installing @bazel/bazelisk@${bazeliskVersion} and bazel@${bazelVersion}` ); - await spawn('yarn', ['global', 'add', `@bazel/bazelisk@${bazeliskVersion}`], { + await spawn('npm', ['install', '--global', `@bazel/bazelisk@${bazeliskVersion}`], { env: { USE_BAZEL_VERSION: bazelVersion, }, @@ -69,7 +83,7 @@ export async function installBazelTools(repoRootPath: string) { const isBazelBinAvailableAfterInstall = await isBazelBinAvailable(); if (!isBazelBinAvailableAfterInstall) { throw new Error(dedent` - [bazel_tools] an error occurred when installing the Bazel tools. Please make sure 'yarn global bin' is on your $PATH, otherwise just add it there + [bazel_tools] an error occurred when installing the Bazel tools. Please make sure you have access to npm globally installed modules on your $PATH `); } } diff --git a/src/dev/ci_setup/setup.sh b/src/dev/ci_setup/setup.sh index e5e21e312b0dd..61f578ba33971 100755 --- a/src/dev/ci_setup/setup.sh +++ b/src/dev/ci_setup/setup.sh @@ -65,8 +65,3 @@ if [ "$GIT_CHANGES" ]; then echo -e "$GIT_CHANGES\n" exit 1 fi - -### -### copy .bazelrc-ci into $HOME/.bazelrc -### -cp "src/dev/ci_setup/.bazelrc-ci" "$HOME/.bazelrc"; diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 5dac270239c4a..0b835d4b9fa94 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -175,4 +175,15 @@ if [[ -d "$ES_DIR" && -f "$ES_JAVA_PROP_PATH" ]]; then export JAVA_HOME=$HOME/.java/$ES_BUILD_JAVA fi +### +### copy .bazelrc-ci into $HOME/.bazelrc +### +cp -f "$KIBANA_DIR/src/dev/ci_setup/.bazelrc-ci" "$HOME/.bazelrc"; + +### +### append auth token to buildbuddy into "$HOME/.bazelrc"; +### +echo "# Appended by $KIBANA_DIR/src/dev/ci_setup/setup.sh" >> "$HOME/.bazelrc" +echo "build --remote_header=x-buildbuddy-api-key=$KIBANA_BUILDBUDDY_CI_API_KEY" >> "$HOME/.bazelrc" + export CI_ENV_SETUP=true From befe41067e2cd4d5cc3e11fe2d910b42344bc4eb Mon Sep 17 00:00:00 2001 From: liza-mae Date: Fri, 5 Feb 2021 15:06:15 -0700 Subject: [PATCH 22/44] [Docs] Update reporting troubleshooting for arm rhel/centos (#90385) * Update reporting document * Move to own section * Remove extra line --- docs/user/reporting/reporting-troubleshooting.asciidoc | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/user/reporting/reporting-troubleshooting.asciidoc b/docs/user/reporting/reporting-troubleshooting.asciidoc index 1f07b0b57d8c7..ebe095e0881b3 100644 --- a/docs/user/reporting/reporting-troubleshooting.asciidoc +++ b/docs/user/reporting/reporting-troubleshooting.asciidoc @@ -15,6 +15,7 @@ Having trouble? Here are solutions to common problems you might encounter while * <> * <> * <> +* <> [float] [[reporting-diagnostics]] @@ -156,3 +157,9 @@ requests to render. If the {kib} instance doesn't have enough memory to run the report, the report fails with an error such as `Error: Page crashed!` In this case, try increasing the memory for the {kib} instance to 2GB. + +[float] +[[reporting-troubleshooting-arm-systems]] +=== ARM systems + +Chromium is not compatible with ARM RHEL/CentOS. From f4dc6d0235f3abeaa5196587eaac199b829aad4c Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Fri, 5 Feb 2021 14:14:31 -0800 Subject: [PATCH 23/44] [Fleet] Fix incorrect conversion of string to numeric values in agent YAML (#90371) * Convert user values back to string after yaml template compilation if they were strings originally * Add better test cases and adjust patch * Fix when field is undefined * Handle array of strings too --- .../server/services/epm/agent/agent.test.ts | 16 ++++++++++++++++ .../fleet/server/services/epm/agent/agent.ts | 12 ++++++++++++ 2 files changed, 28 insertions(+) diff --git a/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts b/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts index 3e1d3d57bbf71..7ab904b2f15e1 100644 --- a/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts @@ -22,10 +22,23 @@ password: {{password}} {{#if password}} hidden_password: {{password}} {{/if}} +{{#if optional_field}} +optional_field: {{optional_field}} +{{/if}} +foo: {{bar}} +some_text_field: {{should_be_text}} +multi_text_field: +{{#each multi_text}} + - {{this}} +{{/each}} `; const vars = { paths: { value: ['/usr/local/var/log/nginx/access.log'] }, password: { type: 'password', value: '' }, + optional_field: { type: 'text', value: undefined }, + bar: { type: 'text', value: 'bar' }, + should_be_text: { type: 'text', value: '1234' }, + multi_text: { type: 'text', value: ['1234', 'foo', 'bar'] }, }; const output = compileTemplate(vars, streamTemplate); @@ -35,6 +48,9 @@ hidden_password: {{password}} exclude_files: ['.gz$'], processors: [{ add_locale: null }], password: '', + foo: 'bar', + some_text_field: '1234', + multi_text_field: ['1234', 'foo', 'bar'], }); }); diff --git a/x-pack/plugins/fleet/server/services/epm/agent/agent.ts b/x-pack/plugins/fleet/server/services/epm/agent/agent.ts index 6b1d84ea28b0a..4f39da5b0b70d 100644 --- a/x-pack/plugins/fleet/server/services/epm/agent/agent.ts +++ b/x-pack/plugins/fleet/server/services/epm/agent/agent.ts @@ -58,6 +58,10 @@ function replaceVariablesInYaml(yamlVariables: { [k: string]: any }, yaml: any) return yaml; } +const maybeEscapeNumericString = (value: string) => { + return value.length && !isNaN(+value) ? `"${value}"` : value; +}; + function buildTemplateVariables(variables: PackagePolicyConfigRecord, templateStr: string) { const yamlValues: { [k: string]: any } = {}; const vars = Object.entries(variables).reduce((acc, [key, recordEntry]) => { @@ -84,6 +88,14 @@ function buildTemplateVariables(variables: PackagePolicyConfigRecord, templateSt const yamlKeyPlaceholder = `##${key}##`; varPart[lastKeyPart] = `"${yamlKeyPlaceholder}"`; yamlValues[yamlKeyPlaceholder] = recordEntry.value ? safeLoad(recordEntry.value) : null; + } else if (recordEntry.type && recordEntry.type === 'text' && recordEntry.value?.length) { + if (Array.isArray(recordEntry.value)) { + varPart[lastKeyPart] = recordEntry.value.map((value: string) => + maybeEscapeNumericString(value) + ); + } else { + varPart[lastKeyPart] = maybeEscapeNumericString(recordEntry.value); + } } else { varPart[lastKeyPart] = recordEntry.value; } From efcd2c38ef0181756af49e7228f1c9e37f372034 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Fri, 5 Feb 2021 17:23:26 -0500 Subject: [PATCH 24/44] Skip failing suite (#90526) --- .../apps/ml/data_frame_analytics/feature_importance.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts index 49728603c246c..b8bdc7de16e1e 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts @@ -14,7 +14,8 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - describe('total feature importance panel and decision path popover', function () { + // Failing: See https://github.com/elastic/kibana/issues/90526 + describe.skip('total feature importance panel and decision path popover', function () { const testDataList: Array<{ suiteTitle: string; archive: string; From a9fce985a5e22e64c07b5d082848d272997373ad Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 5 Feb 2021 23:45:30 +0000 Subject: [PATCH 25/44] chore(NA): integrate build buddy with our bazel setup and remote cache for ci (#90116) * chore(NA): simple changes on bazelrc * chore(NA): integrate bazel tools with BuildBuddy and remote cache service * chore(NA) fix bazelrc line config * chore(NA): move non auth settings out of bazelrc.auth * chore(NA): output home dir * chore(NA): load .bazelrc-ci.auth from /Users/tiagocosta dir * chore(NA): remove bazelrc auth file and append directly into home bazelrc * chore(NA): comment announce option * chore(NA): integrate build buddy metadata * chore(NA): update src/dev/ci_setup/.bazelrc-ci Co-authored-by: Tyler Smalley * chore(NA): move build metadata integation to common confdig * chore(NA): fix problem on bazel file location * chore(NA): correct sh file permissions * chore(NA): only get host on CI * chore(NA): add cores into host info on CI * chore(NA): sync with last settings to setup bazelisk tools on ci * chore(NA): sync last changes on ci setup env * chore(NA): sync settings on ci setup with the other PR * chore(NA): remove yarn export Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Tyler Smalley --- .bazelrc | 9 +++++ src/dev/bazel_workspace_status.sh | 57 +++++++++++++++++++++++++++++ src/dev/ci_setup/.bazelrc-ci | 12 ++++-- src/dev/ci_setup/.bazelrc-ci.common | 3 -- src/dev/ci_setup/load_env_keys.sh | 3 ++ src/dev/ci_setup/setup.sh | 11 ++++++ 6 files changed, 89 insertions(+), 6 deletions(-) create mode 100755 src/dev/bazel_workspace_status.sh diff --git a/.bazelrc b/.bazelrc index 741067e4ff18e..158338ec5f093 100644 --- a/.bazelrc +++ b/.bazelrc @@ -2,8 +2,17 @@ # Import shared settings first so we can override below import %workspace%/.bazelrc.common +## Disabled for now # Remote cache settings for local env # build --remote_cache=https://storage.googleapis.com/kibana-bazel-cache # build --incompatible_remote_results_ignore_disk=true # build --remote_accept_cached=true # build --remote_upload_local_results=false + +# BuildBuddy +## Metadata settings +build --workspace_status_command=$(pwd)/src/dev/bazel_workspace_status.sh +# Enable this in case you want to share your build info +# build --build_metadata=VISIBILITY=PUBLIC +build --build_metadata=TEST_GROUPS=//packages + diff --git a/src/dev/bazel_workspace_status.sh b/src/dev/bazel_workspace_status.sh new file mode 100755 index 0000000000000..efaca4bb98849 --- /dev/null +++ b/src/dev/bazel_workspace_status.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# Inspired on https://github.com/buildbuddy-io/buildbuddy/blob/master/workspace_status.sh +# This script will be run bazel when building process starts to +# generate key-value information that represents the status of the +# workspace. The output should be like +# +# KEY1 VALUE1 +# KEY2 VALUE2 +# +# If the script exits with non-zero code, it's considered as a failure +# and the output will be discarded. + +# Git repo +repo_url=$(git config --get remote.origin.url) +if [[ $? != 0 ]]; +then + exit 1 +fi +echo "REPO_URL ${repo_url}" + +# Commit SHA +commit_sha=$(git rev-parse HEAD) +if [[ $? != 0 ]]; +then + exit 1 +fi +echo "COMMIT_SHA ${commit_sha}" + +# Git branch +repo_url=$(git rev-parse --abbrev-ref HEAD) +if [[ $? != 0 ]]; +then + exit 1 +fi +echo "GIT_BRANCH ${repo_url}" + +# Tree status +git diff-index --quiet HEAD -- +if [[ $? == 0 ]]; +then + tree_status="Clean" +else + tree_status="Modified" +fi +echo "GIT_TREE_STATUS ${tree_status}" + +# Host +if [ "$CI" = "true" ]; then + host=$(hostname | sed 's|\(.*\)-.*|\1|') + cores=$(grep ^cpu\\scores /proc/cpuinfo | uniq | awk '{print $4}' ) + if [[ $? != 0 ]]; + then + exit 1 + fi + echo "HOST ${host}-${cores}" +fi diff --git a/src/dev/ci_setup/.bazelrc-ci b/src/dev/ci_setup/.bazelrc-ci index 5b345d3c9e207..ef6fab3a30590 100644 --- a/src/dev/ci_setup/.bazelrc-ci +++ b/src/dev/ci_setup/.bazelrc-ci @@ -5,6 +5,12 @@ # Import and load bazelrc common settings for ci env try-import %workspace%/src/dev/ci_setup/.bazelrc-ci.common -# Remote cache settings for ci env -# build --google_default_credentials -# build --remote_upload_local_results=true +# BuildBuddy settings +## Remote settings including cache +build --bes_results_url=https://app.buildbuddy.io/invocation/ +build --bes_backend=grpcs://cloud.buildbuddy.io +build --remote_cache=grpcs://cloud.buildbuddy.io +build --remote_timeout=3600 + +## Metadata settings +build --build_metadata=ROLE=CI diff --git a/src/dev/ci_setup/.bazelrc-ci.common b/src/dev/ci_setup/.bazelrc-ci.common index 3f58e4e03a178..9d00ee5639741 100644 --- a/src/dev/ci_setup/.bazelrc-ci.common +++ b/src/dev/ci_setup/.bazelrc-ci.common @@ -4,8 +4,5 @@ # Don't be spammy in the logs build --noshow_progress -# Print all the options that apply to the build. -build --announce_rc - # More details on failures build --verbose_failures=true diff --git a/src/dev/ci_setup/load_env_keys.sh b/src/dev/ci_setup/load_env_keys.sh index 62d29db232eae..5f7a6c26bab21 100644 --- a/src/dev/ci_setup/load_env_keys.sh +++ b/src/dev/ci_setup/load_env_keys.sh @@ -34,6 +34,9 @@ else PERCY_TOKEN=$(retry 5 vault read -field=value secret/kibana-issues/dev/percy) export PERCY_TOKEN + KIBANA_BUILDBUDDY_CI_API_KEY=$(retry 5 vault read -field=value secret/kibana-issues/dev/kibana-buildbuddy-ci-api-key) + export KIBANA_BUILDBUDDY_CI_API_KEY + # remove vault related secrets unset VAULT_ROLE_ID VAULT_SECRET_ID VAULT_TOKEN VAULT_ADDR fi diff --git a/src/dev/ci_setup/setup.sh b/src/dev/ci_setup/setup.sh index 61f578ba33971..0b24f0b22b81a 100755 --- a/src/dev/ci_setup/setup.sh +++ b/src/dev/ci_setup/setup.sh @@ -10,6 +10,17 @@ echo " -- PARENT_DIR='$PARENT_DIR'" echo " -- KIBANA_PKG_BRANCH='$KIBANA_PKG_BRANCH'" echo " -- TEST_ES_SNAPSHOT_VERSION='$TEST_ES_SNAPSHOT_VERSION'" +### +### copy .bazelrc-ci into $HOME/.bazelrc +### +cp "src/dev/ci_setup/.bazelrc-ci" "$HOME/.bazelrc"; + +### +### append auth token to buildbuddy into "$HOME/.bazelrc"; +### +echo "# Appended by src/dev/ci_setup/setup.sh" >> "$HOME/.bazelrc" +echo "build --remote_header=x-buildbuddy-api-key=$KIBANA_BUILDBUDDY_CI_API_KEY" >> "$HOME/.bazelrc" + ### ### install dependencies ### From be725cabc2f74d2c0a812b44717c1f3add4b130a Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Fri, 5 Feb 2021 17:10:49 -0800 Subject: [PATCH 26/44] [test] Await retry.waitFor (#90456) Signed-off-by: Tyler Smalley --- test/functional/apps/console/_console.ts | 4 +++- x-pack/test/functional/page_objects/upgrade_assistant_page.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/test/functional/apps/console/_console.ts b/test/functional/apps/console/_console.ts index 6aeb1e2a624ad..05933ebf1ea2a 100644 --- a/test/functional/apps/console/_console.ts +++ b/test/functional/apps/console/_console.ts @@ -85,7 +85,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.console.dismissTutorial(); expect(await PageObjects.console.hasAutocompleter()).to.be(false); await PageObjects.console.promptAutocomplete(); - retry.waitFor('autocomplete to be visible', () => PageObjects.console.hasAutocompleter()); + await retry.waitFor('autocomplete to be visible', () => + PageObjects.console.hasAutocompleter() + ); }); }); } diff --git a/x-pack/test/functional/page_objects/upgrade_assistant_page.ts b/x-pack/test/functional/page_objects/upgrade_assistant_page.ts index da1518ed72b48..1c4a85450a8da 100644 --- a/x-pack/test/functional/page_objects/upgrade_assistant_page.ts +++ b/x-pack/test/functional/page_objects/upgrade_assistant_page.ts @@ -24,7 +24,7 @@ export function UpgradeAssistantPageProvider({ getPageObjects, getService }: Ftr return await retry.try(async () => { await common.navigateToApp('settings'); await testSubjects.click('upgrade_assistant'); - retry.waitFor('url to contain /upgrade_assistant', async () => { + await retry.waitFor('url to contain /upgrade_assistant', async () => { const url = await browser.getCurrentUrl(); return url.includes('/upgrade_assistant'); }); @@ -61,7 +61,7 @@ export function UpgradeAssistantPageProvider({ getPageObjects, getService }: Ftr async waitForTelemetryHidden() { const self = this; - retry.waitFor('Telemetry to disappear.', async () => { + await retry.waitFor('Telemetry to disappear.', async () => { return (await self.isTelemetryExists()) === false; }); } From 6408a668e454f8c696d59d59ca1b9ffbe938326d Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Sat, 6 Feb 2021 03:27:21 +0000 Subject: [PATCH 27/44] chore(NA): add safe guard to remove bazelisk from yarn global at bootstrap (#90538) --- packages/kbn-pm/dist/index.js | 27 ++++++++++++++++++- .../kbn-pm/src/utils/bazel/install_tools.ts | 26 ++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index abb941d211713..d939e7b3000fa 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -48119,6 +48119,29 @@ async function isBazeliskInstalled(bazeliskVersion) { } } +async function tryRemoveBazeliskFromYarnGlobal() { + try { + // Check if Bazelisk is installed on the yarn global scope + const { + stdout: bazeliskPkgInstallStdout + } = await Object(_child_process__WEBPACK_IMPORTED_MODULE_2__["spawn"])('yarn', ['global', 'list'], { + stdio: 'pipe' + }); // Bazelisk was found on yarn global scope so lets remove it + + if (bazeliskPkgInstallStdout.includes(`@bazel/bazelisk@`)) { + await Object(_child_process__WEBPACK_IMPORTED_MODULE_2__["spawn"])('yarn', ['global', 'remove', `@bazel/bazelisk`], { + stdio: 'pipe' + }); + _log__WEBPACK_IMPORTED_MODULE_4__["log"].info(`[bazel_tools] bazelisk was installed on Yarn global packages and is now removed`); + return true; + } + + return false; + } catch { + return false; + } +} + async function installBazelTools(repoRootPath) { _log__WEBPACK_IMPORTED_MODULE_4__["log"].debug(`[bazel_tools] reading bazel tools versions from version files`); const bazeliskVersion = await readBazelToolsVersionFile(repoRootPath, '.bazeliskversion'); @@ -48128,7 +48151,9 @@ async function installBazelTools(repoRootPath) { const isBazeliskPkgInstalled = await isBazeliskInstalled(bazeliskVersion); // Test if bazel bin is available - const isBazelBinAlreadyAvailable = await isBazelBinAvailable(); // Install bazelisk if not installed + const isBazelBinAlreadyAvailable = await isBazelBinAvailable(); // Check if we need to remove bazelisk from yarn + + await tryRemoveBazeliskFromYarnGlobal(); // Install bazelisk if not installed if (!isBazeliskPkgInstalled || !isBazelBinAlreadyAvailable) { _log__WEBPACK_IMPORTED_MODULE_4__["log"].info(`[bazel_tools] installing Bazel tools`); diff --git a/packages/kbn-pm/src/utils/bazel/install_tools.ts b/packages/kbn-pm/src/utils/bazel/install_tools.ts index cee6eff317afa..b547c2bc141bd 100644 --- a/packages/kbn-pm/src/utils/bazel/install_tools.ts +++ b/packages/kbn-pm/src/utils/bazel/install_tools.ts @@ -52,6 +52,29 @@ async function isBazeliskInstalled(bazeliskVersion: string) { } } +async function tryRemoveBazeliskFromYarnGlobal() { + try { + // Check if Bazelisk is installed on the yarn global scope + const { stdout: bazeliskPkgInstallStdout } = await spawn('yarn', ['global', 'list'], { + stdio: 'pipe', + }); + + // Bazelisk was found on yarn global scope so lets remove it + if (bazeliskPkgInstallStdout.includes(`@bazel/bazelisk@`)) { + await spawn('yarn', ['global', 'remove', `@bazel/bazelisk`], { + stdio: 'pipe', + }); + + log.info(`[bazel_tools] bazelisk was installed on Yarn global packages and is now removed`); + return true; + } + + return false; + } catch { + return false; + } +} + export async function installBazelTools(repoRootPath: string) { log.debug(`[bazel_tools] reading bazel tools versions from version files`); const bazeliskVersion = await readBazelToolsVersionFile(repoRootPath, '.bazeliskversion'); @@ -66,6 +89,9 @@ export async function installBazelTools(repoRootPath: string) { // Test if bazel bin is available const isBazelBinAlreadyAvailable = await isBazelBinAvailable(); + // Check if we need to remove bazelisk from yarn + await tryRemoveBazeliskFromYarnGlobal(); + // Install bazelisk if not installed if (!isBazeliskPkgInstalled || !isBazelBinAlreadyAvailable) { log.info(`[bazel_tools] installing Bazel tools`); From 826a1ecbdbc7de14aebce15330db6a8316ada404 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Sat, 6 Feb 2021 11:52:04 +0200 Subject: [PATCH 28/44] [Search Sessions] Use sync config (#90138) * Search Sessions: Unskip Flaky Functional Test * Save all search sessions and then manage them based on their persisted state * Get default search session expiration from config * randomize sleep time * fix test * fix test * Make sure we poll, and dont persist, searches not in the context of a session * Added keepalive unit tests * fix ts * code review @lukasolson * ts * More tests, rename onScreenTimeout to completedTimeout * lint * lint * Delete async seaches * Support saved object pagination Fix get search status tests * better PersistedSearchSessionSavedObjectAttributes ts * test titles * Remove runAt from monitoring task Increase testing trackingInterval (caused bug) * support workload histograms that take into account overdue tasks * Update touched when changing session status to complete \ error * removed test * Updated management test data * Rename configs * delete tap first add comments * Use sync config in data-enhanced plugin * fix merge * fix merge * ts * code review Co-authored-by: Timothy Sullivan Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Anton Dosov Co-authored-by: Gidi Meir Morris --- x-pack/plugins/data_enhanced/server/plugin.ts | 12 +++----- .../server/search/es_search_strategy.test.ts | 30 +++++++++---------- .../server/search/es_search_strategy.ts | 3 +- .../server/search/session/monitoring_task.ts | 7 ++--- .../search/session/session_service.test.ts | 7 ++--- .../server/search/session/session_service.ts | 29 +++++++++--------- 6 files changed, 39 insertions(+), 49 deletions(-) diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts index 76235c917b139..3aaf50fbeb3e6 100644 --- a/x-pack/plugins/data_enhanced/server/plugin.ts +++ b/x-pack/plugins/data_enhanced/server/plugin.ts @@ -6,7 +6,6 @@ */ import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; -import { Observable } from 'rxjs'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { PluginSetup as DataPluginSetup, @@ -40,11 +39,11 @@ export class EnhancedDataServerPlugin implements Plugin { private readonly logger: Logger; private sessionService!: SearchSessionService; - private config$: Observable; + private config: ConfigSchema; constructor(private initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get('data_enhanced'); - this.config$ = this.initializerContext.config.create(); + this.config = this.initializerContext.config.get(); } public setup(core: CoreSetup, deps: SetupDependencies) { @@ -56,7 +55,7 @@ export class EnhancedDataServerPlugin deps.data.search.registerSearchStrategy( ENHANCED_ES_SEARCH_STRATEGY, enhancedEsSearchStrategyProvider( - this.config$, + this.config, this.initializerContext.config.legacy.globalConfig$, this.logger, usage @@ -68,10 +67,7 @@ export class EnhancedDataServerPlugin eqlSearchStrategyProvider(this.logger) ); - this.sessionService = new SearchSessionService( - this.logger, - this.initializerContext.config.create() - ); + this.sessionService = new SearchSessionService(this.logger, this.config); deps.data.__enhance({ search: { diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts index 019b94f638ca4..d529e981aaea1 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts @@ -72,13 +72,13 @@ describe('ES search strategy', () => { }, }); - const mockConfig$ = new BehaviorSubject({ + const mockConfig: any = { search: { sessions: { defaultExpiration: moment.duration('1', 'm'), }, }, - }); + }; beforeEach(() => { mockApiCaller.mockClear(); @@ -89,7 +89,7 @@ describe('ES search strategy', () => { it('returns a strategy with `search and `cancel`', async () => { const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -104,7 +104,7 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', body: { query: {} } }; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -123,7 +123,7 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', body: { query: {} } }; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -142,7 +142,7 @@ describe('ES search strategy', () => { const params = { index: 'foo-*', body: {} }; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -160,7 +160,7 @@ describe('ES search strategy', () => { const params = { index: 'foo-程', body: {} }; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -189,7 +189,7 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', body: { query: {} } }; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -209,7 +209,7 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', body: { query: {} } }; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -237,7 +237,7 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', body: { query: {} } }; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -262,7 +262,7 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', body: { query: {} } }; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -287,7 +287,7 @@ describe('ES search strategy', () => { const id = 'some_id'; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -311,7 +311,7 @@ describe('ES search strategy', () => { const id = 'some_id'; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -338,7 +338,7 @@ describe('ES search strategy', () => { const id = 'some_other_id'; const keepAlive = '1d'; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -357,7 +357,7 @@ describe('ES search strategy', () => { const id = 'some_other_id'; const keepAlive = '1d'; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 402058a776605..fc1cc63146358 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -39,7 +39,7 @@ import { ConfigSchema } from '../../config'; import { getKbnServerError, KbnServerError } from '../../../../../src/plugins/kibana_utils/server'; export const enhancedEsSearchStrategyProvider = ( - config$: Observable, + config: ConfigSchema, legacyConfig$: Observable, logger: Logger, usage?: SearchUsage @@ -60,7 +60,6 @@ export const enhancedEsSearchStrategyProvider = ( const client = esClient.asCurrentUser.asyncSearch; const search = async () => { - const config = await config$.pipe(first()).toPromise(); const params = id ? getDefaultAsyncGetParams(options) : { diff --git a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts b/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts index 75b6089cddf9b..8aa35def387b7 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { Duration } from 'moment'; import { TaskManagerSetupContract, @@ -24,14 +22,13 @@ export const SEARCH_SESSIONS_TASK_ID = `data_enhanced_${SEARCH_SESSIONS_TASK_TYP interface SearchSessionTaskDeps { taskManager: TaskManagerSetupContract; logger: Logger; - config$: Observable; + config: ConfigSchema; } -function searchSessionRunner(core: CoreSetup, { logger, config$ }: SearchSessionTaskDeps) { +function searchSessionRunner(core: CoreSetup, { logger, config }: SearchSessionTaskDeps) { return ({ taskInstance }: RunContext) => { return { async run() { - const config = await config$.pipe(first()).toPromise(); const sessionConfig = config.search.sessions; const [coreStart] = await core.getStartServices(); const internalRepo = coreStart.savedObjects.createInternalRepository([SEARCH_SESSION_TYPE]); diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts index 19679f02df0ad..24d13cf24ccfb 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { BehaviorSubject } from 'rxjs'; import { SavedObject, SavedObjectsClientContract, @@ -46,7 +45,7 @@ describe('SearchSessionService', () => { beforeEach(async () => { savedObjectsClient = savedObjectsClientMock.create(); - const config$ = new BehaviorSubject({ + const config: ConfigSchema = { search: { sessions: { enabled: true, @@ -59,13 +58,13 @@ describe('SearchSessionService', () => { management: {} as any, }, }, - }); + }; const mockLogger: any = { debug: jest.fn(), warn: jest.fn(), error: jest.fn(), }; - service = new SearchSessionService(mockLogger, config$); + service = new SearchSessionService(mockLogger, config); const coreStart = coreMock.createStart(); const mockTaskManager = taskManagerMock.createStart(); await flushPromises(); diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index 059edd5edf1de..2d0e7e519e3bd 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { CoreSetup, CoreStart, @@ -50,32 +48,33 @@ function sleep(ms: number) { } export class SearchSessionService implements ISearchSessionService { - private config!: SearchSessionsConfig; + private sessionConfig: SearchSessionsConfig; - constructor( - private readonly logger: Logger, - private readonly config$: Observable - ) {} + constructor(private readonly logger: Logger, private readonly config: ConfigSchema) { + this.sessionConfig = this.config.search.sessions; + } public setup(core: CoreSetup, deps: SetupDependencies) { registerSearchSessionsTask(core, { - config$: this.config$, + config: this.config, taskManager: deps.taskManager, logger: this.logger, }); } public async start(core: CoreStart, deps: StartDependencies) { - const configPromise = await this.config$.pipe(first()).toPromise(); - this.config = (await configPromise).search.sessions; return this.setupMonitoring(core, deps); } public stop() {} private setupMonitoring = async (core: CoreStart, deps: StartDependencies) => { - if (this.config.enabled) { - scheduleSearchSessionsTasks(deps.taskManager, this.logger, this.config.trackingInterval); + if (this.sessionConfig.enabled) { + scheduleSearchSessionsTasks( + deps.taskManager, + this.logger, + this.sessionConfig.trackingInterval + ); } }; @@ -107,7 +106,7 @@ export class SearchSessionService } catch (createError) { if ( SavedObjectsErrorHelpers.isConflictError(createError) && - retry < this.config.maxUpdateRetries + retry < this.sessionConfig.maxUpdateRetries ) { return await retryOnConflict(createError); } else { @@ -116,7 +115,7 @@ export class SearchSessionService } } else if ( SavedObjectsErrorHelpers.isConflictError(e) && - retry < this.config.maxUpdateRetries + retry < this.sessionConfig.maxUpdateRetries ) { return await retryOnConflict(e); } else { @@ -164,7 +163,7 @@ export class SearchSessionService sessionId, status: SearchSessionStatus.IN_PROGRESS, expires: new Date( - Date.now() + this.config.defaultExpiration.asMilliseconds() + Date.now() + this.sessionConfig.defaultExpiration.asMilliseconds() ).toISOString(), created: new Date().toISOString(), touched: new Date().toISOString(), From fd1d96503933a4ca8559d7b9fe3951a8e7baafc0 Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Sat, 6 Feb 2021 18:45:20 +0100 Subject: [PATCH 29/44] Unrevert "Migrations v2: don't auto-create indices + FTR/esArchiver support (#85778)" (#89992) * Revert "Revert "Migrations v2: don't auto-create indices + FTR/esArchiver support (#85778)"" This reverts commit f97958043f3e037e5e94daa432a7e71caa4b1ba7. * Fix flaky saved objects management test #89953 * If a clone target exists, wait for yellow, not green, index status * Fix test after master merge * Fix types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...orhelpers.createindexaliasnotfounderror.md | 22 ++ ...helpers.decorateindexaliasnotfounderror.md | 23 ++ ...savedobjectserrorhelpers.isgeneralerror.md | 22 ++ ...in-core-server.savedobjectserrorhelpers.md | 3 + .../src/actions/empty_kibana_index.ts | 3 +- packages/kbn-es-archiver/src/es_archiver.ts | 2 +- .../src/lib/indices/kibana_index.ts | 8 +- .../migrationsv2/actions/index.ts | 19 +- .../integration_tests/actions.test.ts | 112 +++++----- .../saved_objects/migrationsv2/model.test.ts | 202 +++++++++++++++--- .../saved_objects/migrationsv2/model.ts | 20 +- .../saved_objects/routes/bulk_create.ts | 3 +- .../server/saved_objects/routes/bulk_get.ts | 3 +- .../saved_objects/routes/bulk_update.ts | 3 +- .../server/saved_objects/routes/create.ts | 3 +- .../server/saved_objects/routes/delete.ts | 3 +- .../server/saved_objects/routes/export.ts | 4 +- src/core/server/saved_objects/routes/find.ts | 3 +- src/core/server/saved_objects/routes/get.ts | 3 +- .../server/saved_objects/routes/import.ts | 4 +- .../server/saved_objects/routes/migrate.ts | 3 +- .../routes/resolve_import_errors.ts | 5 +- .../server/saved_objects/routes/update.ts | 3 +- .../server/saved_objects/routes/utils.test.ts | 75 +++++++ src/core/server/saved_objects/routes/utils.ts | 34 ++- .../service/lib/decorate_es_error.test.ts | 21 ++ .../service/lib/decorate_es_error.ts | 6 + .../saved_objects/service/lib/errors.ts | 17 ++ .../service/lib/repository.test.js | 9 +- .../saved_objects/service/lib/repository.ts | 33 ++- src/core/server/server.api.md | 6 + .../integration_tests/doc_exists.ts | 6 +- .../integration_tests/doc_missing.ts | 6 +- .../doc_missing_and_index_read_only.ts | 12 +- .../integration_tests/index.test.ts | 13 +- .../integration_tests/lib/servers.ts | 3 - src/core/test_helpers/kbn_server.ts | 2 +- test/accessibility/apps/kibana_overview.ts | 3 +- test/api_integration/apis/home/sample_data.ts | 4 + .../apis/saved_objects/bulk_create.ts | 44 ++-- .../apis/saved_objects/bulk_get.ts | 2 +- .../apis/saved_objects/bulk_update.ts | 16 +- .../apis/saved_objects/create.ts | 48 +---- .../apis/saved_objects/delete.ts | 2 +- .../apis/saved_objects/export.ts | 2 +- .../apis/saved_objects/find.ts | 14 +- .../api_integration/apis/saved_objects/get.ts | 2 +- .../saved_objects/resolve_import_errors.ts | 51 ++++- .../apis/saved_objects/update.ts | 13 +- .../apis/saved_objects_management/find.ts | 4 +- .../apis/saved_objects_management/get.ts | 2 +- test/api_integration/apis/search/search.ts | 1 + test/api_integration/apis/telemetry/opt_in.ts | 3 + .../apis/telemetry/telemetry_local.ts | 1 + .../apis/ui_counters/ui_counters.ts | 4 + .../apis/ui_metric/ui_metric.ts | 5 + test/common/config.js | 2 - .../kibana_server/extend_es_archiver.js | 4 +- .../apps/management/_import_objects.ts | 5 +- .../apps/management/_index_pattern_filter.js | 3 +- .../apps/management/_index_patterns_empty.ts | 3 +- .../management/_mgmt_import_saved_objects.js | 3 +- .../apps/management/_test_huge_fields.js | 1 + test/functional/apps/management/index.ts | 2 - .../input_control_vis/input_control_range.ts | 2 - .../test_suites/core_plugins/applications.ts | 2 + .../test_suites/data_plugin/index_patterns.ts | 4 + .../import_warnings.ts | 7 +- .../insecure_cluster_warning.ts | 1 + .../apps/dashboard/_async_dashboard.ts | 2 + .../saved_objects_management_security.ts | 10 - .../feature_controls/security/data.json | 17 -- .../es_archives/visualize/default/data.json | 24 +-- .../reporting_without_security.config.ts | 1 - 74 files changed, 709 insertions(+), 324 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md new file mode 100644 index 0000000000000..2b897db7bba4c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [createIndexAliasNotFoundError](./kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md) + +## SavedObjectsErrorHelpers.createIndexAliasNotFoundError() method + +Signature: + +```typescript +static createIndexAliasNotFoundError(alias: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| alias | string | | + +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md new file mode 100644 index 0000000000000..c7e10fc42ead1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [decorateIndexAliasNotFoundError](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md) + +## SavedObjectsErrorHelpers.decorateIndexAliasNotFoundError() method + +Signature: + +```typescript +static decorateIndexAliasNotFoundError(error: Error, alias: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | | +| alias | string | | + +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md new file mode 100644 index 0000000000000..4b4ede2f77a7e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [isGeneralError](./kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md) + +## SavedObjectsErrorHelpers.isGeneralError() method + +Signature: + +```typescript +static isGeneralError(error: Error | DecoratedError): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | DecoratedError | | + +Returns: + +`boolean` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md index 9b69012ed5f12..2dc78f2df3a83 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md @@ -18,6 +18,7 @@ export declare class SavedObjectsErrorHelpers | [createBadRequestError(reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.createbadrequesterror.md) | static | | | [createConflictError(type, id, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md) | static | | | [createGenericNotFoundError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfounderror.md) | static | | +| [createIndexAliasNotFoundError(alias)](./kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md) | static | | | [createInvalidVersionError(versionInput)](./kibana-plugin-core-server.savedobjectserrorhelpers.createinvalidversionerror.md) | static | | | [createTooManyRequestsError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.createtoomanyrequestserror.md) | static | | | [createUnsupportedTypeError(type)](./kibana-plugin-core-server.savedobjectserrorhelpers.createunsupportedtypeerror.md) | static | | @@ -27,6 +28,7 @@ export declare class SavedObjectsErrorHelpers | [decorateEsUnavailableError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateesunavailableerror.md) | static | | | [decorateForbiddenError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateforbiddenerror.md) | static | | | [decorateGeneralError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorategeneralerror.md) | static | | +| [decorateIndexAliasNotFoundError(error, alias)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md) | static | | | [decorateNotAuthorizedError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decoratenotauthorizederror.md) | static | | | [decorateRequestEntityTooLargeError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decoraterequestentitytoolargeerror.md) | static | | | [decorateTooManyRequestsError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decoratetoomanyrequestserror.md) | static | | @@ -35,6 +37,7 @@ export declare class SavedObjectsErrorHelpers | [isEsCannotExecuteScriptError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isescannotexecutescripterror.md) | static | | | [isEsUnavailableError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isesunavailableerror.md) | static | | | [isForbiddenError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isforbiddenerror.md) | static | | +| [isGeneralError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md) | static | | | [isInvalidVersionError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isinvalidversionerror.md) | static | | | [isNotAuthorizedError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isnotauthorizederror.md) | static | | | [isNotFoundError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isnotfounderror.md) | static | | diff --git a/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts b/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts index d3494512d055a..f86865ffa6670 100644 --- a/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts +++ b/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts @@ -25,5 +25,6 @@ export async function emptyKibanaIndexAction({ await cleanKibanaIndices({ client, stats, log, kibanaPluginIds }); await migrateKibanaIndex({ client, kbnClient }); - return stats; + stats.createdIndex('.kibana'); + return stats.toJSON(); } diff --git a/packages/kbn-es-archiver/src/es_archiver.ts b/packages/kbn-es-archiver/src/es_archiver.ts index 70dc5370c5a26..b00b9fb8b3f25 100644 --- a/packages/kbn-es-archiver/src/es_archiver.ts +++ b/packages/kbn-es-archiver/src/es_archiver.ts @@ -155,7 +155,7 @@ export class EsArchiver { * @return Promise */ async emptyKibanaIndex() { - await emptyKibanaIndexAction({ + return await emptyKibanaIndexAction({ client: this.client, log: this.log, kbnClient: this.kbnClient, diff --git a/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts b/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts index 6d48c0b2bbaea..64e5626c94c8b 100644 --- a/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts +++ b/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts @@ -82,7 +82,9 @@ export async function migrateKibanaIndex({ */ async function fetchKibanaIndices(client: Client) { const resp = await client.cat.indices({ index: '.kibana*', format: 'json' }); - const isKibanaIndex = (index: string) => /^\.kibana(:?_\d*)?$/.test(index); + const isKibanaIndex = (index: string) => + /^\.kibana(:?_\d*)?$/.test(index) || + /^\.kibana(_task_manager)?_(pre)?\d+\.\d+\.\d+/.test(index); if (!Array.isArray(resp.body)) { throw new Error(`expected response to be an array ${inspect(resp.body)}`); @@ -115,7 +117,7 @@ export async function cleanKibanaIndices({ while (true) { const resp = await client.deleteByQuery( { - index: `.kibana`, + index: `.kibana,.kibana_task_manager`, body: { query: { bool: { @@ -129,7 +131,7 @@ export async function cleanKibanaIndices({ }, }, { - ignore: [409], + ignore: [404, 409], } ); diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrationsv2/actions/index.ts index fe2ce76446cb9..b22c326061f66 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.ts @@ -150,12 +150,23 @@ export const removeWriteBlock = ( .catch(catchRetryableEsClientErrors); }; -const waitForIndexStatusGreen = ( +/** + * A yellow index status means the index's primary shard is allocated and the + * index is ready for searching/indexing documents, but ES wasn't able to + * allocate the replicas. When migrations proceed with a yellow index it means + * we don't have as much data-redundancy as we could have, but waiting for + * replicas would mean that v2 migrations fail where v1 migrations would have + * succeeded. It doesn't feel like it's Kibana's job to force users to keep + * their clusters green and even if it's green when we migrate it can turn + * yellow at any point in the future. So ultimately data-redundancy is up to + * users to maintain. + */ +const waitForIndexStatusYellow = ( client: ElasticsearchClient, index: string ): TaskEither.TaskEither => () => { return client.cluster - .health({ index, wait_for_status: 'green', timeout: '30s' }) + .health({ index, wait_for_status: 'yellow', timeout: '30s' }) .then(() => { return Either.right({}); }) @@ -259,7 +270,7 @@ export const cloneIndex = ( } else { // Otherwise, wait until the target index has a 'green' status. return pipe( - waitForIndexStatusGreen(client, target), + waitForIndexStatusYellow(client, target), TaskEither.map((value) => { /** When the index status is 'green' we know that all shards were started */ return { acknowledged: true, shardsAcknowledged: true }; @@ -687,7 +698,7 @@ export const createIndex = ( } else { // Otherwise, wait until the target index has a 'green' status. return pipe( - waitForIndexStatusGreen(client, indexName), + waitForIndexStatusYellow(client, indexName), TaskEither.map(() => { /** When the index status is 'green' we know that all shards were started */ return 'create_index_succeeded'; diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts index 1bb4e57b0ac29..46cfd935f429b 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts @@ -213,12 +213,8 @@ describe('migration actions', () => { } }); it('resolves right if cloning into a new target index', async () => { + const task = cloneIndex(client, 'existing_index_with_write_block', 'clone_target_1'); expect.assertions(1); - const task = cloneIndex( - client, - 'existing_index_with_write_block', - 'clone_yellow_then_green_index_1' - ); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -229,42 +225,48 @@ describe('migration actions', () => { } `); }); - it('resolves right after waiting for index status to be green if clone target already existed', async () => { + it('resolves right after waiting for index status to be yellow if clone target already existed', async () => { expect.assertions(2); + // Create a yellow index - await client.indices.create({ - index: 'clone_yellow_then_green_index_2', - body: { - mappings: { properties: {} }, - settings: { - // Allocate 1 replica so that this index stays yellow - number_of_replicas: '1', + await client.indices + .create({ + index: 'clone_red_then_yellow_index', + timeout: '5s', + body: { + mappings: { properties: {} }, + settings: { + // Allocate 1 replica so that this index stays yellow + number_of_replicas: '1', + // Disable all shard allocation so that the index status is red + 'index.routing.allocation.enable': 'none', + }, }, - }, - }); + }) + .catch((e) => {}); // Call clone even though the index already exists const cloneIndexPromise = cloneIndex( client, 'existing_index_with_write_block', - 'clone_yellow_then_green_index_2' + 'clone_red_then_yellow_index' )(); - let indexGreen = false; + let indexYellow = false; setTimeout(() => { client.indices.putSettings({ + index: 'clone_red_then_yellow_index', body: { - index: { - number_of_replicas: 0, - }, + // Enable all shard allocation so that the index status goes yellow + 'index.routing.allocation.enable': 'all', }, }); - indexGreen = true; + indexYellow = true; }, 10); await cloneIndexPromise.then((res) => { // Assert that the promise didn't resolve before the index became green - expect(indexGreen).toBe(true); + expect(indexYellow).toBe(true); expect(res).toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -278,7 +280,7 @@ describe('migration actions', () => { }); it('resolves left index_not_found_exception if the source index does not exist', async () => { expect.assertions(1); - const task = cloneIndex(client, 'no_such_index', 'clone_yellow_then_green_index_3'); + const task = cloneIndex(client, 'no_such_index', 'clone_target_3'); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", @@ -674,7 +676,6 @@ describe('migration actions', () => { describe('waitForPickupUpdatedMappingsTask', () => { it('rejects if there are failures', async () => { - expect.assertions(1); const res = (await pickupUpdatedMappings( client, 'existing_index_with_write_block' @@ -689,7 +690,6 @@ describe('migration actions', () => { }); }); it('rejects if there is an error', async () => { - expect.assertions(1); const res = (await pickupUpdatedMappings( client, 'no_such_index' @@ -703,7 +703,6 @@ describe('migration actions', () => { `); }); it('resolves right when successful', async () => { - expect.assertions(1); const res = (await pickupUpdatedMappings( client, 'existing_index_with_docs' @@ -722,7 +721,6 @@ describe('migration actions', () => { describe('updateAndPickupMappings', () => { it('resolves right when mappings were updated and picked up', async () => { - expect.assertions(3); // Create an index without any mappings and insert documents into it await createIndex(client, 'existing_index_without_mappings', { dynamic: false as any, @@ -771,7 +769,6 @@ describe('migration actions', () => { describe('updateAliases', () => { describe('remove', () => { it('resolves left index_not_found_exception when the index does not exist', async () => { - expect.assertions(1); const task = updateAliases(client, [ { remove: { @@ -793,7 +790,6 @@ describe('migration actions', () => { }); describe('with must_exist=false', () => { it('resolves left alias_not_found_exception when alias does not exist', async () => { - expect.assertions(1); const task = updateAliases(client, [ { remove: { @@ -815,7 +811,6 @@ describe('migration actions', () => { }); describe('with must_exist=true', () => { it('resolves left alias_not_found_exception when alias does not exist on specified index', async () => { - expect.assertions(1); const task = updateAliases(client, [ { remove: { @@ -835,7 +830,6 @@ describe('migration actions', () => { `); }); it('resolves left alias_not_found_exception when alias does not exist', async () => { - expect.assertions(1); const task = updateAliases(client, [ { remove: { @@ -858,7 +852,6 @@ describe('migration actions', () => { }); describe('remove_index', () => { it('left index_not_found_exception if index does not exist', async () => { - expect.assertions(1); const task = updateAliases(client, [ { remove_index: { @@ -877,7 +870,6 @@ describe('migration actions', () => { `); }); it('left remove_index_not_a_concrete_index when remove_index targets an alias', async () => { - expect.assertions(1); const task = updateAliases(client, [ { remove_index: { @@ -899,44 +891,50 @@ describe('migration actions', () => { describe('createIndex', () => { afterAll(async () => { - await client.indices.delete({ index: 'yellow_then_green_index' }); + await client.indices.delete({ index: 'red_then_yellow_index' }); }); - it('resolves right after waiting for an index status to be green if the index already existed', async () => { + it('resolves right after waiting for an index status to be yellow if the index already existed', async () => { expect.assertions(2); - // Create a yellow index - await client.indices.create( - { - index: 'yellow_then_green_index', - body: { - mappings: { properties: {} }, - settings: { - // Allocate 1 replica so that this index stays yellow - number_of_replicas: '1', + // Create a red index + await client.indices + .create( + { + index: 'red_then_yellow_index', + timeout: '5s', + body: { + mappings: { properties: {} }, + settings: { + // Allocate 1 replica so that this index stays yellow + number_of_replicas: '1', + // Disable all shard allocation so that the index status is red + 'index.routing.allocation.enable': 'none', + }, }, }, - }, - { maxRetries: 0 /** handle retry ourselves for now */ } - ); + { maxRetries: 0 /** handle retry ourselves for now */ } + ) + .catch((e) => { + /** ignore */ + }); // Call createIndex even though the index already exists - const createIndexPromise = createIndex(client, 'yellow_then_green_index', undefined as any)(); - let indexGreen = false; + const createIndexPromise = createIndex(client, 'red_then_yellow_index', undefined as any)(); + let indexYellow = false; setTimeout(() => { client.indices.putSettings({ - index: 'yellow_then_green_index', + index: 'red_then_yellow_index', body: { - index: { - number_of_replicas: 0, - }, + // Disable all shard allocation so that the index status is red + 'index.routing.allocation.enable': 'all', }, }); - indexGreen = true; + indexYellow = true; }, 10); await createIndexPromise.then((res) => { // Assert that the promise didn't resolve before the index became green - expect(indexGreen).toBe(true); + expect(indexYellow).toBe(true); expect(res).toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -946,7 +944,6 @@ describe('migration actions', () => { }); }); it('rejects when there is an unexpected error creating the index', async () => { - expect.assertions(1); // Creating an index with the same name as an existing alias to induce // failure await expect( @@ -957,7 +954,6 @@ describe('migration actions', () => { describe('bulkOverwriteTransformedDocuments', () => { it('resolves right when documents do not yet exist in the index', async () => { - expect.assertions(1); const newDocs = ([ { _source: { title: 'doc 5' } }, { _source: { title: 'doc 6' } }, @@ -972,7 +968,6 @@ describe('migration actions', () => { `); }); it('resolves right even if there were some version_conflict_engine_exception', async () => { - expect.assertions(1); const existingDocs = ((await searchForOutdatedDocuments( client, 'existing_index_with_docs', @@ -991,7 +986,6 @@ describe('migration actions', () => { `); }); it('rejects if there are errors', async () => { - expect.assertions(1); const newDocs = ([ { _source: { title: 'doc 5' } }, { _source: { title: 'doc 6' } }, diff --git a/src/core/server/saved_objects/migrationsv2/model.test.ts b/src/core/server/saved_objects/migrationsv2/model.test.ts index 895db80983fc1..5531f847f8bb4 100644 --- a/src/core/server/saved_objects/migrationsv2/model.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model.test.ts @@ -182,6 +182,21 @@ describe('migrations v2 model', () => { versionAlias: '.kibana_7.11.0', versionIndex: '.kibana_7.11.0_001', }; + const mappingsWithUnknownType = { + properties: { + disabled_saved_object_type: { + properties: { + value: { type: 'keyword' }, + }, + }, + }, + _meta: { + migrationMappingPropertyHashes: { + disabled_saved_object_type: '7997cf5a56cc02bdc9c93361bde732b0', + }, + }, + }; + test('INIT -> OUTDATED_DOCUMENTS_SEARCH if .kibana is already pointing to the target index', () => { const res: ResponseType<'INIT'> = Either.right({ '.kibana_7.11.0_001': { @@ -189,38 +204,27 @@ describe('migrations v2 model', () => { '.kibana': {}, '.kibana_7.11.0': {}, }, - mappings: { - properties: { - disabled_saved_object_type: { - properties: { - value: { type: 'keyword' }, - }, - }, - }, - _meta: { - migrationMappingPropertyHashes: { - disabled_saved_object_type: '7997cf5a56cc02bdc9c93361bde732b0', - }, - }, - }, + mappings: mappingsWithUnknownType, settings: {}, }, }); const newState = model(initState, res); expect(newState.controlState).toEqual('OUTDATED_DOCUMENTS_SEARCH'); + // This snapshot asserts that we merge the + // migrationMappingPropertyHashes of the existing index, but we leave + // the mappings for the disabled_saved_object_type untouched. There + // might be another Kibana instance that knows about this type and + // needs these mappings in place. expect(newState.targetIndexMappings).toMatchInlineSnapshot(` Object { "_meta": Object { "migrationMappingPropertyHashes": Object { + "disabled_saved_object_type": "7997cf5a56cc02bdc9c93361bde732b0", "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", }, }, "properties": Object { - "disabled_saved_object_type": Object { - "dynamic": false, - "properties": Object {}, - }, "new_saved_object_type": Object { "properties": Object { "value": Object { @@ -271,7 +275,7 @@ describe('migrations v2 model', () => { '.kibana': {}, '.kibana_7.12.0': {}, }, - mappings: { properties: {}, _meta: { migrationMappingPropertyHashes: {} } }, + mappings: mappingsWithUnknownType, settings: {}, }, '.kibana_7.11.0_001': { @@ -288,12 +292,37 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('.kibana_7.invalid.0_001'), targetIndex: '.kibana_7.11.0_001', }); + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); }); test('INIT -> SET_SOURCE_WRITE_BLOCK when migrating from a v2 migrations index (>= 7.11.0)', () => { const res: ResponseType<'INIT'> = Either.right({ '.kibana_7.11.0_001': { aliases: { '.kibana': {}, '.kibana_7.11.0': {} }, - mappings: { properties: {}, _meta: { migrationMappingPropertyHashes: {} } }, + mappings: mappingsWithUnknownType, settings: {}, }, '.kibana_3': { @@ -319,6 +348,31 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('.kibana_7.11.0_001'), targetIndex: '.kibana_7.12.0_001', }); + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); @@ -328,7 +382,7 @@ describe('migrations v2 model', () => { aliases: { '.kibana': {}, }, - mappings: { properties: {}, _meta: { migrationMappingPropertyHashes: {} } }, + mappings: mappingsWithUnknownType, settings: {}, }, }); @@ -339,6 +393,31 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('.kibana_3'), targetIndex: '.kibana_7.11.0_001', }); + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); @@ -346,7 +425,7 @@ describe('migrations v2 model', () => { const res: ResponseType<'INIT'> = Either.right({ '.kibana': { aliases: {}, - mappings: { properties: {}, _meta: {} }, + mappings: mappingsWithUnknownType, settings: {}, }, }); @@ -357,6 +436,31 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('.kibana_pre6.5.0_001'), targetIndex: '.kibana_7.11.0_001', }); + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); @@ -366,7 +470,7 @@ describe('migrations v2 model', () => { aliases: { 'my-saved-objects': {}, }, - mappings: { properties: {}, _meta: { migrationMappingPropertyHashes: {} } }, + mappings: mappingsWithUnknownType, settings: {}, }, }); @@ -386,6 +490,31 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('my-saved-objects_3'), targetIndex: 'my-saved-objects_7.11.0_001', }); + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); @@ -395,7 +524,7 @@ describe('migrations v2 model', () => { aliases: { 'my-saved-objects': {}, }, - mappings: { properties: {}, _meta: { migrationMappingPropertyHashes: {} } }, + mappings: mappingsWithUnknownType, settings: {}, }, }); @@ -416,6 +545,31 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('my-saved-objects_7.11.0'), targetIndex: 'my-saved-objects_7.12.0_001', }); + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); diff --git a/src/core/server/saved_objects/migrationsv2/model.ts b/src/core/server/saved_objects/migrationsv2/model.ts index c9a3aa25db4c1..6f915df9dd958 100644 --- a/src/core/server/saved_objects/migrationsv2/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model.ts @@ -60,13 +60,13 @@ function throwBadResponse(state: State, res: any): never { * Merge the _meta.migrationMappingPropertyHashes mappings of an index with * the given target mappings. * - * @remarks Mapping updates are commutative (deeply merged) by Elasticsearch, - * except for the _meta key. The source index we're migrating from might - * contain documents created by a plugin that is disabled in the Kibana - * instance performing this migration. We merge the - * _meta.migrationMappingPropertyHashes mappings from the source index into - * the targetMappings to ensure that any `migrationPropertyHashes` for - * disabled plugins aren't lost. + * @remarks When another instance already completed a migration, the existing + * target index might contain documents and mappings created by a plugin that + * is disabled in the current Kibana instance performing this migration. + * Mapping updates are commutative (deeply merged) by Elasticsearch, except + * for the `_meta` key. By merging the `_meta.migrationMappingPropertyHashes` + * mappings from the existing target index index into the targetMappings we + * ensure that any `migrationPropertyHashes` for disabled plugins aren't lost. * * Right now we don't use these `migrationPropertyHashes` but it could be used * in the future to detect if mappings were changed. If mappings weren't @@ -209,7 +209,7 @@ export const model = (currentState: State, resW: ResponseType): // index sourceIndex: Option.none, targetIndex: `${stateP.indexPrefix}_${stateP.kibanaVersion}_001`, - targetIndexMappings: disableUnknownTypeMappingFields( + targetIndexMappings: mergeMigrationMappingPropertyHashes( stateP.targetIndexMappings, indices[aliases[stateP.currentAlias]].mappings ), @@ -242,7 +242,7 @@ export const model = (currentState: State, resW: ResponseType): controlState: 'SET_SOURCE_WRITE_BLOCK', sourceIndex: Option.some(source) as Option.Some, targetIndex: target, - targetIndexMappings: mergeMigrationMappingPropertyHashes( + targetIndexMappings: disableUnknownTypeMappingFields( stateP.targetIndexMappings, indices[source].mappings ), @@ -279,7 +279,7 @@ export const model = (currentState: State, resW: ResponseType): controlState: 'LEGACY_SET_WRITE_BLOCK', sourceIndex: Option.some(legacyReindexTarget) as Option.Some, targetIndex: target, - targetIndexMappings: mergeMigrationMappingPropertyHashes( + targetIndexMappings: disableUnknownTypeMappingFields( stateP.targetIndexMappings, indices[stateP.legacyIndex].mappings ), diff --git a/src/core/server/saved_objects/routes/bulk_create.ts b/src/core/server/saved_objects/routes/bulk_create.ts index 7574f26979ab1..344a0d151cfb9 100644 --- a/src/core/server/saved_objects/routes/bulk_create.ts +++ b/src/core/server/saved_objects/routes/bulk_create.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -44,7 +45,7 @@ export const registerBulkCreateRoute = (router: IRouter, { coreUsageData }: Rout ), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { overwrite } = req.query; const usageStatsClient = coreUsageData.getClient(); diff --git a/src/core/server/saved_objects/routes/bulk_get.ts b/src/core/server/saved_objects/routes/bulk_get.ts index 2484daf2ea875..3838e4d3b3c8e 100644 --- a/src/core/server/saved_objects/routes/bulk_get.ts +++ b/src/core/server/saved_objects/routes/bulk_get.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -28,7 +29,7 @@ export const registerBulkGetRoute = (router: IRouter, { coreUsageData }: RouteDe ), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const usageStatsClient = coreUsageData.getClient(); usageStatsClient.incrementSavedObjectsBulkGet({ request: req }).catch(() => {}); diff --git a/src/core/server/saved_objects/routes/bulk_update.ts b/src/core/server/saved_objects/routes/bulk_update.ts index 1a717f330d4c2..de47ab9c59611 100644 --- a/src/core/server/saved_objects/routes/bulk_update.ts +++ b/src/core/server/saved_objects/routes/bulk_update.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -39,7 +40,7 @@ export const registerBulkUpdateRoute = (router: IRouter, { coreUsageData }: Rout ), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const usageStatsClient = coreUsageData.getClient(); usageStatsClient.incrementSavedObjectsBulkUpdate({ request: req }).catch(() => {}); diff --git a/src/core/server/saved_objects/routes/create.ts b/src/core/server/saved_objects/routes/create.ts index db68b2f87d577..2fa7acfb6cab6 100644 --- a/src/core/server/saved_objects/routes/create.ts +++ b/src/core/server/saved_objects/routes/create.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -43,7 +44,7 @@ export const registerCreateRoute = (router: IRouter, { coreUsageData }: RouteDep }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { type, id } = req.params; const { overwrite } = req.query; const { diff --git a/src/core/server/saved_objects/routes/delete.ts b/src/core/server/saved_objects/routes/delete.ts index dbbb0faf35c31..609ce2692c777 100644 --- a/src/core/server/saved_objects/routes/delete.ts +++ b/src/core/server/saved_objects/routes/delete.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -28,7 +29,7 @@ export const registerDeleteRoute = (router: IRouter, { coreUsageData }: RouteDep }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { type, id } = req.params; const { force } = req.query; diff --git a/src/core/server/saved_objects/routes/export.ts b/src/core/server/saved_objects/routes/export.ts index 76e422d24732e..fa5517303f18f 100644 --- a/src/core/server/saved_objects/routes/export.ts +++ b/src/core/server/saved_objects/routes/export.ts @@ -18,7 +18,7 @@ import { SavedObjectsExportByObjectOptions, SavedObjectsExportError, } from '../export'; -import { validateTypes, validateObjects } from './utils'; +import { validateTypes, validateObjects, catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { config: SavedObjectConfig; @@ -163,7 +163,7 @@ export const registerExportRoute = ( }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const cleaned = cleanOptions(req.body); const supportedTypes = context.core.savedObjects.typeRegistry .getImportableAndExportableTypes() diff --git a/src/core/server/saved_objects/routes/find.ts b/src/core/server/saved_objects/routes/find.ts index b9ad6ce15df2b..6ba23747cf374 100644 --- a/src/core/server/saved_objects/routes/find.ts +++ b/src/core/server/saved_objects/routes/find.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -49,7 +50,7 @@ export const registerFindRoute = (router: IRouter, { coreUsageData }: RouteDepen }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const query = req.query; const namespaces = diff --git a/src/core/server/saved_objects/routes/get.ts b/src/core/server/saved_objects/routes/get.ts index 121cb82155b6e..f28822d95d814 100644 --- a/src/core/server/saved_objects/routes/get.ts +++ b/src/core/server/saved_objects/routes/get.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -25,7 +26,7 @@ export const registerGetRoute = (router: IRouter, { coreUsageData }: RouteDepend }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { type, id } = req.params; const usageStatsClient = coreUsageData.getClient(); diff --git a/src/core/server/saved_objects/routes/import.ts b/src/core/server/saved_objects/routes/import.ts index 81220f897f36b..e84c638d3ec99 100644 --- a/src/core/server/saved_objects/routes/import.ts +++ b/src/core/server/saved_objects/routes/import.ts @@ -13,7 +13,7 @@ import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; import { SavedObjectConfig } from '../saved_objects_config'; import { SavedObjectsImportError } from '../import'; -import { createSavedObjectsStreamFromNdJson } from './utils'; +import { catchAndReturnBoomErrors, createSavedObjectsStreamFromNdJson } from './utils'; interface RouteDependencies { config: SavedObjectConfig; @@ -61,7 +61,7 @@ export const registerImportRoute = ( }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { overwrite, createNewCopies } = req.query; const usageStatsClient = coreUsageData.getClient(); diff --git a/src/core/server/saved_objects/routes/migrate.ts b/src/core/server/saved_objects/routes/migrate.ts index 19c6e3d99d6c2..404074124c92b 100644 --- a/src/core/server/saved_objects/routes/migrate.ts +++ b/src/core/server/saved_objects/routes/migrate.ts @@ -8,6 +8,7 @@ import { IRouter } from '../../http'; import { IKibanaMigrator } from '../migrations'; +import { catchAndReturnBoomErrors } from './utils'; export const registerMigrateRoute = ( router: IRouter, @@ -21,7 +22,7 @@ export const registerMigrateRoute = ( tags: ['access:migrateSavedObjects'], }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const migrator = await migratorPromise; await migrator.runMigrations({ rerun: true }); return res.ok({ diff --git a/src/core/server/saved_objects/routes/resolve_import_errors.ts b/src/core/server/saved_objects/routes/resolve_import_errors.ts index 682b583f6a791..2a664328d4df2 100644 --- a/src/core/server/saved_objects/routes/resolve_import_errors.ts +++ b/src/core/server/saved_objects/routes/resolve_import_errors.ts @@ -13,8 +13,7 @@ import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; import { SavedObjectConfig } from '../saved_objects_config'; import { SavedObjectsImportError } from '../import'; -import { createSavedObjectsStreamFromNdJson } from './utils'; - +import { catchAndReturnBoomErrors, createSavedObjectsStreamFromNdJson } from './utils'; interface RouteDependencies { config: SavedObjectConfig; coreUsageData: CoreUsageDataSetup; @@ -69,7 +68,7 @@ export const registerResolveImportErrorsRoute = ( }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { createNewCopies } = req.query; const usageStatsClient = coreUsageData.getClient(); diff --git a/src/core/server/saved_objects/routes/update.ts b/src/core/server/saved_objects/routes/update.ts index 857973c5ae006..cb605dac56777 100644 --- a/src/core/server/saved_objects/routes/update.ts +++ b/src/core/server/saved_objects/routes/update.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -38,7 +39,7 @@ export const registerUpdateRoute = (router: IRouter, { coreUsageData }: RouteDep }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { type, id } = req.params; const { attributes, version, references } = req.body; const options = { version, references }; diff --git a/src/core/server/saved_objects/routes/utils.test.ts b/src/core/server/saved_objects/routes/utils.test.ts index a24a4a1b51f6a..623d2dcc71fac 100644 --- a/src/core/server/saved_objects/routes/utils.test.ts +++ b/src/core/server/saved_objects/routes/utils.test.ts @@ -9,6 +9,15 @@ import { createSavedObjectsStreamFromNdJson, validateTypes, validateObjects } from './utils'; import { Readable } from 'stream'; import { createPromiseFromStreams, createConcatStream } from '@kbn/utils'; +import { catchAndReturnBoomErrors } from './utils'; +import Boom from '@hapi/boom'; +import { + KibanaRequest, + RequestHandler, + RequestHandlerContext, + KibanaResponseFactory, + kibanaResponseFactory, +} from '../../'; async function readStreamToCompletion(stream: Readable) { return createPromiseFromStreams([stream, createConcatStream([])]); @@ -143,3 +152,69 @@ describe('validateObjects', () => { ).toBeUndefined(); }); }); + +describe('catchAndReturnBoomErrors', () => { + let context: RequestHandlerContext; + let request: KibanaRequest; + let response: KibanaResponseFactory; + + const createHandler = (handler: () => any): RequestHandler => () => { + return handler(); + }; + + beforeEach(() => { + context = {} as any; + request = {} as any; + response = kibanaResponseFactory; + }); + + it('should pass-though call parameters to the handler', async () => { + const handler = jest.fn(); + const wrapped = catchAndReturnBoomErrors(handler); + await wrapped(context, request, response); + expect(handler).toHaveBeenCalledWith(context, request, response); + }); + + it('should pass-though result from the handler', async () => { + const handler = createHandler(() => { + return 'handler-response'; + }); + const wrapped = catchAndReturnBoomErrors(handler); + const result = await wrapped(context, request, response); + expect(result).toBe('handler-response'); + }); + + it('should intercept and convert thrown Boom errors', async () => { + const handler = createHandler(() => { + throw Boom.notFound('not there'); + }); + const wrapped = catchAndReturnBoomErrors(handler); + const result = await wrapped(context, request, response); + expect(result.status).toBe(404); + expect(result.payload).toEqual({ + error: 'Not Found', + message: 'not there', + statusCode: 404, + }); + }); + + it('should re-throw non-Boom errors', async () => { + const handler = createHandler(() => { + throw new Error('something went bad'); + }); + const wrapped = catchAndReturnBoomErrors(handler); + await expect(wrapped(context, request, response)).rejects.toMatchInlineSnapshot( + `[Error: something went bad]` + ); + }); + + it('should re-throw Boom internal/500 errors', async () => { + const handler = createHandler(() => { + throw Boom.internal(); + }); + const wrapped = catchAndReturnBoomErrors(handler); + await expect(wrapped(context, request, response)).rejects.toMatchInlineSnapshot( + `[Error: Internal Server Error]` + ); + }); +}); diff --git a/src/core/server/saved_objects/routes/utils.ts b/src/core/server/saved_objects/routes/utils.ts index fc784ac80ed8d..e933badfe80fe 100644 --- a/src/core/server/saved_objects/routes/utils.ts +++ b/src/core/server/saved_objects/routes/utils.ts @@ -7,7 +7,11 @@ */ import { Readable } from 'stream'; -import { SavedObject, SavedObjectsExportResultDetails } from 'src/core/server'; +import { + RequestHandlerWrapper, + SavedObject, + SavedObjectsExportResultDetails, +} from 'src/core/server'; import { createSplitStream, createMapStream, @@ -16,6 +20,7 @@ import { createListStream, createConcatStream, } from '@kbn/utils'; +import Boom from '@hapi/boom'; export async function createSavedObjectsStreamFromNdJson(ndJsonStream: Readable) { const savedObjects = await createPromiseFromStreams([ @@ -52,3 +57,30 @@ export function validateObjects( .join(', ')}`; } } + +/** + * Catches errors thrown by saved object route handlers and returns an error + * with the payload and statusCode of the boom error. + * + * This is very close to the core `router.handleLegacyErrors` except that it + * throws internal errors (statusCode: 500) so that the internal error's + * message get logged by Core. + * + * TODO: Remove once https://github.com/elastic/kibana/issues/65291 is fixed. + */ +export const catchAndReturnBoomErrors: RequestHandlerWrapper = (handler) => { + return async (context, request, response) => { + try { + return await handler(context, request, response); + } catch (e) { + if (Boom.isBoom(e) && e.output.statusCode !== 500) { + return response.customError({ + body: e.output.payload, + statusCode: e.output.statusCode, + headers: e.output.headers as { [key: string]: string }, + }); + } + throw e; + } + }; +}; diff --git a/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts b/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts index 717fd5fc5ab92..32f12193306e7 100644 --- a/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts +++ b/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts @@ -109,6 +109,27 @@ describe('savedObjectsClient/decorateEsError', () => { expect(SavedObjectsErrorHelpers.isNotFoundError(genericError)).toBe(true); }); + it('if saved objects index does not exist makes NotFound a SavedObjectsClient/generalError', () => { + const error = new esErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ + statusCode: 404, + body: { + error: { + reason: + 'no such index [.kibana_8.0.0] and [require_alias] request flag is [true] and [.kibana_8.0.0] is not an alias', + }, + }, + }) + ); + expect(SavedObjectsErrorHelpers.isGeneralError(error)).toBe(false); + const genericError = decorateEsError(error); + expect(genericError.message).toEqual( + `Saved object index alias [.kibana_8.0.0] not found: Response Error` + ); + expect(genericError.output.statusCode).toBe(500); + expect(SavedObjectsErrorHelpers.isGeneralError(error)).toBe(true); + }); + it('makes BadRequest a SavedObjectsClient/BadRequest error', () => { const error = new esErrors.ResponseError( elasticsearchClientMock.createApiResponse({ statusCode: 400 }) diff --git a/src/core/server/saved_objects/service/lib/decorate_es_error.ts b/src/core/server/saved_objects/service/lib/decorate_es_error.ts index 59a9210ff5130..e1aa1ab2f956d 100644 --- a/src/core/server/saved_objects/service/lib/decorate_es_error.ts +++ b/src/core/server/saved_objects/service/lib/decorate_es_error.ts @@ -63,6 +63,12 @@ export function decorateEsError(error: EsErrors) { } if (responseErrors.isNotFound(error.statusCode)) { + const match = error?.meta?.body?.error?.reason?.match( + /no such index \[(.+)\] and \[require_alias\] request flag is \[true\] and \[.+\] is not an alias/ + ); + if (match?.length > 0) { + return SavedObjectsErrorHelpers.decorateIndexAliasNotFoundError(error, match[1]); + } return SavedObjectsErrorHelpers.createGenericNotFoundError(); } diff --git a/src/core/server/saved_objects/service/lib/errors.ts b/src/core/server/saved_objects/service/lib/errors.ts index 2495679a2f8c2..581145c7c09d1 100644 --- a/src/core/server/saved_objects/service/lib/errors.ts +++ b/src/core/server/saved_objects/service/lib/errors.ts @@ -135,6 +135,19 @@ export class SavedObjectsErrorHelpers { return decorate(Boom.notFound(), CODE_NOT_FOUND, 404); } + public static createIndexAliasNotFoundError(alias: string) { + return SavedObjectsErrorHelpers.decorateIndexAliasNotFoundError(Boom.internal(), alias); + } + + public static decorateIndexAliasNotFoundError(error: Error, alias: string) { + return decorate( + error, + CODE_GENERAL_ERROR, + 500, + `Saved object index alias [${alias}] not found` + ); + } + public static isNotFoundError(error: Error | DecoratedError) { return isSavedObjectsClientError(error) && error[code] === CODE_NOT_FOUND; } @@ -185,4 +198,8 @@ export class SavedObjectsErrorHelpers { public static decorateGeneralError(error: Error, reason?: string) { return decorate(error, CODE_GENERAL_ERROR, 500, reason); } + + public static isGeneralError(error: Error | DecoratedError) { + return isSavedObjectsClientError(error) && error[code] === CODE_GENERAL_ERROR; + } } diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 0a1c18c01ad82..aac508fb5b909 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -18,6 +18,7 @@ import { DocumentMigrator } from '../../migrations/core/document_migrator'; import { mockKibanaMigrator } from '../../migrations/kibana/kibana_migrator.mock'; import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; import { esKuery } from '../../es_query'; +import { errors as EsErrors } from '@elastic/elasticsearch'; const { nodeTypes } = esKuery; jest.mock('./search_dsl/search_dsl', () => ({ getSearchDsl: jest.fn() })); @@ -4341,8 +4342,14 @@ describe('SavedObjectsRepository', () => { }); it(`throws when ES is unable to find the document during update`, async () => { + const notFoundError = new EsErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ + statusCode: 404, + body: { error: { type: 'es_type', reason: 'es_reason' } }, + }) + ); client.update.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + elasticsearchClientMock.createErrorTransportRequestPromise(notFoundError) ); await expectNotFoundError(type, id); expect(client.update).toHaveBeenCalledTimes(1); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index a662a374b063e..fcd72aa4326a2 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -299,6 +299,7 @@ export class SavedObjectsRepository { refresh, body: raw._source, ...(overwrite && version ? decodeRequestVersion(version) : {}), + require_alias: true, }; const { body } = @@ -469,6 +470,7 @@ export class SavedObjectsRepository { const bulkResponse = bulkCreateParams.length ? await this.client.bulk({ refresh, + require_alias: true, body: bulkCreateParams, }) : undefined; @@ -1117,8 +1119,8 @@ export class SavedObjectsRepository { ...(Array.isArray(references) && { references }), }; - const { body, statusCode } = await this.client.update( - { + const { body } = await this.client + .update({ id: this._serializer.generateRawId(namespace, type, id), index: this.getIndexForType(type), ...getExpectedVersionProperties(version, preflightResult), @@ -1128,14 +1130,15 @@ export class SavedObjectsRepository { doc, }, _source_includes: ['namespace', 'namespaces', 'originId'], - }, - { ignore: [404] } - ); - - if (statusCode === 404) { - // see "404s from missing index" above - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } + require_alias: true, + }) + .catch((err) => { + if (SavedObjectsErrorHelpers.isNotFoundError(err)) { + // see "404s from missing index" above + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + throw err; + }); const { originId } = body.get._source; let namespaces = []; @@ -1496,6 +1499,7 @@ export class SavedObjectsRepository { refresh, body: bulkUpdateParams, _source_includes: ['originId'], + require_alias: true, }) : undefined; @@ -1712,6 +1716,7 @@ export class SavedObjectsRepository { id: raw._id, index: this.getIndexForType(type), refresh, + require_alias: true, _source: 'true', body: { script: { @@ -1933,12 +1938,18 @@ export class SavedObjectsRepository { } } -function getBulkOperationError(error: { type: string; reason?: string }, type: string, id: string) { +function getBulkOperationError( + error: { type: string; reason?: string; index?: string }, + type: string, + id: string +) { switch (error.type) { case 'version_conflict_engine_exception': return errorContent(SavedObjectsErrorHelpers.createConflictError(type, id)); case 'document_missing_exception': return errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)); + case 'index_not_found_exception': + return errorContent(SavedObjectsErrorHelpers.createIndexAliasNotFoundError(error.index!)); default: return { message: error.reason || JSON.stringify(error), diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 40a12290be31b..f3191c5625f8d 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2335,6 +2335,8 @@ export class SavedObjectsErrorHelpers { // (undocumented) static createGenericNotFoundError(type?: string | null, id?: string | null): DecoratedError; // (undocumented) + static createIndexAliasNotFoundError(alias: string): DecoratedError; + // (undocumented) static createInvalidVersionError(versionInput?: string): DecoratedError; // (undocumented) static createTooManyRequestsError(type: string, id: string): DecoratedError; @@ -2353,6 +2355,8 @@ export class SavedObjectsErrorHelpers { // (undocumented) static decorateGeneralError(error: Error, reason?: string): DecoratedError; // (undocumented) + static decorateIndexAliasNotFoundError(error: Error, alias: string): DecoratedError; + // (undocumented) static decorateNotAuthorizedError(error: Error, reason?: string): DecoratedError; // (undocumented) static decorateRequestEntityTooLargeError(error: Error, reason?: string): DecoratedError; @@ -2369,6 +2373,8 @@ export class SavedObjectsErrorHelpers { // (undocumented) static isForbiddenError(error: Error | DecoratedError): boolean; // (undocumented) + static isGeneralError(error: Error | DecoratedError): boolean; + // (undocumented) static isInvalidVersionError(error: Error | DecoratedError): boolean; // (undocumented) static isNotAuthorizedError(error: Error | DecoratedError): boolean; diff --git a/src/core/server/ui_settings/integration_tests/doc_exists.ts b/src/core/server/ui_settings/integration_tests/doc_exists.ts index b02f2ec9c7610..86a9a24fab6de 100644 --- a/src/core/server/ui_settings/integration_tests/doc_exists.ts +++ b/src/core/server/ui_settings/integration_tests/doc_exists.ts @@ -8,7 +8,7 @@ import { getServices, chance } from './lib'; -export function docExistsSuite() { +export const docExistsSuite = (savedObjectsIndex: string) => () => { async function setup(options: any = {}) { const { initialSettings } = options; @@ -16,7 +16,7 @@ export function docExistsSuite() { // delete the kibana index to ensure we start fresh await callCluster('deleteByQuery', { - index: kbnServer.config.get('kibana.index'), + index: savedObjectsIndex, body: { conflicts: 'proceed', query: { match_all: {} }, @@ -212,4 +212,4 @@ export function docExistsSuite() { }); }); }); -} +}; diff --git a/src/core/server/ui_settings/integration_tests/doc_missing.ts b/src/core/server/ui_settings/integration_tests/doc_missing.ts index ef3b3928e0d9c..9fa3e4c1cfe78 100644 --- a/src/core/server/ui_settings/integration_tests/doc_missing.ts +++ b/src/core/server/ui_settings/integration_tests/doc_missing.ts @@ -8,7 +8,7 @@ import { getServices, chance } from './lib'; -export function docMissingSuite() { +export const docMissingSuite = (savedObjectsIndex: string) => () => { // ensure the kibana index has no documents beforeEach(async () => { const { kbnServer, callCluster } = getServices(); @@ -22,7 +22,7 @@ export function docMissingSuite() { // delete all docs from kibana index to ensure savedConfig is not found await callCluster('deleteByQuery', { - index: kbnServer.config.get('kibana.index'), + index: savedObjectsIndex, body: { query: { match_all: {} }, }, @@ -136,4 +136,4 @@ export function docMissingSuite() { }); }); }); -} +}; diff --git a/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts b/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts index f3a02cfe176e9..78fdab7eb8c5d 100644 --- a/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts +++ b/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts @@ -8,7 +8,7 @@ import { getServices, chance } from './lib'; -export function docMissingAndIndexReadOnlySuite() { +export const docMissingAndIndexReadOnlySuite = (savedObjectsIndex: string) => () => { // ensure the kibana index has no documents beforeEach(async () => { const { kbnServer, callCluster } = getServices(); @@ -22,7 +22,7 @@ export function docMissingAndIndexReadOnlySuite() { // delete all docs from kibana index to ensure savedConfig is not found await callCluster('deleteByQuery', { - index: kbnServer.config.get('kibana.index'), + index: savedObjectsIndex, body: { query: { match_all: {} }, }, @@ -30,7 +30,7 @@ export function docMissingAndIndexReadOnlySuite() { // set the index to read only await callCluster('indices.putSettings', { - index: kbnServer.config.get('kibana.index'), + index: savedObjectsIndex, body: { index: { blocks: { @@ -42,11 +42,11 @@ export function docMissingAndIndexReadOnlySuite() { }); afterEach(async () => { - const { kbnServer, callCluster } = getServices(); + const { callCluster } = getServices(); // disable the read only block await callCluster('indices.putSettings', { - index: kbnServer.config.get('kibana.index'), + index: savedObjectsIndex, body: { index: { blocks: { @@ -142,4 +142,4 @@ export function docMissingAndIndexReadOnlySuite() { }); }); }); -} +}; diff --git a/src/core/server/ui_settings/integration_tests/index.test.ts b/src/core/server/ui_settings/integration_tests/index.test.ts index 184c75d88f3b8..6e6c357e6cccc 100644 --- a/src/core/server/ui_settings/integration_tests/index.test.ts +++ b/src/core/server/ui_settings/integration_tests/index.test.ts @@ -6,20 +6,25 @@ * Side Public License, v 1. */ +import { Env } from '@kbn/config'; +import { REPO_ROOT } from '@kbn/dev-utils'; +import { getEnvOptions } from '@kbn/config/target/mocks'; import { startServers, stopServers } from './lib'; - import { docExistsSuite } from './doc_exists'; import { docMissingSuite } from './doc_missing'; import { docMissingAndIndexReadOnlySuite } from './doc_missing_and_index_read_only'; +const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; +const savedObjectIndex = `.kibana_${kibanaVersion}_001`; + describe('uiSettings/routes', function () { jest.setTimeout(10000); beforeAll(startServers); /* eslint-disable jest/valid-describe */ - describe('doc missing', docMissingSuite); - describe('doc missing and index readonly', docMissingAndIndexReadOnlySuite); - describe('doc exists', docExistsSuite); + describe('doc missing', docMissingSuite(savedObjectIndex)); + describe('doc missing and index readonly', docMissingAndIndexReadOnlySuite(savedObjectIndex)); + describe('doc exists', docExistsSuite(savedObjectIndex)); /* eslint-enable jest/valid-describe */ afterAll(stopServers); }); diff --git a/src/core/server/ui_settings/integration_tests/lib/servers.ts b/src/core/server/ui_settings/integration_tests/lib/servers.ts index 1bea45da51af9..87176bed5de11 100644 --- a/src/core/server/ui_settings/integration_tests/lib/servers.ts +++ b/src/core/server/ui_settings/integration_tests/lib/servers.ts @@ -37,9 +37,6 @@ export async function startServers() { adjustTimeout: (t) => jest.setTimeout(t), settings: { kbn: { - migrations: { - enableV2: false, - }, uiSettings: { overrides: { foo: 'bar', diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts index cf5589fecdf43..011ba67a05512 100644 --- a/src/core/test_helpers/kbn_server.ts +++ b/src/core/test_helpers/kbn_server.ts @@ -40,7 +40,7 @@ const DEFAULTS_SETTINGS = { }, logging: { silent: true }, plugins: {}, - migrations: { skip: true }, + migrations: { skip: false }, }; const DEFAULT_SETTINGS_WITH_CORE_PLUGINS = { diff --git a/test/accessibility/apps/kibana_overview.ts b/test/accessibility/apps/kibana_overview.ts index a6ecd491f169f..8481e2bf334aa 100644 --- a/test/accessibility/apps/kibana_overview.ts +++ b/test/accessibility/apps/kibana_overview.ts @@ -16,7 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); before(async () => { - await esArchiver.load('empty_kibana'); + await esArchiver.emptyKibanaIndex(); await PageObjects.common.navigateToApp('kibanaOverview'); }); @@ -25,7 +25,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { useActualUrl: true, }); await PageObjects.home.removeSampleDataSet('flights'); - await esArchiver.unload('empty_kibana'); }); it('Getting started view', async () => { diff --git a/test/api_integration/apis/home/sample_data.ts b/test/api_integration/apis/home/sample_data.ts index 64ef11167b333..b889b59fdaf32 100644 --- a/test/api_integration/apis/home/sample_data.ts +++ b/test/api_integration/apis/home/sample_data.ts @@ -11,11 +11,15 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); const es = getService('es'); const MILLISECOND_IN_WEEK = 1000 * 60 * 60 * 24 * 7; describe('sample data apis', () => { + before(async () => { + await esArchiver.emptyKibanaIndex(); + }); describe('list', () => { it('should return list of sample data sets with installed status', async () => { const resp = await supertest.get(`/api/sample_data`).set('kbn-xsrf', 'kibana').expect(200); diff --git a/test/api_integration/apis/saved_objects/bulk_create.ts b/test/api_integration/apis/saved_objects/bulk_create.ts index 6239b930434af..57b7ff0935f58 100644 --- a/test/api_integration/apis/saved_objects/bulk_create.ts +++ b/test/api_integration/apis/saved_objects/bulk_create.ts @@ -97,10 +97,11 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); - it('should return 200 with individual responses', async () => + it('should return 200 with errors', async () => { + await new Promise((resolve) => setTimeout(resolve, 2000)); await supertest .post('/api/saved_objects/_bulk_create') .send(BULK_REQUESTS) @@ -109,38 +110,27 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.body).to.eql({ saved_objects: [ { - type: 'visualization', - id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - updated_at: resp.body.saved_objects[0].updated_at, - version: resp.body.saved_objects[0].version, - attributes: { - title: 'An existing visualization', - }, - references: [], - namespaces: ['default'], - migrationVersion: { - visualization: resp.body.saved_objects[0].migrationVersion.visualization, + id: BULK_REQUESTS[0].id, + type: BULK_REQUESTS[0].type, + error: { + error: 'Internal Server Error', + message: 'An internal server error occurred', + statusCode: 500, }, - coreMigrationVersion: KIBANA_VERSION, // updated from 1.2.3 to the latest kibana version }, { - type: 'dashboard', - id: 'a01b2f57-fcfd-4864-b735-09e28f0d815e', - updated_at: resp.body.saved_objects[1].updated_at, - version: resp.body.saved_objects[1].version, - attributes: { - title: 'A great new dashboard', - }, - references: [], - namespaces: ['default'], - migrationVersion: { - dashboard: resp.body.saved_objects[1].migrationVersion.dashboard, + id: BULK_REQUESTS[1].id, + type: BULK_REQUESTS[1].type, + error: { + error: 'Internal Server Error', + message: 'An internal server error occurred', + statusCode: 500, }, - coreMigrationVersion: KIBANA_VERSION, }, ], }); - })); + }); + }); }); }); } diff --git a/test/api_integration/apis/saved_objects/bulk_get.ts b/test/api_integration/apis/saved_objects/bulk_get.ts index e9514d7d55457..77f84dee25ded 100644 --- a/test/api_integration/apis/saved_objects/bulk_get.ts +++ b/test/api_integration/apis/saved_objects/bulk_get.ts @@ -108,7 +108,7 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); it('should return 200 with individual responses', async () => diff --git a/test/api_integration/apis/saved_objects/bulk_update.ts b/test/api_integration/apis/saved_objects/bulk_update.ts index d9e3c27869591..a5f5262196346 100644 --- a/test/api_integration/apis/saved_objects/bulk_update.ts +++ b/test/api_integration/apis/saved_objects/bulk_update.ts @@ -235,10 +235,10 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); - it('should return generic 404', async () => { + it('should return 200 with errors', async () => { const response = await supertest .put(`/api/saved_objects/_bulk_update`) .send([ @@ -267,9 +267,9 @@ export default function ({ getService }: FtrProviderContext) { id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', type: 'visualization', error: { - statusCode: 404, - error: 'Not Found', - message: 'Saved object [visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab] not found', + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', }, }); @@ -277,9 +277,9 @@ export default function ({ getService }: FtrProviderContext) { id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', type: 'dashboard', error: { - statusCode: 404, - error: 'Not Found', - message: 'Saved object [dashboard/be3733a0-9efe-11e7-acb3-3dab96693fab] not found', + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', }, }); }); diff --git a/test/api_integration/apis/saved_objects/create.ts b/test/api_integration/apis/saved_objects/create.ts index 355e5df1f1895..de31b621a6480 100644 --- a/test/api_integration/apis/saved_objects/create.ts +++ b/test/api_integration/apis/saved_objects/create.ts @@ -83,10 +83,10 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); - it('should return 200 and create kibana index', async () => { + it('should return 500 and not auto-create saved objects index', async () => { await supertest .post(`/api/saved_objects/visualization`) .send({ @@ -94,50 +94,16 @@ export default function ({ getService }: FtrProviderContext) { title: 'My favorite vis', }, }) - .expect(200) + .expect(500) .then((resp) => { - // loose uuid validation - expect(resp.body) - .to.have.property('id') - .match(/^[0-9a-f-]{36}$/); - - // loose ISO8601 UTC time with milliseconds validation - expect(resp.body) - .to.have.property('updated_at') - .match(/^[\d-]{10}T[\d:\.]{12}Z$/); - expect(resp.body).to.eql({ - id: resp.body.id, - type: 'visualization', - migrationVersion: resp.body.migrationVersion, - coreMigrationVersion: KIBANA_VERSION, - updated_at: resp.body.updated_at, - version: resp.body.version, - attributes: { - title: 'My favorite vis', - }, - references: [], - namespaces: ['default'], + error: 'Internal Server Error', + message: 'An internal server error occurred.', + statusCode: 500, }); - expect(resp.body.migrationVersion).to.be.ok(); }); - expect((await es.indices.exists({ index: '.kibana' })).body).to.be(true); - }); - - it('result should have the latest coreMigrationVersion', async () => { - await supertest - .post(`/api/saved_objects/visualization`) - .send({ - attributes: { - title: 'My favorite vis', - }, - coreMigrationVersion: '1.2.3', - }) - .expect(200) - .then((resp) => { - expect(resp.body.coreMigrationVersion).to.eql(KIBANA_VERSION); - }); + expect((await es.indices.exists({ index: '.kibana' })).body).to.be(false); }); }); }); diff --git a/test/api_integration/apis/saved_objects/delete.ts b/test/api_integration/apis/saved_objects/delete.ts index 5247bc74131d4..0dfece825d3a1 100644 --- a/test/api_integration/apis/saved_objects/delete.ts +++ b/test/api_integration/apis/saved_objects/delete.ts @@ -44,7 +44,7 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); it('returns generic 404 when kibana index is missing', async () => diff --git a/test/api_integration/apis/saved_objects/export.ts b/test/api_integration/apis/saved_objects/export.ts index 32a72f374cbe1..5206d51054745 100644 --- a/test/api_integration/apis/saved_objects/export.ts +++ b/test/api_integration/apis/saved_objects/export.ts @@ -534,7 +534,7 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); it('should return empty response', async () => { diff --git a/test/api_integration/apis/saved_objects/find.ts b/test/api_integration/apis/saved_objects/find.ts index be31e0faf1e46..66c2a083c79e5 100644 --- a/test/api_integration/apis/saved_objects/find.ts +++ b/test/api_integration/apis/saved_objects/find.ts @@ -40,7 +40,7 @@ export default function ({ getService }: FtrProviderContext) { { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - version: 'WzIsMV0=', + version: 'WzE4LDJd', attributes: { title: 'Count of requests', }, @@ -137,7 +137,7 @@ export default function ({ getService }: FtrProviderContext) { { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - version: 'WzIsMV0=', + version: 'WzE4LDJd', attributes: { title: 'Count of requests', }, @@ -174,7 +174,7 @@ export default function ({ getService }: FtrProviderContext) { { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - version: 'WzIsMV0=', + version: 'WzE4LDJd', attributes: { title: 'Count of requests', }, @@ -209,7 +209,7 @@ export default function ({ getService }: FtrProviderContext) { score: 0, type: 'visualization', updated_at: '2017-09-21T18:51:23.794Z', - version: 'WzYsMV0=', + version: 'WzIyLDJd', }, ], }); @@ -256,7 +256,7 @@ export default function ({ getService }: FtrProviderContext) { migrationVersion: resp.body.saved_objects[0].migrationVersion, coreMigrationVersion: KIBANA_VERSION, updated_at: '2017-09-21T18:51:23.794Z', - version: 'WzIsMV0=', + version: 'WzE4LDJd', }, ], }); @@ -426,11 +426,11 @@ export default function ({ getService }: FtrProviderContext) { })); }); - describe.skip('without kibana index', () => { + describe('without kibana index', () => { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); it('should return 200 with empty response', async () => diff --git a/test/api_integration/apis/saved_objects/get.ts b/test/api_integration/apis/saved_objects/get.ts index f912a2efcf0d9..84ab6e36956d5 100644 --- a/test/api_integration/apis/saved_objects/get.ts +++ b/test/api_integration/apis/saved_objects/get.ts @@ -78,7 +78,7 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); it('should return basic 404 without mentioning index', async () => diff --git a/test/api_integration/apis/saved_objects/resolve_import_errors.ts b/test/api_integration/apis/saved_objects/resolve_import_errors.ts index 4fcce29905beb..b203a2c7b7071 100644 --- a/test/api_integration/apis/saved_objects/resolve_import_errors.ts +++ b/test/api_integration/apis/saved_objects/resolve_import_errors.ts @@ -13,6 +13,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const esDeleteAllIndices = getService('esDeleteAllIndices'); describe('resolve_import_errors', () => { // mock success results including metadata @@ -34,7 +35,11 @@ export default function ({ getService }: FtrProviderContext) { describe('without kibana index', () => { // Cleanup data that got created in import - after(() => esArchiver.unload('saved_objects/basic')); + before( + async () => + // just in case the kibana server has recreated it + await esDeleteAllIndices('.kibana*') + ); it('should return 200 and import nothing when empty parameters are passed in', async () => { await supertest @@ -51,7 +56,7 @@ export default function ({ getService }: FtrProviderContext) { }); }); - it('should return 200 and import everything when overwrite parameters contains all objects', async () => { + it('should return 200 with internal server errors', async () => { await supertest .post('/api/saved_objects/_resolve_import_errors') .field( @@ -78,12 +83,42 @@ export default function ({ getService }: FtrProviderContext) { .expect(200) .then((resp) => { expect(resp.body).to.eql({ - success: true, - successCount: 3, - successResults: [ - { ...indexPattern, overwrite: true }, - { ...visualization, overwrite: true }, - { ...dashboard, overwrite: true }, + successCount: 0, + success: false, + errors: [ + { + ...indexPattern, + ...{ title: indexPattern.meta.title }, + overwrite: true, + error: { + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', + type: 'unknown', + }, + }, + { + ...visualization, + ...{ title: visualization.meta.title }, + overwrite: true, + error: { + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', + type: 'unknown', + }, + }, + { + ...dashboard, + ...{ title: dashboard.meta.title }, + overwrite: true, + error: { + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', + type: 'unknown', + }, + }, ], warnings: [], }); diff --git a/test/api_integration/apis/saved_objects/update.ts b/test/api_integration/apis/saved_objects/update.ts index ce14e9cea7b13..631046a0564a3 100644 --- a/test/api_integration/apis/saved_objects/update.ts +++ b/test/api_integration/apis/saved_objects/update.ts @@ -121,10 +121,10 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); - it('should return generic 404', async () => + it('should return 500', async () => await supertest .put(`/api/saved_objects/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab`) .send({ @@ -132,13 +132,12 @@ export default function ({ getService }: FtrProviderContext) { title: 'My second favorite vis', }, }) - .expect(404) + .expect(500) .then((resp) => { expect(resp.body).eql({ - statusCode: 404, - error: 'Not Found', - message: - 'Saved object [visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab] not found', + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred.', }); })); }); diff --git a/test/api_integration/apis/saved_objects_management/find.ts b/test/api_integration/apis/saved_objects_management/find.ts index 87de59a94fd24..6ab2352ebb05f 100644 --- a/test/api_integration/apis/saved_objects_management/find.ts +++ b/test/api_integration/apis/saved_objects_management/find.ts @@ -42,7 +42,7 @@ export default function ({ getService }: FtrProviderContext) { { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - version: 'WzIsMV0=', + version: 'WzE4LDJd', attributes: { title: 'Count of requests', }, @@ -184,7 +184,7 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); it('should return 200 with empty response', async () => diff --git a/test/api_integration/apis/saved_objects_management/get.ts b/test/api_integration/apis/saved_objects_management/get.ts index 69c85428d0624..4dfd06a61eecf 100644 --- a/test/api_integration/apis/saved_objects_management/get.ts +++ b/test/api_integration/apis/saved_objects_management/get.ts @@ -45,7 +45,7 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); it('should return 404 for object that no longer exists', async () => diff --git a/test/api_integration/apis/search/search.ts b/test/api_integration/apis/search/search.ts index 2b61ed7586384..bc092dd3889bb 100644 --- a/test/api_integration/apis/search/search.ts +++ b/test/api_integration/apis/search/search.ts @@ -17,6 +17,7 @@ export default function ({ getService }: FtrProviderContext) { describe('search', () => { before(async () => { + await esArchiver.emptyKibanaIndex(); await esArchiver.loadIfNeeded('../../../functional/fixtures/es_archiver/logstash_functional'); }); diff --git a/test/api_integration/apis/telemetry/opt_in.ts b/test/api_integration/apis/telemetry/opt_in.ts index 2e42fbfc6ac60..7e0564ac44a43 100644 --- a/test/api_integration/apis/telemetry/opt_in.ts +++ b/test/api_integration/apis/telemetry/opt_in.ts @@ -14,10 +14,13 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function optInTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + describe('/api/telemetry/v2/optIn API', () => { let defaultAttributes: TelemetrySavedObjectAttributes; let kibanaVersion: any; before(async () => { + await esArchiver.emptyKibanaIndex(); const kibanaVersionAccessor = kibanaServer.version; kibanaVersion = await kibanaVersionAccessor.get(); defaultAttributes = diff --git a/test/api_integration/apis/telemetry/telemetry_local.ts b/test/api_integration/apis/telemetry/telemetry_local.ts index 23a0d3fb2cd3c..b424cab9ff45b 100644 --- a/test/api_integration/apis/telemetry/telemetry_local.ts +++ b/test/api_integration/apis/telemetry/telemetry_local.ts @@ -177,6 +177,7 @@ export default function ({ getService }: FtrProviderContext) { describe('basic behaviour', () => { let savedObjectIds: string[] = []; before('create application usage entries', async () => { + await esArchiver.emptyKibanaIndex(); savedObjectIds = await Promise.all([ createSavedObject(), createSavedObject('appView1'), diff --git a/test/api_integration/apis/ui_counters/ui_counters.ts b/test/api_integration/apis/ui_counters/ui_counters.ts index c2286f8ea3dce..c287e73e3ace9 100644 --- a/test/api_integration/apis/ui_counters/ui_counters.ts +++ b/test/api_integration/apis/ui_counters/ui_counters.ts @@ -13,6 +13,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); const es = getService('es'); const createUiCounterEvent = (eventName: string, type: UiCounterMetricType, count = 1) => ({ @@ -24,6 +25,9 @@ export default function ({ getService }: FtrProviderContext) { // FLAKY: https://github.com/elastic/kibana/issues/85086 describe.skip('UI Counters API', () => { + before(async () => { + await esArchiver.emptyKibanaIndex(); + }); const dayDate = moment().format('DDMMYYYY'); it('stores ui counter events in savedObjects', async () => { diff --git a/test/api_integration/apis/ui_metric/ui_metric.ts b/test/api_integration/apis/ui_metric/ui_metric.ts index 1e80487da551a..99007376e1ea4 100644 --- a/test/api_integration/apis/ui_metric/ui_metric.ts +++ b/test/api_integration/apis/ui_metric/ui_metric.ts @@ -13,6 +13,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); const es = getService('es'); const createStatsMetric = ( @@ -34,6 +35,10 @@ export default function ({ getService }: FtrProviderContext) { }); describe('ui_metric savedObject data', () => { + before(async () => { + await esArchiver.emptyKibanaIndex(); + }); + it('increments the count field in the document defined by the {app}/{action_type} path', async () => { const reportManager = new ReportManager(); const uiStatsMetric = createStatsMetric('myEvent'); diff --git a/test/common/config.js b/test/common/config.js index 451324d46f62d..9d108f05fd1fc 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -61,8 +61,6 @@ export default function () { ...(!!process.env.CODE_COVERAGE ? [`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'coverage')}`] : []), - // Disable v2 migrations in tests for now - '--migrations.enableV2=false', ], }, services, diff --git a/test/common/services/kibana_server/extend_es_archiver.js b/test/common/services/kibana_server/extend_es_archiver.js index c2d01eef267bc..9a06dd7b74969 100644 --- a/test/common/services/kibana_server/extend_es_archiver.js +++ b/test/common/services/kibana_server/extend_es_archiver.js @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -const ES_ARCHIVER_LOAD_METHODS = ['load', 'loadIfNeeded', 'unload']; +const ES_ARCHIVER_LOAD_METHODS = ['load', 'loadIfNeeded', 'unload', 'emptyKibanaIndex']; const KIBANA_INDEX = '.kibana'; export function extendEsArchiver({ esArchiver, kibanaServer, retry, defaults }) { @@ -25,7 +25,7 @@ export function extendEsArchiver({ esArchiver, kibanaServer, retry, defaults }) const statsKeys = Object.keys(stats); const kibanaKeys = statsKeys.filter( // this also matches stats keys like '.kibana_1' and '.kibana_2,.kibana_1' - (key) => key.includes(KIBANA_INDEX) && (stats[key].created || stats[key].deleted) + (key) => key.includes(KIBANA_INDEX) && stats[key].created ); // if the kibana index was created by the esArchiver then update the uiSettings diff --git a/test/functional/apps/management/_import_objects.ts b/test/functional/apps/management/_import_objects.ts index d13ba0114a598..e2a056359b48e 100644 --- a/test/functional/apps/management/_import_objects.ts +++ b/test/functional/apps/management/_import_objects.ts @@ -27,9 +27,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe.skip('import objects', function describeIndexTests() { describe('.ndjson file', () => { beforeEach(async function () { + await esArchiver.load('management'); await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); - await esArchiver.load('management'); await PageObjects.settings.clickKibanaSavedObjects(); }); @@ -213,10 +213,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('.json file', () => { beforeEach(async function () { - // delete .kibana index and then wait for Kibana to re-create it + await esArchiver.load('saved_objects_imports'); await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); - await esArchiver.load('saved_objects_imports'); await PageObjects.settings.clickKibanaSavedObjects(); }); diff --git a/test/functional/apps/management/_index_pattern_filter.js b/test/functional/apps/management/_index_pattern_filter.js index b2d27002f690c..eeb0b224d5f0c 100644 --- a/test/functional/apps/management/_index_pattern_filter.js +++ b/test/functional/apps/management/_index_pattern_filter.js @@ -12,10 +12,11 @@ export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const retry = getService('retry'); const PageObjects = getPageObjects(['settings']); + const esArchiver = getService('esArchiver'); describe('index pattern filter', function describeIndexTests() { before(async function () { - // delete .kibana index and then wait for Kibana to re-create it + await esArchiver.emptyKibanaIndex(); await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); diff --git a/test/functional/apps/management/_index_patterns_empty.ts b/test/functional/apps/management/_index_patterns_empty.ts index a58c129810470..90dd8cdc35c30 100644 --- a/test/functional/apps/management/_index_patterns_empty.ts +++ b/test/functional/apps/management/_index_patterns_empty.ts @@ -19,7 +19,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('index pattern empty view', () => { before(async () => { - await esArchiver.load('empty_kibana'); + await esArchiver.emptyKibanaIndex(); await esArchiver.unload('logstash_functional'); await esArchiver.unload('makelogs'); await kibanaServer.uiSettings.replace({}); @@ -27,7 +27,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { - await esArchiver.unload('empty_kibana'); await esArchiver.loadIfNeeded('makelogs'); // @ts-expect-error await es.transport.request({ diff --git a/test/functional/apps/management/_mgmt_import_saved_objects.js b/test/functional/apps/management/_mgmt_import_saved_objects.js index 27745654f495f..8697dc49de46a 100644 --- a/test/functional/apps/management/_mgmt_import_saved_objects.js +++ b/test/functional/apps/management/_mgmt_import_saved_objects.js @@ -18,14 +18,13 @@ export default function ({ getService, getPageObjects }) { describe('mgmt saved objects', function describeIndexTests() { beforeEach(async function () { - await esArchiver.load('empty_kibana'); + await esArchiver.emptyKibanaIndex(); await esArchiver.load('discover'); await PageObjects.settings.navigateTo(); }); afterEach(async function () { await esArchiver.unload('discover'); - await esArchiver.load('empty_kibana'); }); it('should import saved objects mgmt', async function () { diff --git a/test/functional/apps/management/_test_huge_fields.js b/test/functional/apps/management/_test_huge_fields.js index 7ccce2c10c7b1..3102becbe181f 100644 --- a/test/functional/apps/management/_test_huge_fields.js +++ b/test/functional/apps/management/_test_huge_fields.js @@ -20,6 +20,7 @@ export default function ({ getService, getPageObjects }) { const EXPECTED_FIELD_COUNT = '10006'; before(async function () { await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader']); + await esArchiver.emptyKibanaIndex(); await esArchiver.loadIfNeeded('large_fields'); await PageObjects.settings.createIndexPattern('testhuge', 'date'); }); diff --git a/test/functional/apps/management/index.ts b/test/functional/apps/management/index.ts index 15828295b190f..d31245b5492d1 100644 --- a/test/functional/apps/management/index.ts +++ b/test/functional/apps/management/index.ts @@ -14,13 +14,11 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { describe('management', function () { before(async () => { await esArchiver.unload('logstash_functional'); - await esArchiver.load('empty_kibana'); await esArchiver.loadIfNeeded('makelogs'); }); after(async () => { await esArchiver.unload('makelogs'); - await esArchiver.unload('empty_kibana'); }); describe('', function () { diff --git a/test/functional/apps/visualize/input_control_vis/input_control_range.ts b/test/functional/apps/visualize/input_control_vis/input_control_range.ts index 4f4e6d6655be5..caa008080b2a3 100644 --- a/test/functional/apps/visualize/input_control_vis/input_control_range.ts +++ b/test/functional/apps/visualize/input_control_vis/input_control_range.ts @@ -12,7 +12,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); - const kibanaServer = getService('kibanaServer'); const find = getService('find'); const security = getService('security'); const { visualize, visEditor } = getPageObjects(['visualize', 'visEditor']); @@ -53,7 +52,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.loadIfNeeded('long_window_logstash'); await esArchiver.load('visualize'); - await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); await security.testUser.restoreDefaults(); }); }); diff --git a/test/plugin_functional/test_suites/core_plugins/applications.ts b/test/plugin_functional/test_suites/core_plugins/applications.ts index 6c600aa996a33..0e52b536410e4 100644 --- a/test/plugin_functional/test_suites/core_plugins/applications.ts +++ b/test/plugin_functional/test_suites/core_plugins/applications.ts @@ -19,6 +19,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const find = getService('find'); const retry = getService('retry'); const deployment = getService('deployment'); + const esArchiver = getService('esArchiver'); const loadingScreenNotShown = async () => expect(await testSubjects.exists('kbnLoadingMessage')).to.be(false); @@ -50,6 +51,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide describe('ui applications', function describeIndexTests() { before(async () => { + await esArchiver.emptyKibanaIndex(); await PageObjects.common.navigateToApp('foo'); }); diff --git a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts index 918e9f16c5dae..7947616ac6568 100644 --- a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts +++ b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts @@ -12,8 +12,12 @@ import '../../plugins/core_provider_plugin/types'; export default function ({ getService }: PluginFunctionalProviderContext) { const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); describe('index patterns', function () { + before(async () => { + await esArchiver.emptyKibanaIndex(); + }); let indexPatternId = ''; it('can create an index pattern', async () => { diff --git a/test/plugin_functional/test_suites/saved_objects_management/import_warnings.ts b/test/plugin_functional/test_suites/saved_objects_management/import_warnings.ts index 10a088426c8fd..da4c785342733 100644 --- a/test/plugin_functional/test_suites/saved_objects_management/import_warnings.ts +++ b/test/plugin_functional/test_suites/saved_objects_management/import_warnings.ts @@ -10,10 +10,15 @@ import path from 'path'; import expect from '@kbn/expect'; import { PluginFunctionalProviderContext } from '../../services'; -export default function ({ getPageObjects }: PluginFunctionalProviderContext) { +export default function ({ getPageObjects, getService }: PluginFunctionalProviderContext) { const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects']); + const esArchiver = getService('esArchiver'); describe('import warnings', () => { + before(async () => { + await esArchiver.emptyKibanaIndex(); + }); + beforeEach(async () => { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaSavedObjects(); diff --git a/test/security_functional/insecure_cluster_warning.ts b/test/security_functional/insecure_cluster_warning.ts index 229dac20390a2..44a0e2eb0e121 100644 --- a/test/security_functional/insecure_cluster_warning.ts +++ b/test/security_functional/insecure_cluster_warning.ts @@ -31,6 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async () => { await browser.setLocalStorageItem('insecureClusterWarningVisibility', ''); await esArchiver.unload('hamlet'); + await esArchiver.emptyKibanaIndex(); }); it('should not warn when the cluster contains no user data', async () => { diff --git a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts b/x-pack/test/functional/apps/dashboard/_async_dashboard.ts index 55474bd5a7688..5b2632ef710e4 100644 --- a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts +++ b/x-pack/test/functional/apps/dashboard/_async_dashboard.ts @@ -13,6 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const browser = getService('browser'); const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); const log = getService('log'); const pieChart = getService('pieChart'); const find = getService('find'); @@ -30,6 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('sample data dashboard', function describeIndexTests() { before(async () => { + await esArchiver.emptyKibanaIndex(); await PageObjects.common.sleep(5000); await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { useActualUrl: true, diff --git a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts index bb60de86aef82..95ebc7b2ff5d5 100644 --- a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts +++ b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts @@ -70,7 +70,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows all saved objects', async () => { const objects = await PageObjects.savedObjects.getRowTitles(); expect(objects).to.eql([ - 'Advanced Settings [6.0.0]', `Advanced Settings [${version}]`, 'A Dashboard', 'logstash-*', @@ -81,10 +80,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('can view all saved objects in applications', async () => { const bools = await PageObjects.savedObjects.getTableSummary(); expect(bools).to.eql([ - { - title: 'Advanced Settings [6.0.0]', - canViewInApp: false, - }, { title: `Advanced Settings [${version}]`, canViewInApp: false, @@ -189,7 +184,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows all saved objects', async () => { const objects = await PageObjects.savedObjects.getRowTitles(); expect(objects).to.eql([ - 'Advanced Settings [6.0.0]', `Advanced Settings [${version}]`, 'A Dashboard', 'logstash-*', @@ -200,10 +194,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('cannot view any saved objects in applications', async () => { const bools = await PageObjects.savedObjects.getTableSummary(); expect(bools).to.eql([ - { - title: 'Advanced Settings [6.0.0]', - canViewInApp: false, - }, { title: `Advanced Settings [${version}]`, canViewInApp: false, diff --git a/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/data.json b/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/data.json index f085bad4c507e..b63ae2295f70b 100644 --- a/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/data.json +++ b/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/data.json @@ -66,20 +66,3 @@ } } } - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "config:6.0.0", - "source": { - "config": { - "buildNum": 9007199254740991, - "defaultIndex": "logstash-*" - }, - "type": "config", - "updated_at": "2019-01-22T19:32:02.235Z" - } - } -} diff --git a/x-pack/test/functional/es_archives/visualize/default/data.json b/x-pack/test/functional/es_archives/visualize/default/data.json index fe29bad0fa381..26b033e28b4da 100644 --- a/x-pack/test/functional/es_archives/visualize/default/data.json +++ b/x-pack/test/functional/es_archives/visualize/default/data.json @@ -125,26 +125,8 @@ { "type": "doc", "value": { - "id": "custom-space:index-pattern:metricbeat-*", - "index": ".kibana_1", - "source": { - "index-pattern": { - "fieldFormatMap": "{\"aerospike.namespace.device.available.pct\":{\"id\":\"percent\",\"params\":{}},\"aerospike.namespace.device.free.pct\":{\"id\":\"percent\",\"params\":{}},\"aerospike.namespace.device.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aerospike.namespace.device.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aerospike.namespace.memory.free.pct\":{\"id\":\"percent\",\"params\":{}},\"aerospike.namespace.memory.used.data.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aerospike.namespace.memory.used.index.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aerospike.namespace.memory.used.sindex.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aerospike.namespace.memory.used.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.ec2.diskio.read.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.ec2.diskio.write.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.ec2.network.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.ec2.network.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.rds.cpu.total.pct\":{\"id\":\"percent\",\"params\":{}},\"aws.rds.disk_usage.bin_log.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.rds.free_local_storage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.rds.free_storage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.rds.freeable_memory.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.rds.latency.commit\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.ddl\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.dml\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.insert\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.read\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.select\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.update\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.write\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.replica_lag.sec\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.swap_usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.rds.volume_used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.s3_daily_storage.bucket.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.s3_request.downloaded.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.s3_request.latency.first_byte.ms\":{\"id\":\"duration\",\"params\":{}},\"aws.s3_request.latency.total_request.ms\":{\"id\":\"duration\",\"params\":{}},\"aws.s3_request.requests.select_returned.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.s3_request.requests.select_scanned.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.s3_request.uploaded.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.sqs.oldest_message_age.sec\":{\"id\":\"duration\",\"params\":{}},\"aws.sqs.sent_message_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_disk.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_disk.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_disk.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_status.degraded.ratio\":{\"id\":\"percent\",\"params\":{}},\"ceph.cluster_status.misplace.ratio\":{\"id\":\"percent\",\"params\":{}},\"ceph.cluster_status.pg.avail_bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_status.pg.data_bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_status.pg.total_bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_status.pg.used_bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_status.traffic.read_bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_status.traffic.write_bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.monitor_health.store_stats.log.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.monitor_health.store_stats.misc.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.monitor_health.store_stats.sst.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.monitor_health.store_stats.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.osd_df.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.osd_df.total.byte\":{\"id\":\"bytes\",\"params\":{}},\"ceph.osd_df.used.byte\":{\"id\":\"bytes\",\"params\":{}},\"ceph.osd_df.used.pct\":{\"id\":\"percent\",\"params\":{}},\"ceph.pool_disk.stats.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.pool_disk.stats.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"client.bytes\":{\"id\":\"bytes\",\"params\":{}},\"client.nat.port\":{\"id\":\"string\",\"params\":{}},\"client.port\":{\"id\":\"string\",\"params\":{}},\"coredns.stats.dns.request.duration.ns.sum\":{\"id\":\"duration\",\"params\":{}},\"couchbase.bucket.data.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.bucket.disk.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.bucket.memory.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.bucket.quota.ram.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.bucket.quota.use.pct\":{\"id\":\"percent\",\"params\":{}},\"couchbase.cluster.hdd.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.hdd.quota.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.hdd.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.hdd.used.by_data.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.hdd.used.value.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.quota.total.per_node.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.quota.total.value.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.quota.used.per_node.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.quota.used.value.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.used.by_data.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.used.value.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.node.couch.docs.data_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.node.couch.docs.disk_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.node.mcd_memory.allocated.bytes\":{\"id\":\"bytes\",\"params\":{}},\"destination.bytes\":{\"id\":\"bytes\",\"params\":{}},\"destination.nat.port\":{\"id\":\"string\",\"params\":{}},\"destination.port\":{\"id\":\"string\",\"params\":{}},\"docker.cpu.core.*.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.cpu.kernel.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.cpu.system.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.cpu.total.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.cpu.user.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.diskio.read.bytes\":{\"id\":\"bytes\",\"params\":{}},\"docker.diskio.summary.bytes\":{\"id\":\"bytes\",\"params\":{}},\"docker.diskio.write.bytes\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.commit.peak\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.commit.total\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.limit\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.private_working_set.total\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.rss.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.memory.rss.total\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.usage.max\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.usage.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.memory.usage.total\":{\"id\":\"bytes\",\"params\":{}},\"docker.network.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"docker.network.inbound.bytes\":{\"id\":\"bytes\",\"params\":{}},\"docker.network.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"docker.network.outbound.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.index.summary.primaries.segments.memory.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.index.summary.primaries.store.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.index.summary.total.segments.memory.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.index.summary.total.store.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.index.total.segments.memory.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.index.total.store.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.jvm.memory.heap.init.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.jvm.memory.heap.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.jvm.memory.nonheap.init.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.jvm.memory.nonheap.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.fs.summary.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.fs.summary.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.fs.summary.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.indices.segments.memory.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.old.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.old.peak.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.old.peak_max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.old.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.survivor.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.survivor.peak.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.survivor.peak_max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.survivor.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.young.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.young.peak.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.young.peak_max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.young.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"etcd.disk.mvcc_db_total_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"etcd.memory.go_memstats_alloc.bytes\":{\"id\":\"bytes\",\"params\":{}},\"etcd.network.client_grpc_received.bytes\":{\"id\":\"bytes\",\"params\":{}},\"etcd.network.client_grpc_sent.bytes\":{\"id\":\"bytes\",\"params\":{}},\"event.duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"nanoseconds\",\"outputFormat\":\"asMilliseconds\",\"outputPrecision\":1}},\"event.sequence\":{\"id\":\"string\",\"params\":{}},\"event.severity\":{\"id\":\"string\",\"params\":{}},\"golang.heap.allocations.active\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.allocations.allocated\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.allocations.idle\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.allocations.total\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.gc.next_gc_limit\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.system.obtained\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.system.released\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.system.stack\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.system.total\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.info.idle.pct\":{\"id\":\"percent\",\"params\":{}},\"haproxy.info.memory.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.info.ssl.frontend.session_reuse.pct\":{\"id\":\"percent\",\"params\":{}},\"haproxy.stat.compressor.bypassed.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.stat.compressor.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.stat.compressor.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.stat.compressor.response.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.stat.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.stat.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.stat.throttle.pct\":{\"id\":\"percent\",\"params\":{}},\"http.request.body.bytes\":{\"id\":\"bytes\",\"params\":{}},\"http.request.bytes\":{\"id\":\"bytes\",\"params\":{}},\"http.response.body.bytes\":{\"id\":\"bytes\",\"params\":{}},\"http.response.bytes\":{\"id\":\"bytes\",\"params\":{}},\"http.response.status_code\":{\"id\":\"string\",\"params\":{}},\"kibana.stats.process.memory.heap.size_limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kibana.stats.process.memory.heap.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kibana.stats.process.memory.heap.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.apiserver.http.request.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.apiserver.http.response.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.apiserver.process.memory.resident.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.apiserver.process.memory.virtual.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.cpu.usage.limit.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.container.cpu.usage.node.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.container.logs.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.logs.capacity.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.logs.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.memory.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.memory.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.memory.request.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.memory.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.memory.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.memory.usage.limit.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.container.memory.usage.node.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.container.memory.workingset.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.rootfs.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.rootfs.capacity.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.rootfs.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.controllermanager.http.request.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.controllermanager.http.response.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.controllermanager.process.memory.resident.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.controllermanager.process.memory.virtual.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.fs.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.fs.capacity.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.fs.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.memory.allocatable.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.memory.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.memory.capacity.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.memory.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.memory.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.memory.workingset.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.network.rx.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.network.tx.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.runtime.imagefs.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.runtime.imagefs.capacity.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.runtime.imagefs.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.pod.cpu.usage.limit.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.pod.cpu.usage.node.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.pod.memory.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.pod.memory.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.pod.memory.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.pod.memory.usage.limit.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.pod.memory.usage.node.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.pod.memory.working_set.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.pod.network.rx.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.pod.network.tx.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.proxy.http.request.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.proxy.http.response.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.proxy.process.memory.resident.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.proxy.process.memory.virtual.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.scheduler.http.request.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.scheduler.http.response.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.scheduler.process.memory.resident.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.scheduler.process.memory.virtual.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.system.memory.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.system.memory.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.system.memory.workingset.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.volume.fs.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.volume.fs.capacity.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.volume.fs.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.dbstats.avg_obj_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.dbstats.data_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.dbstats.extent_free_list.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.dbstats.file_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.dbstats.index_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.dbstats.storage_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.replstatus.headroom.max\":{\"id\":\"duration\",\"params\":{}},\"mongodb.replstatus.headroom.min\":{\"id\":\"duration\",\"params\":{}},\"mongodb.replstatus.lag.max\":{\"id\":\"duration\",\"params\":{}},\"mongodb.replstatus.lag.min\":{\"id\":\"duration\",\"params\":{}},\"mongodb.replstatus.oplog.size.allocated\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.replstatus.oplog.size.used\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.extra_info.heap_usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.network.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.network.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.wired_tiger.cache.dirty.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.wired_tiger.cache.maximum.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.wired_tiger.cache.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.wired_tiger.log.max_file_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.wired_tiger.log.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.wired_tiger.log.write.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mysql.status.bytes.received\":{\"id\":\"bytes\",\"params\":{}},\"mysql.status.bytes.sent\":{\"id\":\"bytes\",\"params\":{}},\"nats.stats.cpu\":{\"id\":\"percent\",\"params\":{}},\"nats.stats.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"nats.stats.mem.bytes\":{\"id\":\"bytes\",\"params\":{}},\"nats.stats.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"nats.stats.uptime\":{\"id\":\"duration\",\"params\":{}},\"nats.subscriptions.cache.hit_rate\":{\"id\":\"percent\",\"params\":{}},\"network.bytes\":{\"id\":\"bytes\",\"params\":{}},\"oracle.tablespace.data_file.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"oracle.tablespace.data_file.size.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"oracle.tablespace.data_file.size.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"oracle.tablespace.space.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"oracle.tablespace.space.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"oracle.tablespace.space.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"process.pgid\":{\"id\":\"string\",\"params\":{}},\"process.pid\":{\"id\":\"string\",\"params\":{}},\"process.ppid\":{\"id\":\"string\",\"params\":{}},\"process.thread.id\":{\"id\":\"string\",\"params\":{}},\"rabbitmq.connection.frame_max\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.node.disk.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.node.disk.free.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.node.gc.reclaimed.bytes\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.node.io.read.bytes\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.node.io.write.bytes\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.node.mem.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.queue.consumers.utilisation.pct\":{\"id\":\"percent\",\"params\":{}},\"rabbitmq.queue.memory.bytes\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.allocator_stats.active\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.allocator_stats.allocated\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.allocator_stats.fragmentation.bytes\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.allocator_stats.resident\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.allocator_stats.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.fragmentation.bytes\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.max.value\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.used.dataset\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.used.lua\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.used.peak\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.used.rss\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.used.value\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.persistence.aof.buffer.size\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.persistence.aof.copy_on_write.last_size\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.persistence.aof.rewrite.buffer.size\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.persistence.aof.rewrite.current_time.sec\":{\"id\":\"duration\",\"params\":{}},\"redis.info.persistence.aof.rewrite.last_time.sec\":{\"id\":\"duration\",\"params\":{}},\"redis.info.persistence.aof.size.base\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.persistence.aof.size.current\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.persistence.rdb.bgsave.current_time.sec\":{\"id\":\"duration\",\"params\":{}},\"redis.info.persistence.rdb.bgsave.last_time.sec\":{\"id\":\"duration\",\"params\":{}},\"redis.info.persistence.rdb.copy_on_write.last_size\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.replication.backlog.size\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.replication.master.last_io_seconds_ago\":{\"id\":\"duration\",\"params\":{}},\"redis.info.replication.master.sync.last_io_seconds_ago\":{\"id\":\"duration\",\"params\":{}},\"redis.info.replication.master.sync.left_bytes\":{\"id\":\"bytes\",\"params\":{}},\"server.bytes\":{\"id\":\"bytes\",\"params\":{}},\"server.nat.port\":{\"id\":\"string\",\"params\":{}},\"server.port\":{\"id\":\"string\",\"params\":{}},\"source.bytes\":{\"id\":\"bytes\",\"params\":{}},\"source.nat.port\":{\"id\":\"string\",\"params\":{}},\"source.port\":{\"id\":\"string\",\"params\":{}},\"system.core.idle.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.iowait.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.irq.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.nice.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.softirq.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.steal.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.system.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.user.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.idle.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.idle.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.iowait.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.iowait.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.irq.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.irq.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.nice.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.nice.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.softirq.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.softirq.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.steal.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.steal.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.system.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.system.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.total.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.total.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.user.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.user.pct\":{\"id\":\"percent\",\"params\":{}},\"system.diskio.iostat.read.per_sec.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.diskio.iostat.write.per_sec.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.diskio.read.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.diskio.write.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.entropy.pct\":{\"id\":\"percent\",\"params\":{}},\"system.filesystem.available\":{\"id\":\"bytes\",\"params\":{}},\"system.filesystem.free\":{\"id\":\"bytes\",\"params\":{}},\"system.filesystem.total\":{\"id\":\"bytes\",\"params\":{}},\"system.filesystem.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.filesystem.used.pct\":{\"id\":\"percent\",\"params\":{}},\"system.fsstat.total_size.free\":{\"id\":\"bytes\",\"params\":{}},\"system.fsstat.total_size.total\":{\"id\":\"bytes\",\"params\":{}},\"system.fsstat.total_size.used\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.actual.free\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.actual.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.actual.used.pct\":{\"id\":\"percent\",\"params\":{}},\"system.memory.free\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.hugepages.default_size\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.hugepages.free\":{\"id\":\"number\",\"params\":{}},\"system.memory.hugepages.reserved\":{\"id\":\"number\",\"params\":{}},\"system.memory.hugepages.surplus\":{\"id\":\"number\",\"params\":{}},\"system.memory.hugepages.total\":{\"id\":\"number\",\"params\":{}},\"system.memory.hugepages.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.hugepages.used.pct\":{\"id\":\"percent\",\"params\":{}},\"system.memory.swap.free\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.swap.total\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.swap.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.swap.used.pct\":{\"id\":\"percent\",\"params\":{}},\"system.memory.total\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.used.pct\":{\"id\":\"percent\",\"params\":{}},\"system.network.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.network.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.blkio.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.kmem.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.kmem.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.kmem.usage.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.kmem_tcp.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.kmem_tcp.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.kmem_tcp.usage.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.mem.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.mem.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.mem.usage.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.memsw.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.memsw.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.memsw.usage.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.active_anon.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.active_file.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.cache.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.hierarchical_memory_limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.hierarchical_memsw_limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.inactive_anon.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.inactive_file.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.mapped_file.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.rss_huge.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.swap.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.unevictable.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cpu.total.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.process.cpu.total.pct\":{\"id\":\"percent\",\"params\":{}},\"system.process.memory.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.memory.rss.pct\":{\"id\":\"percent\",\"params\":{}},\"system.process.memory.share\":{\"id\":\"bytes\",\"params\":{}},\"system.process.memory.size\":{\"id\":\"bytes\",\"params\":{}},\"system.socket.summary.tcp.memory\":{\"id\":\"bytes\",\"params\":{}},\"system.socket.summary.udp.memory\":{\"id\":\"bytes\",\"params\":{}},\"system.uptime.duration.ms\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"milliseconds\"}},\"url.port\":{\"id\":\"string\",\"params\":{}},\"vsphere.datastore.capacity.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.datastore.capacity.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.datastore.capacity.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.datastore.capacity.used.pct\":{\"id\":\"percent\",\"params\":{}},\"vsphere.host.memory.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.host.memory.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.host.memory.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.virtualmachine.memory.free.guest.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.virtualmachine.memory.total.guest.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.virtualmachine.memory.used.guest.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.virtualmachine.memory.used.host.bytes\":{\"id\":\"bytes\",\"params\":{}},\"windows.service.uptime.ms\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"milliseconds\"}}}", - "timeFieldName": "@timestamp", - "title": "metricbeat-*" - }, - "migrationVersion": { - "index-pattern": "7.6.0" - }, - "type": "index-pattern", - "updated_at": "2020-01-22T15:34:59.061Z" - } - } -} - -{ - "type": "doc", - "value": { + "index": ".kibana", + "type": "doc", "id": "index-pattern:logstash-*", "index": ".kibana_1", "source": { @@ -297,4 +279,4 @@ "updated_at": "2019-07-17T17:54:26.378Z" } } -} \ No newline at end of file +} diff --git a/x-pack/test/reporting_api_integration/reporting_without_security.config.ts b/x-pack/test/reporting_api_integration/reporting_without_security.config.ts index 5946a502a4ce3..59d6074d9d8ca 100644 --- a/x-pack/test/reporting_api_integration/reporting_without_security.config.ts +++ b/x-pack/test/reporting_api_integration/reporting_without_security.config.ts @@ -33,7 +33,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { kbnTestServer: { ...apiConfig.get('kbnTestServer'), serverArgs: [ - `--migrations.enableV2=false`, `--elasticsearch.hosts=${formatUrl(esTestConfig.getUrlParts())}`, `--logging.json=false`, `--server.maxPayloadBytes=1679958`, From 8769c8d3ba0b9daf244b67c742b545eab73146d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Sun, 7 Feb 2021 12:04:33 +0100 Subject: [PATCH 30/44] Bump immer dependencies (#90267) * Bump immer dependencies * Update processed_search_response.ts * Update processed_search_response.ts Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 2 +- .../fixtures/processed_search_response.ts | 20 +++++++++++-------- yarn.lock | 7 ++++++- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 4afe7a579ad45..a5c6fa6f7b3c2 100644 --- a/package.json +++ b/package.json @@ -220,7 +220,7 @@ "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^5.0.0", "idx": "^2.5.6", - "immer": "^1.5.0", + "immer": "^8.0.1", "inline-style": "^2.0.0", "intl": "^1.2.5", "intl-format-cache": "^2.1.0", diff --git a/x-pack/plugins/searchprofiler/public/application/components/profile_tree/__jest__/fixtures/processed_search_response.ts b/x-pack/plugins/searchprofiler/public/application/components/profile_tree/__jest__/fixtures/processed_search_response.ts index 2d8e812408efe..c2a9bfa6506e5 100644 --- a/x-pack/plugins/searchprofiler/public/application/components/profile_tree/__jest__/fixtures/processed_search_response.ts +++ b/x-pack/plugins/searchprofiler/public/application/components/profile_tree/__jest__/fixtures/processed_search_response.ts @@ -6,6 +6,7 @@ */ import { produce } from 'immer'; +import { Index } from '../../../../types'; const shard1 = { id: ['L22w_FX2SbqlQYOP5QrYDg', '.kibana_1', '0'], @@ -336,11 +337,14 @@ const search1Child = { (searchRoot.treeRoot as any) = search1; (shard1.searches[0] as any) = searchRoot; -export const processedResponseWithFirstShard = produce(null, () => [ - { - shards: [shard1], - time: 0.058419, - name: '.kibana_1', - visible: false, - }, -]); +export const processedResponseWithFirstShard = produce( + [ + { + shards: [shard1], + time: 0.058419, + name: '.kibana_1', + visible: false, + }, + ], + () => undefined +); diff --git a/yarn.lock b/yarn.lock index e4f5fc2e1a8e1..fa7ebacb1cd70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17000,11 +17000,16 @@ immediate@~3.0.5: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= -immer@1.10.0, immer@^1.5.0: +immer@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d" integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg== +immer@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" + integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA== + import-cwd@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" From 44eff7184eb06dcbf990962ac7690bf438a9986a Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Sun, 7 Feb 2021 16:04:53 +0000 Subject: [PATCH 31/44] skip flaky suite (#90552) --- .../api_integration/tagging_api/apis/delete.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/delete.ts b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/delete.ts index 415cdf4814176..ed4bc8f4f8c7b 100644 --- a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/delete.ts +++ b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/delete.ts @@ -13,7 +13,8 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); - describe('DELETE /api/saved_objects_tagging/tags/{id}', () => { + // FLAKY: https://github.com/elastic/kibana/issues/90552 + describe.skip('DELETE /api/saved_objects_tagging/tags/{id}', () => { beforeEach(async () => { await esArchiver.load('delete_with_references'); }); From 91ffe7373a66d5ce512a8b2497bcd02db21cf540 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Sun, 7 Feb 2021 18:57:50 -0500 Subject: [PATCH 32/44] [Fleet] Support Fleet server system indices (#89372) --- .../fleet/common/types/models/agent.ts | 164 +++++++++++++++++- .../server/routes/agent/acks_handlers.ts | 2 +- .../server/routes/agent/actions_handlers.ts | 2 +- .../fleet/server/routes/agent/handlers.ts | 39 +++-- .../server/routes/agent/upgrade_handler.ts | 20 +-- .../fleet/server/services/agent_policy.ts | 16 +- .../fleet/server/services/agents/acks.test.ts | 40 ++--- .../fleet/server/services/agents/acks.ts | 37 ++-- .../server/services/agents/actions.test.ts | 6 +- .../fleet/server/services/agents/actions.ts | 70 +++++++- .../services/agents/authenticate.test.ts | 14 +- .../server/services/agents/authenticate.ts | 5 +- .../server/services/agents/checkin/index.ts | 14 +- .../agents/checkin/state_connected_agents.ts | 18 +- .../agents/checkin/state_new_actions.test.ts | 19 +- .../agents/checkin/state_new_actions.ts | 44 +++-- .../fleet/server/services/agents/crud.ts | 72 ++++---- .../services/agents/crud_fleet_server.ts | 134 ++++++++++---- .../fleet/server/services/agents/crud_so.ts | 36 +++- .../fleet/server/services/agents/enroll.ts | 35 +++- .../fleet/server/services/agents/helpers.ts | 23 ++- .../server/services/agents/reassign.test.ts | 3 + .../fleet/server/services/agents/reassign.ts | 33 ++-- .../fleet/server/services/agents/status.ts | 8 +- .../server/services/agents/unenroll.test.ts | 4 + .../fleet/server/services/agents/unenroll.ts | 46 +++-- .../fleet/server/services/agents/upgrade.ts | 33 ++-- .../services/api_keys/enrollment_api_key.ts | 12 ++ .../enrollment_api_key_fleet_server.ts | 56 +++--- .../api_keys/enrollment_api_key_so.ts | 21 ++- .../fleet/server/services/api_keys/index.ts | 39 +---- .../server/services/fleet_server_migration.ts | 147 ++++++++++++++-- x-pack/plugins/fleet/server/services/index.ts | 1 + x-pack/plugins/fleet/server/services/setup.ts | 10 ++ x-pack/plugins/fleet/server/types/index.tsx | 3 + .../artifacts/download_exception_list.test.ts | 15 ++ .../artifacts/download_exception_list.ts | 6 +- 37 files changed, 882 insertions(+), 365 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts index 57c42d887bc83..2e18d427272ce 100644 --- a/x-pack/plugins/fleet/common/types/models/agent.ts +++ b/x-pack/plugins/fleet/common/types/models/agent.ts @@ -130,8 +130,8 @@ interface AgentBase { enrolled_at: string; unenrolled_at?: string; unenrollment_started_at?: string; - upgraded_at?: string; - upgrade_started_at?: string; + upgraded_at?: string | null; + upgrade_started_at?: string | null; access_api_key_id?: string; default_api_key?: string; default_api_key_id?: string; @@ -155,3 +155,163 @@ export interface AgentSOAttributes extends AgentBase { current_error_events?: string; packages?: string[]; } + +// Generated from FleetServer schema.json + +/** + * An Elastic Agent that has enrolled into Fleet + */ +export interface FleetServerAgent { + /** + * The version of the document in the index + */ + _version?: number; + /** + * Shared ID + */ + shared_id?: string; + /** + * Type + */ + type: AgentType; + /** + * Active flag + */ + active: boolean; + /** + * Date/time the Elastic Agent enrolled + */ + enrolled_at: string; + /** + * Date/time the Elastic Agent unenrolled + */ + unenrolled_at?: string; + /** + * Date/time the Elastic Agent unenrolled started + */ + unenrollment_started_at?: string; + /** + * Date/time the Elastic Agent was last upgraded + */ + upgraded_at?: string | null; + /** + * Date/time the Elastic Agent started the current upgrade + */ + upgrade_started_at?: string | null; + /** + * ID of the API key the Elastic Agent must used to contact Fleet Server + */ + access_api_key_id?: string; + agent?: FleetServerAgentMetadata; + /** + * User provided metadata information for the Elastic Agent + */ + user_provided_metadata: AgentMetadata; + /** + * Local metadata information for the Elastic Agent + */ + local_metadata: AgentMetadata; + /** + * The policy ID for the Elastic Agent + */ + policy_id?: string; + /** + * The current policy revision_idx for the Elastic Agent + */ + policy_revision_idx?: number | null; + /** + * The current policy coordinator for the Elastic Agent + */ + policy_coordinator_idx?: number; + /** + * Date/time the Elastic Agent was last updated + */ + last_updated?: string; + /** + * Date/time the Elastic Agent checked in last time + */ + last_checkin?: string; + /** + * Lst checkin status + */ + last_checkin_status?: 'error' | 'online' | 'degraded' | 'updating'; + /** + * ID of the API key the Elastic Agent uses to authenticate with elasticsearch + */ + default_api_key_id?: string; + /** + * API key the Elastic Agent uses to authenticate with elasticsearch + */ + default_api_key?: string; + /** + * Date/time the Elastic Agent was last updated + */ + updated_at?: string; + /** + * Packages array + */ + packages?: string[]; + /** + * The last acknowledged action sequence number for the Elastic Agent + */ + action_seq_no?: number; +} +/** + * An Elastic Agent metadata + */ +export interface FleetServerAgentMetadata { + /** + * The unique identifier for the Elastic Agent + */ + id: string; + /** + * The version of the Elastic Agent + */ + version: string; + [k: string]: any; +} + +/** + * An Elastic Agent action + */ +export interface FleetServerAgentAction { + /** + * The unique identifier for action document + */ + _id?: string; + /** + * The action sequence number + */ + _seq_no?: number; + /** + * The unique identifier for the Elastic Agent action. There could be multiple documents with the same action_id if the action is split into two separate documents. + */ + action_id?: string; + /** + * Date/time the action was created + */ + '@timestamp'?: string; + /** + * The action expiration date/time + */ + expiration?: string; + /** + * The action type. APP_ACTION is the value for the actions that suppose to be routed to the endpoints/beats. + */ + type?: string; + /** + * The input identifier the actions should be routed to. + */ + input_id?: string; + /** + * The Agent IDs the action is intended for. No support for json.RawMessage with the current generator. Could be useful to lazy parse the agent ids + */ + agents?: string[]; + /** + * The opaque payload. + */ + data?: { + [k: string]: unknown; + }; + [k: string]: unknown; +} diff --git a/x-pack/plugins/fleet/server/routes/agent/acks_handlers.ts b/x-pack/plugins/fleet/server/routes/agent/acks_handlers.ts index 2d7c884edad83..22b5035378a20 100644 --- a/x-pack/plugins/fleet/server/routes/agent/acks_handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/acks_handlers.ts @@ -20,7 +20,7 @@ export const postAgentAcksHandlerBuilder = function ( try { const soClient = ackService.getSavedObjectsClientContract(request); const esClient = ackService.getElasticsearchClientContract(); - const agent = await ackService.authenticateAgentWithAccessToken(soClient, request); + const agent = await ackService.authenticateAgentWithAccessToken(soClient, esClient, request); const agentEvents = request.body.events as AgentEvent[]; // validate that all events are for the authorized agent obtained from the api key diff --git a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts index bf0cfd2d476dd..d032945245faf 100644 --- a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts @@ -30,7 +30,7 @@ export const postNewAgentActionHandlerBuilder = function ( const newAgentAction = request.body.action; - const savedAgentAction = await actionsService.createAgentAction(soClient, { + const savedAgentAction = await actionsService.createAgentAction(soClient, esClient, { created_at: new Date().toISOString(), ...newAgentAction, agent_id: agent.id, diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index 411da6da0223c..cd91e8c325c06 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -132,8 +132,8 @@ export const updateAgentHandler: RequestHandler< const esClient = context.core.elasticsearch.client.asCurrentUser; try { - await AgentService.updateAgent(soClient, request.params.agentId, { - userProvidedMetatada: request.body.user_provided_metadata, + await AgentService.updateAgent(soClient, esClient, request.params.agentId, { + user_provided_metadata: request.body.user_provided_metadata, }); const agent = await AgentService.getAgent(soClient, esClient, request.params.agentId); @@ -164,12 +164,13 @@ export const postAgentCheckinHandler: RequestHandler< try { const soClient = appContextService.getInternalUserSOClient(request); const esClient = appContextService.getInternalUserESClient(); - const agent = await AgentService.authenticateAgentWithAccessToken(soClient, request); + const agent = await AgentService.authenticateAgentWithAccessToken(soClient, esClient, request); const abortController = new AbortController(); request.events.aborted$.subscribe(() => { abortController.abort(); }); const signal = abortController.signal; + const { actions } = await AgentService.agentCheckin( soClient, esClient, @@ -205,8 +206,13 @@ export const postAgentEnrollHandler: RequestHandler< > = async (context, request, response) => { try { const soClient = appContextService.getInternalUserSOClient(request); + const esClient = context.core.elasticsearch.client.asInternalUser; const { apiKeyId } = APIKeyService.parseApiKeyFromHeaders(request.headers); - const enrollmentAPIKey = await APIKeyService.getEnrollmentAPIKeyById(soClient, apiKeyId); + const enrollmentAPIKey = await APIKeyService.getEnrollmentAPIKeyById( + soClient, + esClient, + apiKeyId + ); if (!enrollmentAPIKey || !enrollmentAPIKey.active) { return response.unauthorized({ @@ -311,21 +317,16 @@ export const postBulkAgentsReassignHandler: RequestHandler< const soClient = context.core.savedObjects.client; const esClient = context.core.elasticsearch.client.asInternalUser; try { - // Reassign by array of IDs - const result = Array.isArray(request.body.agents) - ? await AgentService.reassignAgents( - soClient, - esClient, - { agentIds: request.body.agents }, - request.body.policy_id - ) - : await AgentService.reassignAgents( - soClient, - esClient, - { kuery: request.body.agents }, - request.body.policy_id - ); - const body: PostBulkAgentReassignResponse = result.saved_objects.reduce((acc, so) => { + const results = await AgentService.reassignAgents( + soClient, + esClient, + Array.isArray(request.body.agents) + ? { agentIds: request.body.agents } + : { kuery: request.body.agents }, + request.body.policy_id + ); + + const body: PostBulkAgentReassignResponse = results.items.reduce((acc, so) => { return { ...acc, [so.id]: { diff --git a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts index 0215b8f27b393..086a9411f20b8 100644 --- a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts @@ -8,18 +8,13 @@ import { RequestHandler } from 'src/core/server'; import { TypeOf } from '@kbn/config-schema'; import semverCoerce from 'semver/functions/coerce'; -import { - AgentSOAttributes, - PostAgentUpgradeResponse, - PostBulkAgentUpgradeResponse, -} from '../../../common/types'; +import { PostAgentUpgradeResponse, PostBulkAgentUpgradeResponse } from '../../../common/types'; import { PostAgentUpgradeRequestSchema, PostBulkAgentUpgradeRequestSchema } from '../../types'; import * as AgentService from '../../services/agents'; import { appContextService } from '../../services'; import { defaultIngestErrorHandler } from '../../errors'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; -import { savedObjectToAgent } from '../../services/agents/saved_objects'; import { isAgentUpgradeable } from '../../../common/services'; +import { getAgent } from '../../services/agents'; export const postAgentUpgradeHandler: RequestHandler< TypeOf, @@ -27,6 +22,7 @@ export const postAgentUpgradeHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asInternalUser; const { version, source_uri: sourceUri, force } = request.body; const kibanaVersion = appContextService.getKibanaVersion(); try { @@ -39,12 +35,8 @@ export const postAgentUpgradeHandler: RequestHandler< }, }); } - - const agentSO = await soClient.get( - AGENT_SAVED_OBJECT_TYPE, - request.params.agentId - ); - if (agentSO.attributes.unenrollment_started_at || agentSO.attributes.unenrolled_at) { + const agent = await getAgent(soClient, esClient, request.params.agentId); + if (agent.unenrollment_started_at || agent.unenrolled_at) { return response.customError({ statusCode: 400, body: { @@ -53,7 +45,6 @@ export const postAgentUpgradeHandler: RequestHandler< }); } - const agent = savedObjectToAgent(agentSO); if (!force && !isAgentUpgradeable(agent, kibanaVersion)) { return response.customError({ statusCode: 400, @@ -66,6 +57,7 @@ export const postAgentUpgradeHandler: RequestHandler< try { await AgentService.sendUpgradeAgentAction({ soClient, + esClient, agentId: request.params.agentId, version, sourceUri, diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index dfe5c19bc417b..ca131efeff68c 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -488,17 +488,17 @@ class AgentPolicyService { soClient: SavedObjectsClientContract, agentPolicyId: string ) { - return appContextService.getConfig()?.agents.fleetServerEnabled - ? this.createFleetPolicyChangeFleetServer( - soClient, - appContextService.getInternalUserESClient(), - agentPolicyId - ) - : this.createFleetPolicyChangeActionSO(soClient, agentPolicyId); + const esClient = appContextService.getInternalUserESClient(); + if (appContextService.getConfig()?.agents?.fleetServerEnabled) { + await this.createFleetPolicyChangeFleetServer(soClient, esClient, agentPolicyId); + } + + return this.createFleetPolicyChangeActionSO(soClient, esClient, agentPolicyId); } public async createFleetPolicyChangeActionSO( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agentPolicyId: string ) { // If Agents is not setup skip the creation of POLICY_CHANGE agent actions @@ -518,7 +518,7 @@ class AgentPolicyService { return acc; }, []); - await createAgentPolicyAction(soClient, { + await createAgentPolicyAction(soClient, esClient, { type: 'POLICY_CHANGE', data: { policy }, ack_data: { packages }, diff --git a/x-pack/plugins/fleet/server/services/agents/acks.test.ts b/x-pack/plugins/fleet/server/services/agents/acks.test.ts index c1a6067195c97..5aec696f5e144 100644 --- a/x-pack/plugins/fleet/server/services/agents/acks.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/acks.test.ts @@ -106,19 +106,19 @@ describe('test agent acks services', () => { } as AgentEvent, ] ); - expect(mockSavedObjectsClient.bulkUpdate).toBeCalled(); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(1); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0][0]).toMatchInlineSnapshot(` - Object { - "attributes": Object { + expect(mockSavedObjectsClient.bulkUpdate).not.toBeCalled(); + expect(mockSavedObjectsClient.update).toBeCalled(); + expect(mockSavedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "fleet-agents", + "id", + Object { "packages": Array [ "system", ], "policy_revision": 4, }, - "id": "id", - "type": "fleet-agents", - } + ] `); }); @@ -168,19 +168,19 @@ describe('test agent acks services', () => { } as AgentEvent, ] ); - expect(mockSavedObjectsClient.bulkUpdate).toBeCalled(); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(1); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0][0]).toMatchInlineSnapshot(` - Object { - "attributes": Object { + expect(mockSavedObjectsClient.bulkUpdate).not.toBeCalled(); + expect(mockSavedObjectsClient.update).toBeCalled(); + expect(mockSavedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "fleet-agents", + "id", + Object { "packages": Array [ "system", ], "policy_revision": 4, }, - "id": "id", - "type": "fleet-agents", - } + ] `); }); @@ -230,8 +230,8 @@ describe('test agent acks services', () => { } as AgentEvent, ] ); - expect(mockSavedObjectsClient.bulkUpdate).toBeCalled(); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(0); + expect(mockSavedObjectsClient.bulkUpdate).not.toBeCalled(); + expect(mockSavedObjectsClient.update).not.toBeCalled(); }); it('should not update config field on the agent if a policy change for an old revision is acknowledged', async () => { @@ -277,8 +277,8 @@ describe('test agent acks services', () => { } as AgentEvent, ] ); - expect(mockSavedObjectsClient.bulkUpdate).toBeCalled(); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(0); + expect(mockSavedObjectsClient.bulkUpdate).not.toBeCalled(); + expect(mockSavedObjectsClient.update).not.toBeCalled(); }); it('should fail for actions that cannot be found on agent actions list', async () => { diff --git a/x-pack/plugins/fleet/server/services/agents/acks.ts b/x-pack/plugins/fleet/server/services/agents/acks.ts index a09107b90a015..c639a9b0332ac 100644 --- a/x-pack/plugins/fleet/server/services/agents/acks.ts +++ b/x-pack/plugins/fleet/server/services/agents/acks.ts @@ -24,14 +24,11 @@ import { AgentSOAttributes, AgentActionSOAttributes, } from '../../types'; -import { - AGENT_EVENT_SAVED_OBJECT_TYPE, - AGENT_SAVED_OBJECT_TYPE, - AGENT_ACTION_SAVED_OBJECT_TYPE, -} from '../../constants'; +import { AGENT_EVENT_SAVED_OBJECT_TYPE, AGENT_ACTION_SAVED_OBJECT_TYPE } from '../../constants'; import { getAgentActionByIds } from './actions'; import { forceUnenrollAgent } from './unenroll'; import { ackAgentUpgraded } from './upgrade'; +import { updateAgent } from './crud'; const ALLOWED_ACKNOWLEDGEMENT_TYPE: string[] = ['ACTION_RESULT']; @@ -87,26 +84,23 @@ export async function acknowledgeAgentActions( const upgradeAction = actions.find((action) => action.type === 'UPGRADE'); if (upgradeAction) { - await ackAgentUpgraded(soClient, upgradeAction); + await ackAgentUpgraded(soClient, esClient, upgradeAction); } const configChangeAction = getLatestConfigChangePolicyActionIfUpdated(agent, actions); - await soClient.bulkUpdate([ - ...(configChangeAction - ? [ - { - type: AGENT_SAVED_OBJECT_TYPE, - id: agent.id, - attributes: { - policy_revision: configChangeAction.policy_revision, - packages: configChangeAction?.ack_data?.packages, - }, - }, - ] - : []), - ...buildUpdateAgentActionSentAt(agentActionsIds), - ]); + if (configChangeAction) { + await updateAgent(soClient, esClient, agent.id, { + policy_revision: configChangeAction.policy_revision, + packages: configChangeAction?.ack_data?.packages, + }); + } + + if (agentActionsIds.length > 0) { + await soClient.bulkUpdate([ + ...buildUpdateAgentActionSentAt(agentActionsIds), + ]); + } return actions; } @@ -206,6 +200,7 @@ export interface AcksService { authenticateAgentWithAccessToken: ( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, request: KibanaRequest ) => Promise; diff --git a/x-pack/plugins/fleet/server/services/agents/actions.test.ts b/x-pack/plugins/fleet/server/services/agents/actions.test.ts index 5b3c2ea5ce708..3d391cc89a7e3 100644 --- a/x-pack/plugins/fleet/server/services/agents/actions.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/actions.test.ts @@ -8,12 +8,12 @@ import { createAgentAction } from './actions'; import { SavedObject } from 'kibana/server'; import { AgentAction } from '../../../common/types/models'; -import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { savedObjectsClientMock, elasticsearchServiceMock } from 'src/core/server/mocks'; describe('test agent actions services', () => { it('should create a new action', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); - + const mockedEsClient = elasticsearchServiceMock.createInternalClient(); const newAgentAction: Omit = { agent_id: 'agentid', type: 'POLICY_CHANGE', @@ -32,7 +32,7 @@ describe('test agent actions services', () => { }, } as SavedObject) ); - await createAgentAction(mockSavedObjectsClient, newAgentAction); + await createAgentAction(mockSavedObjectsClient, mockedEsClient, newAgentAction); const createdAction = (mockSavedObjectsClient.create.mock .calls[0][1] as unknown) as AgentAction; diff --git a/x-pack/plugins/fleet/server/services/agents/actions.ts b/x-pack/plugins/fleet/server/services/agents/actions.ts index b45b7836eb46f..8dfeac11dacf3 100644 --- a/x-pack/plugins/fleet/server/services/agents/actions.ts +++ b/x-pack/plugins/fleet/server/services/agents/actions.ts @@ -13,8 +13,9 @@ import { BaseAgentActionSOAttributes, AgentActionSOAttributes, AgentPolicyActionSOAttributes, + FleetServerAgentAction, } from '../../../common/types/models'; -import { AGENT_ACTION_SAVED_OBJECT_TYPE } from '../../../common/constants'; +import { AGENT_ACTION_SAVED_OBJECT_TYPE, AGENT_ACTIONS_INDEX } from '../../../common/constants'; import { isAgentActionSavedObject, isPolicyActionSavedObject, @@ -23,37 +24,45 @@ import { import { appContextService } from '../app_context'; import { nodeTypes } from '../../../../../../src/plugins/data/common'; +const ONE_MONTH_IN_MS = 2592000000; + export async function createAgentAction( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentAction: Omit ): Promise { - return createAction(soClient, newAgentAction); + return createAction(soClient, esClient, newAgentAction); } export async function bulkCreateAgentActions( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentActions: Array> ): Promise { - return bulkCreateActions(soClient, newAgentActions); + return bulkCreateActions(soClient, esClient, newAgentActions); } export function createAgentPolicyAction( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentAction: Omit ): Promise { - return createAction(soClient, newAgentAction); + return createAction(soClient, esClient, newAgentAction); } async function createAction( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentAction: Omit ): Promise; async function createAction( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentAction: Omit ): Promise; async function createAction( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentAction: Omit | Omit ): Promise { const actionSO = await soClient.create( @@ -65,6 +74,27 @@ async function createAction( } ); + if ( + appContextService.getConfig()?.agents?.fleetServerEnabled && + isAgentActionSavedObject(actionSO) + ) { + const body: FleetServerAgentAction = { + '@timestamp': new Date().toISOString(), + expiration: new Date(Date.now() + ONE_MONTH_IN_MS).toISOString(), + agents: [actionSO.attributes.agent_id], + action_id: actionSO.id, + data: newAgentAction.data, + type: newAgentAction.type, + }; + + await esClient.create({ + index: AGENT_ACTIONS_INDEX, + id: actionSO.id, + body, + refresh: 'wait_for', + }); + } + if (isAgentActionSavedObject(actionSO)) { const agentAction = savedObjectToAgentAction(actionSO); // Action `data` is encrypted, so is not returned from the saved object @@ -84,14 +114,17 @@ async function createAction( async function bulkCreateActions( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentActions: Array> ): Promise; async function bulkCreateActions( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentActions: Array> ): Promise; async function bulkCreateActions( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentActions: Array | Omit> ): Promise> { const { saved_objects: actionSOs } = await soClient.bulkCreate( @@ -105,6 +138,34 @@ async function bulkCreateActions( })) ); + if (appContextService.getConfig()?.agents?.fleetServerEnabled) { + await esClient.bulk({ + index: AGENT_ACTIONS_INDEX, + body: actionSOs.flatMap((actionSO) => { + if (!isAgentActionSavedObject(actionSO)) { + return []; + } + const body: FleetServerAgentAction = { + '@timestamp': new Date().toISOString(), + expiration: new Date(Date.now() + ONE_MONTH_IN_MS).toISOString(), + agents: [actionSO.attributes.agent_id], + action_id: actionSO.id, + data: actionSO.attributes.data ? JSON.parse(actionSO.attributes.data) : undefined, + type: actionSO.type, + }; + + return [ + { + create: { + _id: actionSO.id, + }, + }, + body, + ]; + }), + }); + } + return actionSOs.map((actionSO) => { if (isAgentActionSavedObject(actionSO)) { const agentAction = savedObjectToAgentAction(actionSO); @@ -316,6 +377,7 @@ export interface ActionsService { createAgentAction: ( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentAction: Omit ) => Promise; } diff --git a/x-pack/plugins/fleet/server/services/agents/authenticate.test.ts b/x-pack/plugins/fleet/server/services/agents/authenticate.test.ts index c59e2decebd99..5a1e86c15c002 100644 --- a/x-pack/plugins/fleet/server/services/agents/authenticate.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/authenticate.test.ts @@ -6,10 +6,12 @@ */ import { KibanaRequest } from 'kibana/server'; -import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { savedObjectsClientMock, elasticsearchServiceMock } from 'src/core/server/mocks'; import { authenticateAgentWithAccessToken } from './authenticate'; +const mockEsClient = elasticsearchServiceMock.createInternalClient(); + describe('test agent autenticate services', () => { it('should succeed with a valid API key and an active agent', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); @@ -32,7 +34,7 @@ describe('test agent autenticate services', () => { ], }) ); - await authenticateAgentWithAccessToken(mockSavedObjectsClient, { + await authenticateAgentWithAccessToken(mockSavedObjectsClient, mockEsClient, { auth: { isAuthenticated: true }, headers: { authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==', @@ -62,7 +64,7 @@ describe('test agent autenticate services', () => { }) ); expect( - authenticateAgentWithAccessToken(mockSavedObjectsClient, { + authenticateAgentWithAccessToken(mockSavedObjectsClient, mockEsClient, { auth: { isAuthenticated: false }, headers: { authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==', @@ -93,7 +95,7 @@ describe('test agent autenticate services', () => { }) ); expect( - authenticateAgentWithAccessToken(mockSavedObjectsClient, { + authenticateAgentWithAccessToken(mockSavedObjectsClient, mockEsClient, { auth: { isAuthenticated: true }, headers: { authorization: 'aaaa', @@ -124,7 +126,7 @@ describe('test agent autenticate services', () => { }) ); expect( - authenticateAgentWithAccessToken(mockSavedObjectsClient, { + authenticateAgentWithAccessToken(mockSavedObjectsClient, mockEsClient, { auth: { isAuthenticated: true }, headers: { authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==', @@ -144,7 +146,7 @@ describe('test agent autenticate services', () => { }) ); expect( - authenticateAgentWithAccessToken(mockSavedObjectsClient, { + authenticateAgentWithAccessToken(mockSavedObjectsClient, mockEsClient, { auth: { isAuthenticated: true }, headers: { authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==', diff --git a/x-pack/plugins/fleet/server/services/agents/authenticate.ts b/x-pack/plugins/fleet/server/services/agents/authenticate.ts index a773173b1ddc1..a03c35bdc6e73 100644 --- a/x-pack/plugins/fleet/server/services/agents/authenticate.ts +++ b/x-pack/plugins/fleet/server/services/agents/authenticate.ts @@ -6,13 +6,14 @@ */ import Boom from '@hapi/boom'; -import { KibanaRequest, SavedObjectsClientContract } from 'src/core/server'; +import { KibanaRequest, SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; import { Agent } from '../../types'; import * as APIKeyService from '../api_keys'; import { getAgentByAccessAPIKeyId } from './crud'; export async function authenticateAgentWithAccessToken( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, request: KibanaRequest ): Promise { if (!request.auth.isAuthenticated) { @@ -25,7 +26,7 @@ export async function authenticateAgentWithAccessToken( throw Boom.unauthorized(err.message); } - const agent = await getAgentByAccessAPIKeyId(soClient, res.apiKeyId); + const agent = await getAgentByAccessAPIKeyId(soClient, esClient, res.apiKeyId); return agent; } diff --git a/x-pack/plugins/fleet/server/services/agents/checkin/index.ts b/x-pack/plugins/fleet/server/services/agents/checkin/index.ts index 9a60abdc69423..bcebedae2e07a 100644 --- a/x-pack/plugins/fleet/server/services/agents/checkin/index.ts +++ b/x-pack/plugins/fleet/server/services/agents/checkin/index.ts @@ -19,9 +19,10 @@ import { AgentEventSOAttributes, } from '../../../types'; -import { AGENT_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../../constants'; +import { AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../../constants'; import { agentCheckinState } from './state'; import { getAgentActionsForCheckin } from '../actions'; +import { updateAgent } from '../crud'; export async function agentCheckin( soClient: SavedObjectsClientContract, @@ -35,13 +36,7 @@ export async function agentCheckin( options?: { signal: AbortSignal } ) { const updateData: Partial = {}; - const { updatedErrorEvents } = await processEventsForCheckin(soClient, agent, data.events); - if ( - updatedErrorEvents && - !(updatedErrorEvents.length === 0 && agent.current_error_events.length === 0) - ) { - updateData.current_error_events = JSON.stringify(updatedErrorEvents); - } + await processEventsForCheckin(soClient, agent, data.events); if (data.localMetadata && !deepEqual(data.localMetadata, agent.local_metadata)) { updateData.local_metadata = data.localMetadata; } @@ -50,9 +45,8 @@ export async function agentCheckin( } // Update agent only if something changed if (Object.keys(updateData).length > 0) { - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agent.id, updateData); + await updateAgent(soClient, esClient, agent.id, updateData); } - // Check if some actions are not acknowledged let actions = await getAgentActionsForCheckin(soClient, agent.id); if (actions.length > 0) { diff --git a/x-pack/plugins/fleet/server/services/agents/checkin/state_connected_agents.ts b/x-pack/plugins/fleet/server/services/agents/checkin/state_connected_agents.ts index 83fd139a1e8e8..6156212a63203 100644 --- a/x-pack/plugins/fleet/server/services/agents/checkin/state_connected_agents.ts +++ b/x-pack/plugins/fleet/server/services/agents/checkin/state_connected_agents.ts @@ -5,10 +5,9 @@ * 2.0. */ -import { KibanaRequest, SavedObjectsBulkUpdateObject } from 'src/core/server'; +import { KibanaRequest } from 'src/core/server'; import { appContextService } from '../../app_context'; -import { AgentSOAttributes } from '../../../types'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; +import { bulkUpdateAgents } from '../crud'; function getInternalUserSOClient() { const fakeRequest = ({ @@ -57,20 +56,17 @@ export function agentCheckinStateConnectedAgentsFactory() { if (agentToUpdate.size === 0) { return; } + const esClient = appContextService.getInternalUserESClient(); const internalSOClient = getInternalUserSOClient(); const now = new Date().toISOString(); - const updates: Array> = [ - ...agentToUpdate.values(), - ].map((agentId) => ({ - type: AGENT_SAVED_OBJECT_TYPE, - id: agentId, - attributes: { + const updates = [...agentToUpdate.values()].map((agentId) => ({ + agentId, + data: { last_checkin: now, }, })); - agentToUpdate = new Set([...connectedAgentsIds.values()]); - await internalSOClient.bulkUpdate(updates, { refresh: false }); + await bulkUpdateAgents(internalSOClient, esClient, updates); } return { diff --git a/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.test.ts b/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.test.ts index ca8378c117b7d..cd6e0ef61e3f0 100644 --- a/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import { savedObjectsClientMock } from 'src/core/server/mocks'; import { take } from 'rxjs/operators'; import { @@ -17,6 +18,7 @@ import { outputType } from '../../../../common/constants'; jest.mock('../../app_context', () => ({ appContextService: { + getConfig: () => ({}), getInternalUserSOClient: () => { return {}; }, @@ -42,6 +44,8 @@ function getMockedNewActionSince() { return getNewActionsSince as jest.MockedFunction; } +const mockedEsClient = {} as ElasticsearchClient; + describe('test agent checkin new action services', () => { describe('newAgetActionObservable', () => { beforeEach(() => { @@ -161,12 +165,18 @@ describe('test agent checkin new action services', () => { ]; expect( - await createAgentActionFromPolicyAction(mockSavedObjectsClient, mockAgent, mockPolicyAction) + await createAgentActionFromPolicyAction( + mockSavedObjectsClient, + mockedEsClient, + mockAgent, + mockPolicyAction + ) ).toEqual(expectedResult); expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '7.10.0-SNAPSHOT' } } } }, mockPolicyAction ) @@ -175,6 +185,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '7.10.2' } } } }, mockPolicyAction ) @@ -183,6 +194,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '8.0.0' } } } }, mockPolicyAction ) @@ -191,6 +203,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '8.0.0-SNAPSHOT' } } } }, mockPolicyAction ) @@ -218,6 +231,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '7.9.0' } } } }, mockPolicyAction ) @@ -226,6 +240,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '7.9.3' } } } }, mockPolicyAction ) @@ -234,6 +249,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '7.9.1-SNAPSHOT' } } } }, mockPolicyAction ) @@ -242,6 +258,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '7.8.2' } } } }, mockPolicyAction ) diff --git a/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts b/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts index 624b7bbcae572..01759c2015cdf 100644 --- a/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts +++ b/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts @@ -45,7 +45,7 @@ import { } from '../actions'; import { appContextService } from '../../app_context'; import { toPromiseAbortable, AbortError, createRateLimiter } from './rxjs_utils'; -import { getAgent } from '../crud'; +import { getAgent, updateAgent } from '../crud'; function getInternalUserSOClient() { const fakeRequest = ({ @@ -106,31 +106,45 @@ function createAgentPolicyActionSharedObservable(agentPolicyId: string) { ); } -async function getOrCreateAgentDefaultOutputAPIKey( +async function getAgentDefaultOutputAPIKey( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agent: Agent -): Promise { - const { - attributes: { default_api_key: defaultApiKey }, - } = await appContextService - .getEncryptedSavedObjects() - .getDecryptedAsInternalUser(AGENT_SAVED_OBJECT_TYPE, agent.id); +) { + if (appContextService.getConfig()?.agents?.fleetServerEnabled) { + return agent.default_api_key; + } else { + const { + attributes: { default_api_key: defaultApiKey }, + } = await appContextService + .getEncryptedSavedObjects() + .getDecryptedAsInternalUser(AGENT_SAVED_OBJECT_TYPE, agent.id); - if (defaultApiKey) { return defaultApiKey; } +} + +async function getOrCreateAgentDefaultOutputAPIKey( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + agent: Agent +): Promise { + const defaultAPIKey = await getAgentDefaultOutputAPIKey(soClient, esClient, agent); + if (defaultAPIKey) { + return defaultAPIKey; + } const outputAPIKey = await APIKeysService.generateOutputApiKey(soClient, 'default', agent.id); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agent.id, { + await updateAgent(soClient, esClient, agent.id, { default_api_key: outputAPIKey.key, default_api_key_id: outputAPIKey.id, }); - return outputAPIKey.key; } export async function createAgentActionFromPolicyAction( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agent: Agent, policyAction: AgentPolicyAction ) { @@ -168,7 +182,7 @@ export async function createAgentActionFromPolicyAction( ); // Mutate the policy to set the api token for this agent - const apiKey = await getOrCreateAgentDefaultOutputAPIKey(soClient, agent); + const apiKey = await getOrCreateAgentDefaultOutputAPIKey(soClient, esClient, agent); if (newAgentAction.data.policy) { newAgentAction.data.policy.outputs.default.api_key = apiKey; } @@ -249,7 +263,9 @@ export function agentCheckinStateNewActionsFactory() { (!agent.policy_revision || action.policy_revision > agent.policy_revision) ), rateLimiter(), - concatMap((policyAction) => createAgentActionFromPolicyAction(soClient, agent, policyAction)), + concatMap((policyAction) => + createAgentActionFromPolicyAction(soClient, esClient, agent, policyAction) + ), merge(newActions$), concatMap((data: AgentAction[] | undefined) => { if (data === undefined) { @@ -274,7 +290,7 @@ export function agentCheckinStateNewActionsFactory() { }), rateLimiter(), concatMap((policyAction) => - createAgentActionFromPolicyAction(soClient, agent, policyAction) + createAgentActionFromPolicyAction(soClient, esClient, agent, policyAction) ) ); } diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index 36506d0590595..c80fd77fc11ec 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -5,13 +5,8 @@ * 2.0. */ -import Boom from '@hapi/boom'; import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; - -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { AgentSOAttributes, Agent, ListWithKuery } from '../../types'; -import { escapeSearchQueryPhrase } from '../saved_object'; -import { savedObjectToAgent } from './saved_objects'; import { appContextService, agentPolicyService } from '../../services'; import * as crudServiceSO from './crud_so'; import * as crudServiceFleetServer from './crud_fleet_server'; @@ -75,15 +70,15 @@ export async function getAgent( : crudServiceSO.getAgent(soClient, agentId); } -export async function getAgents(soClient: SavedObjectsClientContract, agentIds: string[]) { - const agentSOs = await soClient.bulkGet( - agentIds.map((agentId) => ({ - id: agentId, - type: AGENT_SAVED_OBJECT_TYPE, - })) - ); - const agents = agentSOs.saved_objects.map(savedObjectToAgent); - return agents; +export async function getAgents( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + agentIds: string[] +) { + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + return fleetServerEnabled + ? crudServiceFleetServer.getAgents(esClient, agentIds) + : crudServiceSO.getAgents(soClient, agentIds); } export async function getAgentPolicyForAgent( @@ -104,38 +99,39 @@ export async function getAgentPolicyForAgent( export async function getAgentByAccessAPIKeyId( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, accessAPIKeyId: string ): Promise { - const response = await soClient.find({ - type: AGENT_SAVED_OBJECT_TYPE, - searchFields: ['access_api_key_id'], - search: escapeSearchQueryPhrase(accessAPIKeyId), - }); - const [agent] = response.saved_objects.map(savedObjectToAgent); - - if (!agent) { - throw Boom.notFound('Agent not found'); - } - if (agent.access_api_key_id !== accessAPIKeyId) { - throw new Error('Agent api key id is not matching'); - } - if (!agent.active) { - throw Boom.forbidden('Agent inactive'); - } - - return agent; + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + return fleetServerEnabled + ? crudServiceFleetServer.getAgentByAccessAPIKeyId(esClient, accessAPIKeyId) + : crudServiceSO.getAgentByAccessAPIKeyId(soClient, accessAPIKeyId); } export async function updateAgent( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agentId: string, - data: { - userProvidedMetatada: any; - } + data: Partial +) { + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + return fleetServerEnabled + ? crudServiceFleetServer.updateAgent(esClient, agentId, data) + : crudServiceSO.updateAgent(soClient, agentId, data); +} + +export async function bulkUpdateAgents( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + data: Array<{ + agentId: string; + data: Partial; + }> ) { - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { - user_provided_metadata: data.userProvidedMetatada, - }); + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + return fleetServerEnabled + ? crudServiceFleetServer.bulkUpdateAgents(esClient, data) + : crudServiceSO.bulkUpdateAgents(soClient, data); } export async function deleteAgent( diff --git a/x-pack/plugins/fleet/server/services/agents/crud_fleet_server.ts b/x-pack/plugins/fleet/server/services/agents/crud_fleet_server.ts index c9aa221edf4d2..caff15efff68c 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud_fleet_server.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud_fleet_server.ts @@ -6,31 +6,46 @@ */ import Boom from '@hapi/boom'; -import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; +import { SearchResponse } from 'elasticsearch'; +import { ElasticsearchClient } from 'src/core/server'; -import { isAgentUpgradeable, SO_SEARCH_LIMIT } from '../../../common'; +import { FleetServerAgent, isAgentUpgradeable, SO_SEARCH_LIMIT } from '../../../common'; import { AGENT_SAVED_OBJECT_TYPE, AGENTS_INDEX } from '../../constants'; import { ESSearchHit } from '../../../../../typings/elasticsearch'; import { AgentSOAttributes, Agent, ListWithKuery } from '../../types'; import { escapeSearchQueryPhrase, normalizeKuery } from '../saved_object'; -import { savedObjectToAgent } from './saved_objects'; -import { searchHitToAgent } from './helpers'; +import { searchHitToAgent, agentSOAttributesToFleetServerAgentDoc } from './helpers'; import { appContextService } from '../../services'; +import { esKuery, KueryNode } from '../../../../../../src/plugins/data/server'; const ACTIVE_AGENT_CONDITION = 'active:true'; const INACTIVE_AGENT_CONDITION = `NOT (${ACTIVE_AGENT_CONDITION})`; -function _joinFilters(filters: string[], operator = 'AND') { - return filters.reduce((acc: string | undefined, filter) => { - if (acc) { - return `${acc} ${operator} (${filter})`; - } +function _joinFilters(filters: Array): KueryNode | undefined { + return filters + .filter((filter) => filter !== undefined) + .reduce((acc: KueryNode | undefined, kuery: string | KueryNode | undefined): + | KueryNode + | undefined => { + if (kuery === undefined) { + return acc; + } + const kueryNode: KueryNode = + typeof kuery === 'string' ? esKuery.fromKueryExpression(removeSOAttributes(kuery)) : kuery; - return `(${filter})`; - }, undefined); + if (!acc) { + return kueryNode; + } + + return { + type: 'function', + function: 'and', + arguments: [acc, kueryNode], + }; + }, undefined as KueryNode | undefined); } -function removeSOAttributes(kuery: string) { +export function removeSOAttributes(kuery: string) { return kuery.replace(/attributes\./g, '').replace(/fleet-agents\./g, ''); } @@ -57,20 +72,23 @@ export async function listAgents( const filters = []; if (kuery && kuery !== '') { - filters.push(removeSOAttributes(kuery)); + filters.push(kuery); } if (showInactive === false) { filters.push(ACTIVE_AGENT_CONDITION); } + const kueryNode = _joinFilters(filters); + const body = kueryNode ? { query: esKuery.toElasticsearchQuery(kueryNode) } : {}; + const res = await esClient.search({ index: AGENTS_INDEX, from: (page - 1) * perPage, size: perPage, sort: `${sortField}:${sortOrder}`, track_total_hits: true, - q: _joinFilters(filters), + body, }); let agentResults: Agent[] = res.body.hits.hits.map(searchHitToAgent); @@ -121,18 +139,20 @@ export async function countInactiveAgents( filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); } + const kueryNode = _joinFilters(filters); + const body = kueryNode ? { query: esKuery.toElasticsearchQuery(kueryNode) } : {}; + const res = await esClient.search({ index: AGENTS_INDEX, size: 0, track_total_hits: true, - q: _joinFilters(filters), + body, }); - return res.body.hits.total.value; } export async function getAgent(esClient: ElasticsearchClient, agentId: string) { - const agentHit = await esClient.get>({ + const agentHit = await esClient.get>({ index: AGENTS_INDEX, id: agentId, }); @@ -141,27 +161,31 @@ export async function getAgent(esClient: ElasticsearchClient, agentId: string) { return agent; } -export async function getAgents(soClient: SavedObjectsClientContract, agentIds: string[]) { - const agentSOs = await soClient.bulkGet( - agentIds.map((agentId) => ({ - id: agentId, - type: AGENT_SAVED_OBJECT_TYPE, - })) - ); - const agents = agentSOs.saved_objects.map(savedObjectToAgent); +export async function getAgents( + esClient: ElasticsearchClient, + agentIds: string[] +): Promise { + const body = { docs: agentIds.map((_id) => ({ _id })) }; + + const res = await esClient.mget({ + body, + index: AGENTS_INDEX, + }); + + const agents = res.body.docs.map(searchHitToAgent); return agents; } export async function getAgentByAccessAPIKeyId( - soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, accessAPIKeyId: string ): Promise { - const response = await soClient.find({ - type: AGENT_SAVED_OBJECT_TYPE, - searchFields: ['access_api_key_id'], - search: escapeSearchQueryPhrase(accessAPIKeyId), + const res = await esClient.search>({ + index: AGENTS_INDEX, + q: `access_api_key_id:${escapeSearchQueryPhrase(accessAPIKeyId)}`, }); - const [agent] = response.saved_objects.map(savedObjectToAgent); + + const [agent] = res.body.hits.hits.map(searchHitToAgent); if (!agent) { throw Boom.notFound('Agent not found'); @@ -177,15 +201,49 @@ export async function getAgentByAccessAPIKeyId( } export async function updateAgent( - soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agentId: string, - data: { - userProvidedMetatada: any; - } + data: Partial ) { - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { - user_provided_metadata: data.userProvidedMetatada, + await esClient.update({ + id: agentId, + index: AGENTS_INDEX, + body: { doc: agentSOAttributesToFleetServerAgentDoc(data) }, + refresh: 'wait_for', + }); +} + +export async function bulkUpdateAgents( + esClient: ElasticsearchClient, + updateData: Array<{ + agentId: string; + data: Partial; + }> +) { + const body = updateData.flatMap(({ agentId, data }) => [ + { + update: { + _id: agentId, + }, + }, + { + doc: { ...agentSOAttributesToFleetServerAgentDoc(data) }, + }, + ]); + + const res = await esClient.bulk({ + body, + index: AGENTS_INDEX, + refresh: 'wait_for', }); + + return { + items: res.body.items.map((item: { update: { _id: string; error?: Error } }) => ({ + id: item.update._id, + success: !item.update.error, + error: item.update.error, + })), + }; } export async function deleteAgent(esClient: ElasticsearchClient, agentId: string) { @@ -193,7 +251,7 @@ export async function deleteAgent(esClient: ElasticsearchClient, agentId: string id: agentId, index: AGENT_SAVED_OBJECT_TYPE, body: { - active: false, + doc: { active: false }, }, }); } diff --git a/x-pack/plugins/fleet/server/services/agents/crud_so.ts b/x-pack/plugins/fleet/server/services/agents/crud_so.ts index 11991a971829a..c3ceb4b7502e2 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud_so.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud_so.ts @@ -6,7 +6,7 @@ */ import Boom from '@hapi/boom'; -import { SavedObjectsClientContract } from 'src/core/server'; +import { SavedObjectsBulkUpdateObject, SavedObjectsClientContract } from 'src/core/server'; import { isAgentUpgradeable } from '../../../common'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; @@ -197,13 +197,35 @@ export async function getAgentByAccessAPIKeyId( export async function updateAgent( soClient: SavedObjectsClientContract, agentId: string, - data: { - userProvidedMetatada: any; - } + data: Partial ) { - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { - user_provided_metadata: data.userProvidedMetatada, - }); + await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, data); +} + +export async function bulkUpdateAgents( + soClient: SavedObjectsClientContract, + updateData: Array<{ + agentId: string; + data: Partial; + }> +) { + const updates: Array> = updateData.map( + ({ agentId, data }) => ({ + type: AGENT_SAVED_OBJECT_TYPE, + id: agentId, + attributes: data, + }) + ); + + const res = await soClient.bulkUpdate(updates); + + return { + items: res.saved_objects.map((so) => ({ + id: so.id, + success: !so.error, + error: so.error, + })), + }; } export async function deleteAgent(soClient: SavedObjectsClientContract, agentId: string) { diff --git a/x-pack/plugins/fleet/server/services/agents/enroll.ts b/x-pack/plugins/fleet/server/services/agents/enroll.ts index b8be02af101b4..c984a84ceea01 100644 --- a/x-pack/plugins/fleet/server/services/agents/enroll.ts +++ b/x-pack/plugins/fleet/server/services/agents/enroll.ts @@ -6,14 +6,15 @@ */ import Boom from '@hapi/boom'; +import uuid from 'uuid/v4'; import semverParse from 'semver/functions/parse'; import semverDiff from 'semver/functions/diff'; import semverLte from 'semver/functions/lte'; import { SavedObjectsClientContract } from 'src/core/server'; -import { AgentType, Agent, AgentSOAttributes } from '../../types'; +import { AgentType, Agent, AgentSOAttributes, FleetServerAgent } from '../../types'; import { savedObjectToAgent } from './saved_objects'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; +import { AGENT_SAVED_OBJECT_TYPE, AGENTS_INDEX } from '../../constants'; import * as APIKeyService from '../api_keys'; import { appContextService } from '../app_context'; @@ -26,6 +27,36 @@ export async function enroll( const agentVersion = metadata?.local?.elastic?.agent?.version; validateAgentVersion(agentVersion); + if (appContextService.getConfig()?.agents?.fleetServerEnabled) { + const esClient = appContextService.getInternalUserESClient(); + + const agentId = uuid(); + const accessAPIKey = await APIKeyService.generateAccessApiKey(soClient, agentId); + const fleetServerAgent: FleetServerAgent = { + active: true, + policy_id: agentPolicyId, + type, + enrolled_at: new Date().toISOString(), + user_provided_metadata: metadata?.userProvided ?? {}, + local_metadata: metadata?.local ?? {}, + access_api_key_id: accessAPIKey.id, + }; + await esClient.create({ + index: AGENTS_INDEX, + body: fleetServerAgent, + id: agentId, + refresh: 'wait_for', + }); + + return { + id: agentId, + current_error_events: [], + packages: [], + ...fleetServerAgent, + access_api_key: accessAPIKey.key, + } as Agent; + } + const agentData: AgentSOAttributes = { active: true, policy_id: agentPolicyId, diff --git a/x-pack/plugins/fleet/server/services/agents/helpers.ts b/x-pack/plugins/fleet/server/services/agents/helpers.ts index 1000a1b145932..90d85e98ecd67 100644 --- a/x-pack/plugins/fleet/server/services/agents/helpers.ts +++ b/x-pack/plugins/fleet/server/services/agents/helpers.ts @@ -6,17 +6,30 @@ */ import { ESSearchHit } from '../../../../../typings/elasticsearch'; -import { Agent, AgentSOAttributes } from '../../types'; +import { Agent, AgentSOAttributes, FleetServerAgent } from '../../types'; -export function searchHitToAgent(hit: ESSearchHit): Agent { +export function searchHitToAgent(hit: ESSearchHit): Agent { return { id: hit._id, ...hit._source, - current_error_events: hit._source.current_error_events - ? JSON.parse(hit._source.current_error_events) - : [], + policy_revision: hit._source.policy_revision_idx, + current_error_events: [], access_api_key: undefined, status: undefined, packages: hit._source.packages ?? [], }; } + +export function agentSOAttributesToFleetServerAgentDoc( + data: Partial +): Partial> { + const { policy_revision: policyRevison, ...rest } = data; + + const doc: Partial> = { ...rest }; + + if (policyRevison !== undefined) { + doc.policy_revision_idx = policyRevison; + } + + return doc; +} diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.test.ts b/x-pack/plugins/fleet/server/services/agents/reassign.test.ts index 7338c440483ea..466870bead71c 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.test.ts @@ -107,6 +107,9 @@ function createClientMock() { saved_objects: [await soClientMock.create(type, attributes)], }; }); + soClientMock.bulkUpdate.mockResolvedValue({ + saved_objects: [], + }); soClientMock.get.mockImplementation(async (_, id) => { switch (id) { diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.ts b/x-pack/plugins/fleet/server/services/agents/reassign.ts index 9f4373ab553ec..62d59aada3b7b 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.ts @@ -7,11 +7,16 @@ import type { SavedObjectsClientContract, ElasticsearchClient } from 'kibana/server'; import Boom from '@hapi/boom'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; -import type { AgentSOAttributes } from '../../types'; -import { AgentReassignmentError } from '../../errors'; import { agentPolicyService } from '../agent_policy'; -import { getAgentPolicyForAgent, getAgents, listAllAgents } from './crud'; +import { + getAgents, + getAgentPolicyForAgent, + listAllAgents, + updateAgent, + bulkUpdateAgents, +} from './crud'; +import { AgentReassignmentError } from '../../errors'; + import { createAgentAction, bulkCreateAgentActions } from './actions'; export async function reassignAgent( @@ -27,12 +32,12 @@ export async function reassignAgent( await reassignAgentIsAllowed(soClient, esClient, agentId, newAgentPolicyId); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { + await updateAgent(soClient, esClient, agentId, { policy_id: newAgentPolicyId, policy_revision: null, }); - await createAgentAction(soClient, { + await createAgentAction(soClient, esClient, { agent_id: agentId, created_at: new Date().toISOString(), type: 'INTERNAL_POLICY_REASSIGN', @@ -73,7 +78,7 @@ export async function reassignAgents( kuery: string; }, newAgentPolicyId: string -) { +): Promise<{ items: Array<{ id: string; sucess: boolean; error?: Error }> }> { const agentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId); if (!agentPolicy) { throw Boom.notFound(`Agent policy not found: ${newAgentPolicyId}`); @@ -82,7 +87,7 @@ export async function reassignAgents( // Filter to agents that do not already use the new agent policy ID const agents = 'agentIds' in options - ? await getAgents(soClient, options.agentIds) + ? await getAgents(soClient, esClient, options.agentIds) : ( await listAllAgents(soClient, esClient, { kuery: options.kuery, @@ -99,20 +104,22 @@ export async function reassignAgents( (agent, index) => settled[index].status === 'fulfilled' && agent.policy_id !== newAgentPolicyId ); - // Update the necessary agents - const res = await soClient.bulkUpdate( + const res = await bulkUpdateAgents( + soClient, + esClient, agentsToUpdate.map((agent) => ({ - type: AGENT_SAVED_OBJECT_TYPE, - id: agent.id, - attributes: { + agentId: agent.id, + data: { policy_id: newAgentPolicyId, policy_revision: null, }, })) ); + const now = new Date().toISOString(); await bulkCreateAgentActions( soClient, + esClient, agentsToUpdate.map((agent) => ({ agent_id: agent.id, created_at: now, diff --git a/x-pack/plugins/fleet/server/services/agents/status.ts b/x-pack/plugins/fleet/server/services/agents/status.ts index c75b91b3fbd11..42d3aff2b0d70 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.ts @@ -14,6 +14,8 @@ import { AgentStatus } from '../../types'; import { AgentStatusKueryHelper } from '../../../common/services'; import { esKuery, KueryNode } from '../../../../../../src/plugins/data/server'; import { normalizeKuery } from '../saved_object'; +import { appContextService } from '../app_context'; +import { removeSOAttributes } from './crud_fleet_server'; export async function getAgentStatusById( soClient: SavedObjectsClientContract, @@ -27,6 +29,8 @@ export async function getAgentStatusById( export const getAgentStatus = AgentStatusKueryHelper.getAgentStatus; function joinKuerys(...kuerys: Array) { + const isFleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + return kuerys .filter((kuery) => kuery !== undefined) .reduce((acc: KueryNode | undefined, kuery: string | undefined): KueryNode | undefined => { @@ -34,7 +38,9 @@ function joinKuerys(...kuerys: Array) { return acc; } const normalizedKuery: KueryNode = esKuery.fromKueryExpression( - normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery || '') + isFleetServerEnabled + ? removeSOAttributes(kuery || '') + : normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery || '') ); if (!acc) { diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts index b8c1b7befb443..cd46cff0f8a17 100644 --- a/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts @@ -73,6 +73,7 @@ describe('unenrollAgents (plural)', () => { }); it('cannot unenroll from a managed policy', async () => { const soClient = createClientMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const idsToUnenroll = [agentInUnmanagedSO.id, agentInManagedSO.id, agentInUnmanagedSO2.id]; await unenrollAgents(soClient, esClient, { agentIds: idsToUnenroll }); @@ -98,6 +99,9 @@ function createClientMock() { saved_objects: [await soClientMock.create(type, attributes)], }; }); + soClientMock.bulkUpdate.mockResolvedValue({ + saved_objects: [], + }); soClientMock.get.mockImplementation(async (_, id) => { switch (id) { diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.ts index e2fa83cf32b63..72d551a122980 100644 --- a/x-pack/plugins/fleet/server/services/agents/unenroll.ts +++ b/x-pack/plugins/fleet/server/services/agents/unenroll.ts @@ -4,13 +4,19 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; -import type { AgentSOAttributes } from '../../types'; -import { AgentUnenrollmentError } from '../../errors'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; + +import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; import * as APIKeyService from '../api_keys'; import { createAgentAction, bulkCreateAgentActions } from './actions'; -import { getAgent, getAgentPolicyForAgent, getAgents, listAllAgents } from './crud'; +import { + getAgent, + updateAgent, + getAgentPolicyForAgent, + getAgents, + listAllAgents, + bulkUpdateAgents, +} from './crud'; +import { AgentUnenrollmentError } from '../../errors'; async function unenrollAgentIsAllowed( soClient: SavedObjectsClientContract, @@ -35,12 +41,12 @@ export async function unenrollAgent( await unenrollAgentIsAllowed(soClient, esClient, agentId); const now = new Date().toISOString(); - await createAgentAction(soClient, { + await createAgentAction(soClient, esClient, { agent_id: agentId, created_at: now, type: 'UNENROLL', }); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { + await updateAgent(soClient, esClient, agentId, { unenrollment_started_at: now, }); } @@ -58,7 +64,7 @@ export async function unenrollAgents( ) { const agents = 'agentIds' in options - ? await getAgents(soClient, options.agentIds) + ? await getAgents(soClient, esClient, options.agentIds) : ( await listAllAgents(soClient, esClient, { kuery: options.kuery, @@ -83,6 +89,7 @@ export async function unenrollAgents( // Create unenroll action for each agent await bulkCreateAgentActions( soClient, + esClient, agentsToUpdate.map((agent) => ({ agent_id: agent.id, created_at: now, @@ -91,11 +98,12 @@ export async function unenrollAgents( ); // Update the necessary agents - return await soClient.bulkUpdate( + return bulkUpdateAgents( + soClient, + esClient, agentsToUpdate.map((agent) => ({ - type: AGENT_SAVED_OBJECT_TYPE, - id: agent.id, - attributes: { + agentId: agent.id, + data: { unenrollment_started_at: now, }, })) @@ -118,7 +126,7 @@ export async function forceUnenrollAgent( : undefined, ]); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { + await updateAgent(soClient, esClient, agentId, { active: false, unenrolled_at: new Date().toISOString(), }); @@ -138,7 +146,7 @@ export async function forceUnenrollAgents( // Filter to agents that are not already unenrolled const agents = 'agentIds' in options - ? await getAgents(soClient, options.agentIds) + ? await getAgents(soClient, esClient, options.agentIds) : ( await listAllAgents(soClient, esClient, { kuery: options.kuery, @@ -163,13 +171,13 @@ export async function forceUnenrollAgents( if (apiKeys.length) { APIKeyService.invalidateAPIKeys(soClient, apiKeys); } - // Update the necessary agents - return await soClient.bulkUpdate( + return bulkUpdateAgents( + soClient, + esClient, agentsToUpdate.map((agent) => ({ - type: AGENT_SAVED_OBJECT_TYPE, - id: agent.id, - attributes: { + agentId: agent.id, + data: { active: false, unenrolled_at: now, }, diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.ts index 7475ad4968142..5105e14530982 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade.ts @@ -6,20 +6,22 @@ */ import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; -import { AgentSOAttributes, AgentAction, AgentActionSOAttributes } from '../../types'; -import { AGENT_ACTION_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE } from '../../constants'; +import { AgentAction, AgentActionSOAttributes } from '../../types'; +import { AGENT_ACTION_SAVED_OBJECT_TYPE } from '../../constants'; import { bulkCreateAgentActions, createAgentAction } from './actions'; -import { getAgents, listAllAgents } from './crud'; +import { getAgents, listAllAgents, updateAgent, bulkUpdateAgents } from './crud'; import { isAgentUpgradeable } from '../../../common/services'; import { appContextService } from '../app_context'; export async function sendUpgradeAgentAction({ soClient, + esClient, agentId, version, sourceUri, }: { soClient: SavedObjectsClientContract; + esClient: ElasticsearchClient; agentId: string; version: string; sourceUri: string | undefined; @@ -29,21 +31,22 @@ export async function sendUpgradeAgentAction({ version, source_uri: sourceUri, }; - await createAgentAction(soClient, { + await createAgentAction(soClient, esClient, { agent_id: agentId, created_at: now, data, ack_data: data, type: 'UPGRADE', }); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { - upgraded_at: undefined, + await updateAgent(soClient, esClient, agentId, { + upgraded_at: null, upgrade_started_at: now, }); } export async function ackAgentUpgraded( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agentAction: AgentAction ) { const { @@ -52,9 +55,9 @@ export async function ackAgentUpgraded( if (!ackData) throw new Error('data missing from UPGRADE action'); const { version } = JSON.parse(ackData); if (!version) throw new Error('version missing from UPGRADE action'); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentAction.agent_id, { + await updateAgent(soClient, esClient, agentAction.agent_id, { upgraded_at: new Date().toISOString(), - upgrade_started_at: undefined, + upgrade_started_at: null, }); } @@ -79,7 +82,7 @@ export async function sendUpgradeAgentsActions( // Filter out agents currently unenrolling, agents unenrolled, and agents not upgradeable const agents = 'agentIds' in options - ? await getAgents(soClient, options.agentIds) + ? await getAgents(soClient, esClient, options.agentIds) : ( await listAllAgents(soClient, esClient, { kuery: options.kuery, @@ -97,6 +100,7 @@ export async function sendUpgradeAgentsActions( // Create upgrade action for each agent await bulkCreateAgentActions( soClient, + esClient, agentsToUpdate.map((agent) => ({ agent_id: agent.id, created_at: now, @@ -106,12 +110,13 @@ export async function sendUpgradeAgentsActions( })) ); - return await soClient.bulkUpdate( + return await bulkUpdateAgents( + soClient, + esClient, agentsToUpdate.map((agent) => ({ - type: AGENT_SAVED_OBJECT_TYPE, - id: agent.id, - attributes: { - upgraded_at: undefined, + agentId: agent.id, + data: { + upgraded_at: null, upgrade_started_at: now, }, })) diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts index 97d48702cf4c6..85812fee3885c 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts @@ -79,3 +79,15 @@ export async function generateEnrollmentAPIKey( return enrollmentApiKeyServiceSO.generateEnrollmentAPIKey(soClient, data); } } + +export async function getEnrollmentAPIKeyById( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + apiKeyId: string +) { + if (appContextService.getConfig()?.agents?.fleetServerEnabled === true) { + return enrollmentApiKeyServiceFleetServer.getEnrollmentAPIKeyById(esClient, apiKeyId); + } else { + return enrollmentApiKeyServiceSO.getEnrollmentAPIKeyById(soClient, apiKeyId); + } +} diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_fleet_server.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_fleet_server.ts index d42cb19a340bd..f5d0015297daa 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_fleet_server.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_fleet_server.ts @@ -7,41 +7,15 @@ import uuid from 'uuid'; import Boom from '@hapi/boom'; +import { GetResponse } from 'elasticsearch'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; +import { ESSearchResponse as SearchResponse } from '../../../../../typings/elasticsearch'; import { EnrollmentAPIKey, FleetServerEnrollmentAPIKey } from '../../types'; import { ENROLLMENT_API_KEYS_INDEX } from '../../constants'; import { createAPIKey, invalidateAPIKeys } from './security'; import { agentPolicyService } from '../agent_policy'; - -// TODO Move these types to another file -interface SearchResponse { - took: number; - timed_out: boolean; - _scroll_id?: string; - hits: { - total: { - value: number; - relation: string; - }; - max_score: number; - hits: Array<{ - _index: string; - _type: string; - _id: string; - _score: number; - _source: T; - _version?: number; - fields?: any; - highlight?: any; - inner_hits?: any; - matched_queries?: string[]; - sort?: string[]; - }>; - }; -} - -type SearchHit = SearchResponse['hits']['hits'][0]; +import { escapeSearchQueryPhrase } from '../saved_object'; export async function listEnrollmentApiKeys( esClient: ElasticsearchClient, @@ -54,7 +28,7 @@ export async function listEnrollmentApiKeys( ): Promise<{ items: EnrollmentAPIKey[]; total: any; page: any; perPage: any }> { const { page = 1, perPage = 20, kuery } = options; - const res = await esClient.search>({ + const res = await esClient.search>({ index: ENROLLMENT_API_KEYS_INDEX, from: (page - 1) * perPage, size: perPage, @@ -78,7 +52,7 @@ export async function getEnrollmentAPIKey( id: string ): Promise { try { - const res = await esClient.get>({ + const res = await esClient.get>({ index: ENROLLMENT_API_KEYS_INDEX, id, }); @@ -185,6 +159,21 @@ export async function generateEnrollmentAPIKey( }; } +export async function getEnrollmentAPIKeyById(esClient: ElasticsearchClient, apiKeyId: string) { + const res = await esClient.search>({ + index: ENROLLMENT_API_KEYS_INDEX, + q: `api_key_id:${escapeSearchQueryPhrase(apiKeyId)}`, + }); + + const [enrollmentAPIKey] = res.body.hits.hits.map(esDocToEnrollmentApiKey); + + if (enrollmentAPIKey?.api_key_id !== apiKeyId) { + throw new Error('find enrollmentKeyById returned an incorrect key'); + } + + return enrollmentAPIKey; +} + async function validateAgentPolicyId(soClient: SavedObjectsClientContract, agentPolicyId: string) { try { await agentPolicyService.get(soClient, agentPolicyId); @@ -196,7 +185,10 @@ async function validateAgentPolicyId(soClient: SavedObjectsClientContract, agent } } -function esDocToEnrollmentApiKey(doc: SearchHit): EnrollmentAPIKey { +function esDocToEnrollmentApiKey(doc: { + _id: string; + _source: FleetServerEnrollmentAPIKey; +}): EnrollmentAPIKey { return { id: doc._id, ...doc._source, diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts index b3beab546c811..014bc58e747ea 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts @@ -13,7 +13,7 @@ import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; import { createAPIKey, invalidateAPIKeys } from './security'; import { agentPolicyService } from '../agent_policy'; import { appContextService } from '../app_context'; -import { normalizeKuery } from '../saved_object'; +import { normalizeKuery, escapeSearchQueryPhrase } from '../saved_object'; export async function listEnrollmentApiKeys( soClient: SavedObjectsClientContract, @@ -159,6 +159,25 @@ async function validateAgentPolicyId(soClient: SavedObjectsClientContract, agent } } +export async function getEnrollmentAPIKeyById( + soClient: SavedObjectsClientContract, + apiKeyId: string +) { + const [enrollmentAPIKey] = ( + await soClient.find({ + type: ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + searchFields: ['api_key_id'], + search: escapeSearchQueryPhrase(apiKeyId), + }) + ).saved_objects.map(savedObjectToEnrollmentApiKey); + + if (enrollmentAPIKey?.api_key_id !== apiKeyId) { + throw new Error('find enrollmentKeyById returned an incorrect key'); + } + + return enrollmentAPIKey; +} + function savedObjectToEnrollmentApiKey({ error, attributes, diff --git a/x-pack/plugins/fleet/server/services/api_keys/index.ts b/x-pack/plugins/fleet/server/services/api_keys/index.ts index 5cdadeb0c82d8..65051163c78c3 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/index.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/index.ts @@ -5,11 +5,8 @@ * 2.0. */ -import { SavedObjectsClientContract, SavedObject, KibanaRequest } from 'src/core/server'; -import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; -import { EnrollmentAPIKeySOAttributes, EnrollmentAPIKey } from '../../types'; +import { SavedObjectsClientContract, KibanaRequest } from 'src/core/server'; import { createAPIKey } from './security'; -import { escapeSearchQueryPhrase } from '../saved_object'; export { invalidateAPIKeys } from './security'; export * from './enrollment_api_key'; @@ -70,25 +67,6 @@ export async function generateAccessApiKey(soClient: SavedObjectsClientContract, return { id: key.id, key: Buffer.from(`${key.id}:${key.api_key}`).toString('base64') }; } -export async function getEnrollmentAPIKeyById( - soClient: SavedObjectsClientContract, - apiKeyId: string -) { - const [enrollmentAPIKey] = ( - await soClient.find({ - type: ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, - searchFields: ['api_key_id'], - search: escapeSearchQueryPhrase(apiKeyId), - }) - ).saved_objects.map(_savedObjectToEnrollmentApiKey); - - if (enrollmentAPIKey?.api_key_id !== apiKeyId) { - throw new Error('find enrollmentKeyById returned an incorrect key'); - } - - return enrollmentAPIKey; -} - export function parseApiKeyFromHeaders(headers: KibanaRequest['headers']) { const authorizationHeader = headers.authorization; @@ -117,18 +95,3 @@ export function parseApiKey(apiKey: string) { apiKeyId, }; } - -function _savedObjectToEnrollmentApiKey({ - error, - attributes, - id, -}: SavedObject): EnrollmentAPIKey { - if (error) { - throw new Error(error.message); - } - - return { - id, - ...attributes, - }; -} diff --git a/x-pack/plugins/fleet/server/services/fleet_server_migration.ts b/x-pack/plugins/fleet/server/services/fleet_server_migration.ts index f982332886e94..170bec54983c0 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server_migration.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server_migration.ts @@ -5,11 +5,18 @@ * 2.0. */ +import { isBoom } from '@hapi/boom'; import { KibanaRequest } from 'src/core/server'; import { ENROLLMENT_API_KEYS_INDEX, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + AGENT_POLICY_INDEX, + AGENTS_INDEX, FleetServerEnrollmentAPIKey, + AGENT_SAVED_OBJECT_TYPE, + AgentSOAttributes, + FleetServerAgent, + SO_SEARCH_LIMIT, FLEET_SERVER_PACKAGE, FLEET_SERVER_INDICES, } from '../../common'; @@ -17,6 +24,9 @@ import { listEnrollmentApiKeys, getEnrollmentAPIKey } from './api_keys/enrollmen import { appContextService } from './app_context'; import { getInstallation } from './epm/packages'; +import { isAgentsSetup } from './agents'; +import { agentPolicyService } from './agent_policy'; + export async function isFleetServerSetup() { const pkgInstall = await getInstallation({ savedObjectsClient: getInternalUserSOClient(), @@ -28,7 +38,6 @@ export async function isFleetServerSetup() { } const esClient = appContextService.getInternalUserESClient(); - const exists = await Promise.all( FLEET_SERVER_INDICES.map(async (index) => { const res = await esClient.indices.exists({ @@ -42,7 +51,11 @@ export async function isFleetServerSetup() { } export async function runFleetServerMigration() { - await migrateEnrollmentApiKeys(); + // If Agents are not setup skip as there is nothing to migrate + if (!(await isAgentsSetup(getInternalUserSOClient()))) { + return; + } + await Promise.all([migrateEnrollmentApiKeys(), migrateAgentPolicies(), migrateAgents()]); } function getInternalUserSOClient() { @@ -64,6 +77,65 @@ function getInternalUserSOClient() { return appContextService.getInternalUserSOClient(fakeRequest); } +async function migrateAgents() { + const esClient = appContextService.getInternalUserESClient(); + const soClient = getInternalUserSOClient(); + let hasMore = true; + while (hasMore) { + const res = await soClient.find({ + type: AGENT_SAVED_OBJECT_TYPE, + page: 1, + perPage: 100, + }); + + if (res.total === 0) { + hasMore = false; + } + for (const so of res.saved_objects) { + try { + const { + attributes, + } = await appContextService + .getEncryptedSavedObjects() + .getDecryptedAsInternalUser(AGENT_SAVED_OBJECT_TYPE, so.id); + + const body: FleetServerAgent = { + type: attributes.type, + active: attributes.active, + enrolled_at: attributes.enrolled_at, + unenrolled_at: attributes.unenrolled_at, + unenrollment_started_at: attributes.unenrollment_started_at, + upgraded_at: attributes.upgraded_at, + upgrade_started_at: attributes.upgrade_started_at, + access_api_key_id: attributes.access_api_key_id, + user_provided_metadata: attributes.user_provided_metadata, + local_metadata: attributes.local_metadata, + policy_id: attributes.policy_id, + policy_revision_idx: attributes.policy_revision || undefined, + last_checkin: attributes.last_checkin, + last_checkin_status: attributes.last_checkin_status, + default_api_key_id: attributes.default_api_key_id, + default_api_key: attributes.default_api_key, + packages: attributes.packages, + }; + await esClient.create({ + index: AGENTS_INDEX, + body, + id: so.id, + refresh: 'wait_for', + }); + + await soClient.delete(AGENT_SAVED_OBJECT_TYPE, so.id); + } catch (error) { + // swallow 404 error has multiple Kibana can run the migration at the same time + if (!is404Error(error)) { + throw error; + } + } + } + } +} + async function migrateEnrollmentApiKeys() { const esClient = appContextService.getInternalUserESClient(); const soClient = getInternalUserSOClient(); @@ -77,24 +149,61 @@ async function migrateEnrollmentApiKeys() { hasMore = false; } for (const item of res.items) { - const key = await getEnrollmentAPIKey(soClient, item.id); - - const body: FleetServerEnrollmentAPIKey = { - api_key: key.api_key, - api_key_id: key.api_key_id, - active: key.active, - created_at: key.created_at, - name: key.name, - policy_id: key.policy_id, - }; - await esClient.create({ - index: ENROLLMENT_API_KEYS_INDEX, - body, - id: key.id, - refresh: 'wait_for', - }); + try { + const key = await getEnrollmentAPIKey(soClient, item.id); + + const body: FleetServerEnrollmentAPIKey = { + api_key: key.api_key, + api_key_id: key.api_key_id, + active: key.active, + created_at: key.created_at, + name: key.name, + policy_id: key.policy_id, + }; + await esClient.create({ + index: ENROLLMENT_API_KEYS_INDEX, + body, + id: key.id, + refresh: 'wait_for', + }); - await soClient.delete(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, key.id); + await soClient.delete(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, key.id); + } catch (error) { + // swallow 404 error has multiple Kibana can run the migration at the same time + if (!is404Error(error)) { + throw error; + } + } } } } + +async function migrateAgentPolicies() { + const esClient = appContextService.getInternalUserESClient(); + const soClient = getInternalUserSOClient(); + const { items: agentPolicies } = await agentPolicyService.list(soClient, { + perPage: SO_SEARCH_LIMIT, + }); + + await Promise.all( + agentPolicies.map(async (agentPolicy) => { + const res = await esClient.search({ + index: AGENT_POLICY_INDEX, + q: `policy_id:${agentPolicy.id}`, + track_total_hits: true, + }); + + if (res.body.hits.total.value === 0) { + return agentPolicyService.createFleetPolicyChangeFleetServer( + soClient, + esClient, + agentPolicy.id + ); + } + }) + ); +} + +function is404Error(error: any) { + return isBoom(error) && error.output.statusCode === 404; +} diff --git a/x-pack/plugins/fleet/server/services/index.ts b/x-pack/plugins/fleet/server/services/index.ts index 9999ab91e31b2..77ce882275b6b 100644 --- a/x-pack/plugins/fleet/server/services/index.ts +++ b/x-pack/plugins/fleet/server/services/index.ts @@ -49,6 +49,7 @@ export interface AgentService { */ authenticateAgentWithAccessToken( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, request: KibanaRequest ): Promise; /** diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index ab24c26e0cdfa..f19ad4e7fe417 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -93,6 +93,16 @@ async function createSetupSideEffects( await runFleetServerMigration(); } + if (appContextService.getConfig()?.agents?.fleetServerEnabled) { + await ensureInstalledPackage({ + savedObjectsClient: soClient, + pkgName: FLEET_SERVER_PACKAGE, + callCluster, + }); + await ensureFleetServerIndicesCreated(esClient); + await runFleetServerMigration(); + } + // If we just created the default policy, ensure default packages are added to it if (defaultAgentPolicyCreated) { const agentPolicyWithPackagePolicies = await agentPolicyService.get( diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index 0c35fc29e01cd..fda1568c56e0e 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -81,6 +81,9 @@ export { dataTypes, // Fleet Server types FleetServerEnrollmentAPIKey, + FleetServerAgent, + FleetServerAgentAction, + FleetServerPolicy, } from '../../common'; export type CallESAsCurrentUser = LegacyScopedClusterClient['callAsCurrentUser']; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts index 6b58ca71f7f4e..a2aff41b68df7 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts @@ -174,6 +174,9 @@ describe('test alerts route', () => { savedObjects: { client: mockSavedObjectClient, }, + elasticsearch: { + client: { asInternalUser: elasticsearchServiceMock.createInternalClient() }, + }, }, } as unknown) as SecuritySolutionRequestHandlerContext, mockRequest, @@ -218,6 +221,9 @@ describe('test alerts route', () => { savedObjects: { client: mockSavedObjectClient, }, + elasticsearch: { + client: { asInternalUser: elasticsearchServiceMock.createInternalClient() }, + }, }, } as unknown) as SecuritySolutionRequestHandlerContext, mockRequest, @@ -252,6 +258,9 @@ describe('test alerts route', () => { savedObjects: { client: mockSavedObjectClient, }, + elasticsearch: { + client: { asInternalUser: elasticsearchServiceMock.createInternalClient() }, + }, }, } as unknown) as SecuritySolutionRequestHandlerContext, mockRequest, @@ -280,6 +289,9 @@ describe('test alerts route', () => { savedObjects: { client: mockSavedObjectClient, }, + elasticsearch: { + client: { asInternalUser: elasticsearchServiceMock.createInternalClient() }, + }, }, } as unknown) as SecuritySolutionRequestHandlerContext, mockRequest, @@ -314,6 +326,9 @@ describe('test alerts route', () => { savedObjects: { client: mockSavedObjectClient, }, + elasticsearch: { + client: { asInternalUser: elasticsearchServiceMock.createInternalClient() }, + }, }, } as unknown) as SecuritySolutionRequestHandlerContext, mockRequest, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts index 95563c7c48ef5..3dbaa137bb928 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts @@ -54,7 +54,11 @@ export function registerDownloadExceptionListRoute( // The ApiKey must be associated with an enrolled Fleet agent try { scopedSOClient = endpointContext.service.getScopedSavedObjectsClient(req); - await authenticateAgentWithAccessToken(scopedSOClient, req); + await authenticateAgentWithAccessToken( + scopedSOClient, + context.core.elasticsearch.client.asInternalUser, + req + ); } catch (err) { if ((err.isBoom ? err.output.statusCode : err.statusCode) === 401) { return res.unauthorized(); From e221992da39734638708dfbb8dfcf19490e6b526 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Sun, 7 Feb 2021 19:11:45 -0500 Subject: [PATCH 33/44] [actions] improve email action doc (#90020) resolves https://github.com/elastic/kibana/issues/88333 Fixed: - add note that `secure: false` will use TLS, but after an initial connection with TCP; we have been getting questions from customers who believed that `secure: false` implied TLS was not used at all. - added a link to the nodemailer "well-known services" module, to allow customers to see examples of other email service configurations - updated the Outlook config example to use the current nodemailer values - couple of other small tweaks --- docs/user/alerting/action-types/email.asciidoc | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/user/alerting/action-types/email.asciidoc b/docs/user/alerting/action-types/email.asciidoc index 83e7edc5a016a..d7a9373a6e2a9 100644 --- a/docs/user/alerting/action-types/email.asciidoc +++ b/docs/user/alerting/action-types/email.asciidoc @@ -14,7 +14,7 @@ Name:: The name of the connector. The name is used to identify a connector Sender:: The from address for all emails sent with this connector, specified in `user@host-name` format. Host:: Host name of the service provider. If you are using the <> setting, make sure this hostname is added to the allowed hosts. Port:: The port to connect to on the service provider. -Secure:: If true the connection will use TLS when connecting to the service provider. See https://nodemailer.com/smtp/#tls-options[nodemailer TLS documentation] for more information. +Secure:: If true, the connection will use TLS when connecting to the service provider. Refer to the https://nodemailer.com/smtp/#tls-options[Nodemailer TLS documentation] for more information. If not true, the connection will initially connect over TCP, then attempt to switch to TLS via the SMTP STARTTLS command. Username:: username for 'login' type authentication. Password:: password for 'login' type authentication. @@ -92,6 +92,8 @@ systems, refer to: * <> * <> +For other email servers, you can check the list of well-known services that Nodemailer supports in the JSON file https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json[well-known/services.json]. The properties of the objects in those files — `host`, `port`, and `secure` — correspond to the same email action configuration properties. A missing `secure` property in the "well-known/services.json" file is considered `false`. Typically, `port: 465` uses `secure: true`, and `port: 25` and `port: 587` use `secure: false`. + [float] [[gmail]] ===== Sending email from Gmail @@ -109,7 +111,6 @@ https://mail.google.com[Gmail] SMTP service: user: password: -------------------------------------------------- -// CONSOLE If you get an authentication error that indicates that you need to continue the sign-in process from a web browser when the action attempts to send email, you need @@ -131,9 +132,9 @@ https://www.outlook.com/[Outlook.com] SMTP service: [source,text] -------------------------------------------------- config: - host: smtp-mail.outlook.com - port: 465 - secure: true + host: smtp.office365.com + port: 587 + secure: false secrets: user: password: @@ -163,7 +164,7 @@ secrets: user: password: -------------------------------------------------- -<1> `smtp.host` varies depending on the region +<1> `config.host` varies depending on the region NOTE: You must use your Amazon SES SMTP credentials to send email through Amazon SES. For more information, see From c6fc80c748a200572b6b9bf8d19ce93695aab91f Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Sun, 7 Feb 2021 22:48:01 -0500 Subject: [PATCH 34/44] skip flaky suite (#64473) --- .../apis/management/index_management/indices.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/management/index_management/indices.js b/x-pack/test/api_integration/apis/management/index_management/indices.js index cef1bdbba754b..3653d9916466d 100644 --- a/x-pack/test/api_integration/apis/management/index_management/indices.js +++ b/x-pack/test/api_integration/apis/management/index_management/indices.js @@ -34,7 +34,8 @@ export default function ({ getService }) { clearCache, } = registerHelpers({ supertest }); - describe('indices', () => { + // Failing: See https://github.com/elastic/kibana/issues/64473 + describe.skip('indices', () => { after(() => Promise.all([cleanUpEsResources()])); describe('clear cache', () => { From 2279c06d1e0732ffea9de6f2ef1824eb00613ad5 Mon Sep 17 00:00:00 2001 From: spalger Date: Sun, 7 Feb 2021 23:24:04 -0700 Subject: [PATCH 35/44] skip flaky suite (#90555) --- x-pack/test/accessibility/apps/uptime.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/accessibility/apps/uptime.ts b/x-pack/test/accessibility/apps/uptime.ts index ec1f37ca02be2..d7a9cfc0d08b4 100644 --- a/x-pack/test/accessibility/apps/uptime.ts +++ b/x-pack/test/accessibility/apps/uptime.ts @@ -18,7 +18,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const es = getService('es'); - describe('uptime', () => { + // FLAKY: https://github.com/elastic/kibana/issues/90555 + describe.skip('uptime', () => { before(async () => { await esArchiver.load('uptime/blank'); await makeChecks(es, A11Y_TEST_MONITOR_ID, 150, 1, 1000, { From 3b3327dbc3c3041c9681e0cd86bd31cf411dc460 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Mon, 8 Feb 2021 10:19:54 +0100 Subject: [PATCH 36/44] Migrate most plugins to synchronous lifecycle (#89562) * first pass * migrate more plugins * migrate yet more plugins * more oss plugins * fix test file * change Plugin signature on the client-side too * fix test types * migrate OSS client-side plugins * migrate OSS client-side test plugins * migrate xpack client-side plugins * revert fix attempt on fleet plugin * fix presentation start signature * fix yet another signature * add warnings for server-side async plugins in dev mode * remove unused import * fix isPromise * Add client-side deprecations * update migration examples * update generated doc * fix xpack unit tests * nit * (will be reverted) explicitly await for license to be ready in the auth hook * Revert "(will be reverted) explicitly await for license to be ready in the auth hook" This reverts commit fdf73feb * restore await on on promise contracts * Revert "(will be reverted) explicitly await for license to be ready in the auth hook" This reverts commit fdf73feb * Revert "restore await on on promise contracts" This reverts commit c5f2fe51 * add delay before starting tests in FTR * update deprecation ts doc * add explicit contract for monitoring setup * migrate monitoring plugin to sync * change plugin timeout to 10sec * use delay instead of silence --- ...migrating-legacy-plugins-examples.asciidoc | 12 +- .../kibana-plugin-core-public.asyncplugin.md | 27 ++++ ...na-plugin-core-public.asyncplugin.setup.md | 23 +++ ...na-plugin-core-public.asyncplugin.start.md | 23 +++ ...ana-plugin-core-public.asyncplugin.stop.md | 15 ++ .../core/public/kibana-plugin-core-public.md | 1 + .../kibana-plugin-core-public.plugin.setup.md | 4 +- .../kibana-plugin-core-public.plugin.start.md | 4 +- ...na-plugin-core-public.plugininitializer.md | 2 +- .../kibana-plugin-core-server.asyncplugin.md | 27 ++++ ...na-plugin-core-server.asyncplugin.setup.md | 23 +++ ...na-plugin-core-server.asyncplugin.start.md | 23 +++ ...ana-plugin-core-server.asyncplugin.stop.md | 15 ++ .../core/server/kibana-plugin-core-server.md | 1 + .../kibana-plugin-core-server.plugin.setup.md | 4 +- .../kibana-plugin-core-server.plugin.start.md | 4 +- ...na-plugin-core-server.plugininitializer.md | 2 +- packages/kbn-std/src/index.ts | 2 +- packages/kbn-std/src/promise.test.ts | 29 +++- packages/kbn-std/src/promise.ts | 4 + .../kbn-test/src/functional_tests/tasks.js | 6 + src/core/public/index.ts | 9 +- src/core/public/mocks.ts | 8 +- src/core/public/plugins/index.ts | 2 +- src/core/public/plugins/plugin.test.ts | 12 +- src/core/public/plugins/plugin.ts | 57 +++++-- .../plugins/plugins_service.test.mocks.ts | 7 +- .../public/plugins/plugins_service.test.ts | 144 +++++++++++++++-- src/core/public/plugins/plugins_service.ts | 63 +++++--- src/core/public/public.api.md | 16 +- src/core/server/index.ts | 1 + .../integration_tests/plugins_service.test.ts | 4 +- src/core/server/plugins/plugin.test.ts | 36 ++--- src/core/server/plugins/plugin.ts | 27 +++- .../server/plugins/plugins_system.test.ts | 152 +++++++++++++++--- src/core/server/plugins/plugins_system.ts | 56 +++++-- src/core/server/plugins/types.ts | 23 ++- src/core/server/server.api.md | 24 ++- src/plugins/apm_oss/server/plugin.ts | 7 +- src/plugins/console/server/plugin.ts | 7 +- src/plugins/inspector/public/plugin.tsx | 2 +- src/plugins/legacy_export/server/plugin.ts | 7 +- src/plugins/maps_legacy/server/index.ts | 8 +- .../presentation_util/public/plugin.ts | 4 +- src/plugins/region_map/public/plugin.ts | 2 +- .../saved_objects_management/server/plugin.ts | 4 +- src/plugins/share/server/plugin.ts | 2 +- src/plugins/tile_map/public/plugin.ts | 2 +- src/plugins/usage_collection/server/plugin.ts | 12 +- src/plugins/vis_type_table/public/plugin.ts | 5 +- src/plugins/vis_type_timelion/server/index.ts | 4 +- .../vis_type_timelion/server/plugin.ts | 15 +- .../vis_type_timeseries/public/plugin.ts | 4 +- src/plugins/vis_type_vega/public/plugin.ts | 4 +- src/plugins/vis_type_vislib/public/plugin.ts | 2 +- src/plugins/vis_type_xy/public/plugin.ts | 2 +- src/plugins/visualize/public/plugin.ts | 2 +- .../plugins/app_link_test/public/plugin.ts | 2 +- .../plugins/core_plugin_b/public/plugin.tsx | 2 +- x-pack/plugins/actions/server/plugin.ts | 45 +++--- x-pack/plugins/apm/server/plugin.ts | 7 +- .../plugins/beats_management/server/plugin.ts | 18 ++- x-pack/plugins/canvas/server/plugin.ts | 7 +- x-pack/plugins/case/server/plugin.ts | 9 +- x-pack/plugins/cloud/public/plugin.ts | 2 +- x-pack/plugins/cloud/server/plugin.ts | 17 +- x-pack/plugins/code/server/plugin.ts | 10 +- .../encrypted_saved_objects/server/index.ts | 4 +- .../server/plugin.test.ts | 18 +-- .../encrypted_saved_objects/server/plugin.ts | 23 ++- .../enterprise_search/server/plugin.ts | 12 +- x-pack/plugins/event_log/server/plugin.ts | 22 +-- x-pack/plugins/event_log/server/types.ts | 2 - x-pack/plugins/features/server/index.ts | 4 +- x-pack/plugins/features/server/plugin.test.ts | 12 +- x-pack/plugins/features/server/plugin.ts | 9 +- x-pack/plugins/fleet/public/plugin.ts | 2 +- x-pack/plugins/fleet/server/plugin.ts | 4 +- x-pack/plugins/global_search/server/plugin.ts | 11 +- x-pack/plugins/graph/server/plugin.ts | 2 +- .../server/plugin.ts | 13 +- x-pack/plugins/infra/server/plugin.ts | 18 +-- x-pack/plugins/licensing/public/plugin.ts | 2 +- x-pack/plugins/licensing/server/plugin.ts | 12 +- x-pack/plugins/lists/server/create_config.ts | 18 --- x-pack/plugins/lists/server/plugin.ts | 8 +- x-pack/plugins/maps/server/plugin.ts | 8 +- x-pack/plugins/monitoring/public/plugin.ts | 2 +- x-pack/plugins/monitoring/server/index.ts | 6 +- .../plugins/monitoring/server/plugin.test.ts | 56 +------ x-pack/plugins/monitoring/server/plugin.ts | 19 +-- x-pack/plugins/monitoring/server/types.ts | 4 + x-pack/plugins/observability/server/plugin.ts | 7 +- .../plugins/osquery/server/create_config.ts | 8 +- x-pack/plugins/osquery/server/plugin.ts | 7 +- x-pack/plugins/painless_lab/server/plugin.ts | 2 +- .../plugins/remote_clusters/server/plugin.ts | 11 +- .../plugins/searchprofiler/server/plugin.ts | 2 +- x-pack/plugins/security/server/index.ts | 4 +- x-pack/plugins/security/server/plugin.test.ts | 6 +- x-pack/plugins/security/server/plugin.ts | 10 +- .../security_solution/server/config.ts | 9 +- .../security_solution/server/plugin.ts | 15 +- .../plugins/snapshot_restore/server/plugin.ts | 10 +- x-pack/plugins/spaces/server/index.ts | 4 +- x-pack/plugins/spaces/server/plugin.test.ts | 12 +- x-pack/plugins/spaces/server/plugin.ts | 4 +- x-pack/plugins/stack_alerts/server/plugin.ts | 9 +- .../task_manager/server/plugin.test.ts | 2 +- x-pack/plugins/task_manager/server/plugin.ts | 12 +- .../triggers_actions_ui/server/plugin.ts | 4 +- x-pack/plugins/uptime/public/apps/plugin.ts | 5 +- x-pack/plugins/watcher/server/plugin.ts | 2 +- x-pack/plugins/xpack_legacy/server/plugin.ts | 8 +- 114 files changed, 1000 insertions(+), 550 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.asyncplugin.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.asyncplugin.setup.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.asyncplugin.start.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.asyncplugin.stop.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.asyncplugin.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.asyncplugin.setup.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.asyncplugin.start.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.asyncplugin.stop.md delete mode 100644 x-pack/plugins/lists/server/create_config.ts diff --git a/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc b/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc index a033bbd26a1a7..92a624649d3c5 100644 --- a/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc +++ b/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc @@ -71,22 +71,20 @@ export function plugin(initializerContext: PluginInitializerContext) { *plugins/my_plugin/(public|server)/plugin.ts* [source,typescript] ---- -import type { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { CoreSetup, Logger, Plugin, PluginInitializerContext, PluginName } from 'kibana/server'; import type { MyPluginConfig } from './config'; export class MyPlugin implements Plugin { - private readonly config$: Observable; + private readonly config: MyPluginConfig; private readonly log: Logger; constructor(private readonly initializerContext: PluginInitializerContext) { this.log = initializerContext.logger.get(); - this.config$ = initializerContext.config.create(); + this.config = initializerContext.config.get(); } - public async setup(core: CoreSetup, deps: Record) { - const isEnabled = await this.config$.pipe(first()).toPromise(); + public setup(core: CoreSetup, deps: Record) { + const { someConfigValue } = this.config; } } ---- @@ -96,7 +94,7 @@ Additionally, some plugins need to access the runtime env configuration. [source,typescript] ---- export class MyPlugin implements Plugin { - public async setup(core: CoreSetup, deps: Record) { + public setup(core: CoreSetup, deps: Record) { const { mode: { dev }, packageInfo: { version } } = this.initializerContext.env } ---- diff --git a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.md b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.md new file mode 100644 index 0000000000000..cf315e1fd337e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) + +## AsyncPlugin interface + +> Warning: This API is now obsolete. +> +> Asynchronous lifecycles are deprecated, and should be migrated to sync [plugin](./kibana-plugin-core-public.plugin.md) +> + +A plugin with asynchronous lifecycle methods. + +Signature: + +```typescript +export interface AsyncPlugin +``` + +## Methods + +| Method | Description | +| --- | --- | +| [setup(core, plugins)](./kibana-plugin-core-public.asyncplugin.setup.md) | | +| [start(core, plugins)](./kibana-plugin-core-public.asyncplugin.start.md) | | +| [stop()](./kibana-plugin-core-public.asyncplugin.stop.md) | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.setup.md b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.setup.md new file mode 100644 index 0000000000000..54507b44cdd72 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.setup.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) > [setup](./kibana-plugin-core-public.asyncplugin.setup.md) + +## AsyncPlugin.setup() method + +Signature: + +```typescript +setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreSetup<TPluginsStart, TStart> | | +| plugins | TPluginsSetup | | + +Returns: + +`TSetup | Promise` + diff --git a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.start.md b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.start.md new file mode 100644 index 0000000000000..f16d3c46bf849 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.start.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) > [start](./kibana-plugin-core-public.asyncplugin.start.md) + +## AsyncPlugin.start() method + +Signature: + +```typescript +start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreStart | | +| plugins | TPluginsStart | | + +Returns: + +`TStart | Promise` + diff --git a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.stop.md b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.stop.md new file mode 100644 index 0000000000000..f809f75783c26 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.stop.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) > [stop](./kibana-plugin-core-public.asyncplugin.stop.md) + +## AsyncPlugin.stop() method + +Signature: + +```typescript +stop?(): void; +``` +Returns: + +`void` + diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index efd499823ffad..e307b5c9971b0 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -39,6 +39,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ApplicationStart](./kibana-plugin-core-public.applicationstart.md) | | | [AppMeta](./kibana-plugin-core-public.appmeta.md) | Input type for meta data for an application.Meta fields include keywords and searchDeepLinks Keywords is an array of string with which to associate the app, must include at least one unique string as an array. searchDeepLinks is an array of links that represent secondary in-app locations for the app. | | [AppMountParameters](./kibana-plugin-core-public.appmountparameters.md) | | +| [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) | A plugin with asynchronous lifecycle methods. | | [Capabilities](./kibana-plugin-core-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. | | [ChromeBadge](./kibana-plugin-core-public.chromebadge.md) | | | [ChromeBrand](./kibana-plugin-core-public.chromebrand.md) | | diff --git a/docs/development/core/public/kibana-plugin-core-public.plugin.setup.md b/docs/development/core/public/kibana-plugin-core-public.plugin.setup.md index 7fa05588a3301..232851cd342ce 100644 --- a/docs/development/core/public/kibana-plugin-core-public.plugin.setup.md +++ b/docs/development/core/public/kibana-plugin-core-public.plugin.setup.md @@ -7,7 +7,7 @@ Signature: ```typescript -setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; +setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; ``` ## Parameters @@ -19,5 +19,5 @@ setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Returns: -`TSetup | Promise` +`TSetup` diff --git a/docs/development/core/public/kibana-plugin-core-public.plugin.start.md b/docs/development/core/public/kibana-plugin-core-public.plugin.start.md index 0d3c19a8217a6..ec5ed211a9d2b 100644 --- a/docs/development/core/public/kibana-plugin-core-public.plugin.start.md +++ b/docs/development/core/public/kibana-plugin-core-public.plugin.start.md @@ -7,7 +7,7 @@ Signature: ```typescript -start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; +start(core: CoreStart, plugins: TPluginsStart): TStart; ``` ## Parameters @@ -19,5 +19,5 @@ start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; Returns: -`TStart | Promise` +`TStart` diff --git a/docs/development/core/public/kibana-plugin-core-public.plugininitializer.md b/docs/development/core/public/kibana-plugin-core-public.plugininitializer.md index 1fcc2999dfd2e..b7c3e11e492bd 100644 --- a/docs/development/core/public/kibana-plugin-core-public.plugininitializer.md +++ b/docs/development/core/public/kibana-plugin-core-public.plugininitializer.md @@ -9,5 +9,5 @@ The `plugin` export at the root of a plugin's `public` directory should conform Signature: ```typescript -export declare type PluginInitializer = (core: PluginInitializerContext) => Plugin; +export declare type PluginInitializer = (core: PluginInitializerContext) => Plugin | AsyncPlugin; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.md b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.md new file mode 100644 index 0000000000000..1ad1d87220b74 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) + +## AsyncPlugin interface + +> Warning: This API is now obsolete. +> +> Asynchronous lifecycles are deprecated, and should be migrated to sync [plugin](./kibana-plugin-core-server.plugin.md) +> + +A plugin with asynchronous lifecycle methods. + +Signature: + +```typescript +export interface AsyncPlugin +``` + +## Methods + +| Method | Description | +| --- | --- | +| [setup(core, plugins)](./kibana-plugin-core-server.asyncplugin.setup.md) | | +| [start(core, plugins)](./kibana-plugin-core-server.asyncplugin.start.md) | | +| [stop()](./kibana-plugin-core-server.asyncplugin.stop.md) | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.setup.md b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.setup.md new file mode 100644 index 0000000000000..1d033b7b88b05 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.setup.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) > [setup](./kibana-plugin-core-server.asyncplugin.setup.md) + +## AsyncPlugin.setup() method + +Signature: + +```typescript +setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreSetup | | +| plugins | TPluginsSetup | | + +Returns: + +`TSetup | Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.start.md b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.start.md new file mode 100644 index 0000000000000..3cce90f01603b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.start.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) > [start](./kibana-plugin-core-server.asyncplugin.start.md) + +## AsyncPlugin.start() method + +Signature: + +```typescript +start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreStart | | +| plugins | TPluginsStart | | + +Returns: + +`TStart | Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.stop.md b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.stop.md new file mode 100644 index 0000000000000..9272fc2c4eba0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.stop.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) > [stop](./kibana-plugin-core-server.asyncplugin.stop.md) + +## AsyncPlugin.stop() method + +Signature: + +```typescript +stop?(): void; +``` +Returns: + +`void` + diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 82f4a285409c9..5fe5eda7a8172 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -49,6 +49,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AppCategory](./kibana-plugin-core-server.appcategory.md) | A category definition for nav links to know where to sort them in the left hand nav | | [AssistanceAPIResponse](./kibana-plugin-core-server.assistanceapiresponse.md) | | | [AssistantAPIClientParams](./kibana-plugin-core-server.assistantapiclientparams.md) | | +| [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) | A plugin with asynchronous lifecycle methods. | | [Authenticated](./kibana-plugin-core-server.authenticated.md) | | | [AuthNotHandled](./kibana-plugin-core-server.authnothandled.md) | | | [AuthRedirected](./kibana-plugin-core-server.authredirected.md) | | diff --git a/docs/development/core/server/kibana-plugin-core-server.plugin.setup.md b/docs/development/core/server/kibana-plugin-core-server.plugin.setup.md index b4e6623098736..a8b0aae28d251 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugin.setup.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugin.setup.md @@ -7,7 +7,7 @@ Signature: ```typescript -setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; +setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; ``` ## Parameters @@ -19,5 +19,5 @@ setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; Returns: -`TSetup | Promise` +`TSetup` diff --git a/docs/development/core/server/kibana-plugin-core-server.plugin.start.md b/docs/development/core/server/kibana-plugin-core-server.plugin.start.md index 03e889a018b6f..851f84474fe11 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugin.start.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugin.start.md @@ -7,7 +7,7 @@ Signature: ```typescript -start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; +start(core: CoreStart, plugins: TPluginsStart): TStart; ``` ## Parameters @@ -19,5 +19,5 @@ start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; Returns: -`TStart | Promise` +`TStart` diff --git a/docs/development/core/server/kibana-plugin-core-server.plugininitializer.md b/docs/development/core/server/kibana-plugin-core-server.plugininitializer.md index 839eabff29a18..fe55e131065dd 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugininitializer.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugininitializer.md @@ -9,5 +9,5 @@ The `plugin` export at the root of a plugin's `server` directory should conform Signature: ```typescript -export declare type PluginInitializer = (core: PluginInitializerContext) => Plugin; +export declare type PluginInitializer = (core: PluginInitializerContext) => Plugin | AsyncPlugin; ``` diff --git a/packages/kbn-std/src/index.ts b/packages/kbn-std/src/index.ts index f3d9e0f77fa19..d79594c97cec7 100644 --- a/packages/kbn-std/src/index.ts +++ b/packages/kbn-std/src/index.ts @@ -12,7 +12,7 @@ export { get } from './get'; export { mapToObject } from './map_to_object'; export { merge } from './merge'; export { pick } from './pick'; -export { withTimeout } from './promise'; +export { withTimeout, isPromise } from './promise'; export { isRelativeUrl, modifyUrl, getUrlOrigin, URLMeaningfulParts } from './url'; export { unset } from './unset'; export { getFlattenedObject } from './get_flattened_object'; diff --git a/packages/kbn-std/src/promise.test.ts b/packages/kbn-std/src/promise.test.ts index 61197a2a8bf70..f7c119acd0c7a 100644 --- a/packages/kbn-std/src/promise.test.ts +++ b/packages/kbn-std/src/promise.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { withTimeout } from './promise'; +import { withTimeout, isPromise } from './promise'; const delay = (ms: number, resolveValue?: any) => new Promise((resolve) => setTimeout(resolve, ms, resolveValue)); @@ -50,3 +50,30 @@ describe('withTimeout', () => { ).rejects.toMatchInlineSnapshot(`[Error: from-promise]`); }); }); + +describe('isPromise', () => { + it('returns true when arg is a Promise', () => { + expect(isPromise(Promise.resolve('foo'))).toEqual(true); + expect(isPromise(Promise.reject('foo').catch(() => undefined))).toEqual(true); + }); + + it('returns false when arg is not a Promise', () => { + expect(isPromise(12)).toEqual(false); + expect(isPromise('foo')).toEqual(false); + expect(isPromise({ hello: 'dolly' })).toEqual(false); + expect(isPromise([1, 2, 3])).toEqual(false); + }); + + it('returns false for objects with a non-function `then` property', () => { + expect(isPromise({ then: 'bar' })).toEqual(false); + }); + + it('returns false for null and undefined', () => { + expect(isPromise(null)).toEqual(false); + expect(isPromise(undefined)).toEqual(false); + }); + + it('returns true for Promise-Like objects', () => { + expect(isPromise({ then: () => 12 })).toEqual(true); + }); +}); diff --git a/packages/kbn-std/src/promise.ts b/packages/kbn-std/src/promise.ts index ce4e50bf9b2ac..9d8f7703c026d 100644 --- a/packages/kbn-std/src/promise.ts +++ b/packages/kbn-std/src/promise.ts @@ -20,3 +20,7 @@ export function withTimeout({ new Promise((resolve, reject) => setTimeout(() => reject(new Error(errorMessage)), timeout)), ]) as Promise; } + +export function isPromise(maybePromise: T | Promise): maybePromise is Promise { + return maybePromise ? typeof (maybePromise as Promise).then === 'function' : false; +} diff --git a/packages/kbn-test/src/functional_tests/tasks.js b/packages/kbn-test/src/functional_tests/tasks.js index 099963545a2dc..02c55b6af91dc 100644 --- a/packages/kbn-test/src/functional_tests/tasks.js +++ b/packages/kbn-test/src/functional_tests/tasks.js @@ -95,6 +95,8 @@ export async function runTests(options) { try { es = await runElasticsearch({ config, options: opts }); await runKibanaServer({ procs, config, options: opts }); + // workaround until https://github.com/elastic/kibana/issues/89828 is addressed + await delay(5000); await runFtr({ configPath, options: opts }); } finally { try { @@ -160,3 +162,7 @@ async function silence(log, milliseconds) { ) .toPromise(); } + +async function delay(ms) { + await new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/src/core/public/index.ts b/src/core/public/index.ts index afa129adc061f..a1cb036ce38f8 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -53,7 +53,13 @@ import { HttpSetup, HttpStart } from './http'; import { I18nStart } from './i18n'; import { NotificationsSetup, NotificationsStart } from './notifications'; import { OverlayStart } from './overlays'; -import { Plugin, PluginInitializer, PluginInitializerContext, PluginOpaqueId } from './plugins'; +import { + Plugin, + AsyncPlugin, + PluginInitializer, + PluginInitializerContext, + PluginOpaqueId, +} from './plugins'; import { UiSettingsState, IUiSettingsClient } from './ui_settings'; import { ApplicationSetup, Capabilities, ApplicationStart } from './application'; import { DocLinksStart } from './doc_links'; @@ -304,6 +310,7 @@ export { NotificationsSetup, NotificationsStart, Plugin, + AsyncPlugin, PluginInitializer, PluginInitializerContext, SavedObjectsStart, diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index d208ea76c48fe..e47de84ea12b2 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -110,14 +110,14 @@ function pluginInitializerContextMock(config: any = {}) { return mock; } -function createCoreContext(): CoreContext { +function createCoreContext({ production = false }: { production?: boolean } = {}): CoreContext { return { coreId: Symbol('core context mock'), env: { mode: { - dev: true, - name: 'development', - prod: false, + dev: !production, + name: production ? 'production' : 'development', + prod: production, }, packageInfo: { version: 'version', diff --git a/src/core/public/plugins/index.ts b/src/core/public/plugins/index.ts index 76811d4908d22..be805c6a521ce 100644 --- a/src/core/public/plugins/index.ts +++ b/src/core/public/plugins/index.ts @@ -7,6 +7,6 @@ */ export * from './plugins_service'; -export { Plugin, PluginInitializer } from './plugin'; +export { Plugin, AsyncPlugin, PluginInitializer } from './plugin'; export { PluginInitializerContext } from './plugin_context'; export { PluginOpaqueId } from '../../server/types'; diff --git a/src/core/public/plugins/plugin.test.ts b/src/core/public/plugins/plugin.test.ts index e8e930a5befca..ef919018f120b 100644 --- a/src/core/public/plugins/plugin.test.ts +++ b/src/core/public/plugins/plugin.test.ts @@ -39,16 +39,16 @@ beforeEach(() => { }); describe('PluginWrapper', () => { - test('`setup` fails if plugin.setup is not a function', async () => { + test('`setup` fails if plugin.setup is not a function', () => { mockInitializer.mockReturnValueOnce({ start: jest.fn() } as any); - await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( + expect(() => plugin.setup({} as any, {} as any)).toThrowErrorMatchingInlineSnapshot( `"Instance of plugin \\"plugin-a\\" does not define \\"setup\\" function."` ); }); - test('`setup` fails if plugin.start is not a function', async () => { + test('`setup` fails if plugin.start is not a function', () => { mockInitializer.mockReturnValueOnce({ setup: jest.fn() } as any); - await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( + expect(() => plugin.setup({} as any, {} as any)).toThrowErrorMatchingInlineSnapshot( `"Instance of plugin \\"plugin-a\\" does not define \\"start\\" function."` ); }); @@ -65,8 +65,8 @@ describe('PluginWrapper', () => { expect(mockPlugin.setup).toHaveBeenCalledWith(context, deps); }); - test('`start` fails if setup is not called first', async () => { - await expect(plugin.start({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( + test('`start` fails if setup is not called first', () => { + expect(() => plugin.start({} as any, {} as any)).toThrowErrorMatchingInlineSnapshot( `"Plugin \\"plugin-a\\" can't be started since it isn't set up."` ); }); diff --git a/src/core/public/plugins/plugin.ts b/src/core/public/plugins/plugin.ts index af95e831a6472..a08a6cf0b431a 100644 --- a/src/core/public/plugins/plugin.ts +++ b/src/core/public/plugins/plugin.ts @@ -8,6 +8,7 @@ import { Subject } from 'rxjs'; import { first } from 'rxjs/operators'; +import { isPromise } from '@kbn/std'; import { DiscoveredPlugin, PluginOpaqueId } from '../../server'; import { PluginInitializerContext } from './plugin_context'; import { read } from './plugin_reader'; @@ -23,6 +24,23 @@ export interface Plugin< TStart = void, TPluginsSetup extends object = object, TPluginsStart extends object = object +> { + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; + start(core: CoreStart, plugins: TPluginsStart): TStart; + stop?(): void; +} + +/** + * A plugin with asynchronous lifecycle methods. + * + * @deprecated Asynchronous lifecycles are deprecated, and should be migrated to sync {@link Plugin | plugin} + * @public + */ +export interface AsyncPlugin< + TSetup = void, + TStart = void, + TPluginsSetup extends object = object, + TPluginsStart extends object = object > { setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; @@ -40,7 +58,11 @@ export type PluginInitializer< TStart, TPluginsSetup extends object = object, TPluginsStart extends object = object -> = (core: PluginInitializerContext) => Plugin; +> = ( + core: PluginInitializerContext +) => + | Plugin + | AsyncPlugin; /** * Lightweight wrapper around discovered plugin that is responsible for instantiating @@ -58,7 +80,9 @@ export class PluginWrapper< public readonly configPath: DiscoveredPlugin['configPath']; public readonly requiredPlugins: DiscoveredPlugin['requiredPlugins']; public readonly optionalPlugins: DiscoveredPlugin['optionalPlugins']; - private instance?: Plugin; + private instance?: + | Plugin + | AsyncPlugin; private readonly startDependencies$ = new Subject<[CoreStart, TPluginsStart, TStart]>(); public readonly startDependencies = this.startDependencies$.pipe(first()).toPromise(); @@ -81,10 +105,12 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `setup` function. */ - public async setup(setupContext: CoreSetup, plugins: TPluginsSetup) { - this.instance = await this.createPluginInstance(); - - return await this.instance.setup(setupContext, plugins); + public setup( + setupContext: CoreSetup, + plugins: TPluginsSetup + ): TSetup | Promise { + this.instance = this.createPluginInstance(); + return this.instance.setup(setupContext, plugins); } /** @@ -94,16 +120,21 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `start` function. */ - public async start(startContext: CoreStart, plugins: TPluginsStart) { + public start(startContext: CoreStart, plugins: TPluginsStart) { if (this.instance === undefined) { throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`); } - const startContract = await this.instance.start(startContext, plugins); - - this.startDependencies$.next([startContext, plugins, startContract]); - - return startContract; + const startContract = this.instance.start(startContext, plugins); + if (isPromise(startContract)) { + return startContract.then((resolvedContract) => { + this.startDependencies$.next([startContext, plugins, resolvedContract]); + return resolvedContract; + }); + } else { + this.startDependencies$.next([startContext, plugins, startContract]); + return startContract; + } } /** @@ -121,7 +152,7 @@ export class PluginWrapper< this.instance = undefined; } - private async createPluginInstance() { + private createPluginInstance() { const initializer = read(this.name) as PluginInitializer< TSetup, TStart, diff --git a/src/core/public/plugins/plugins_service.test.mocks.ts b/src/core/public/plugins/plugins_service.test.mocks.ts index d44657f9039a3..1f85482569dbc 100644 --- a/src/core/public/plugins/plugins_service.test.mocks.ts +++ b/src/core/public/plugins/plugins_service.test.mocks.ts @@ -7,9 +7,12 @@ */ import { PluginName } from 'kibana/server'; -import { Plugin } from './plugin'; +import { Plugin, AsyncPlugin } from './plugin'; -export type MockedPluginInitializer = jest.Mock>, any>; +export type MockedPluginInitializer = jest.Mock< + Plugin | AsyncPlugin, + any +>; export const mockPluginInitializerProvider: jest.Mock< MockedPluginInitializer, diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index a22d48c50247a..e70b78f237d75 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -146,16 +146,16 @@ describe('PluginsService', () => { it('returns dependency tree of symbols', () => { const pluginsService = new PluginsService(mockCoreContext, plugins); expect(pluginsService.getOpaqueIds()).toMatchInlineSnapshot(` - Map { - Symbol(pluginA) => Array [], - Symbol(pluginB) => Array [ - Symbol(pluginA), - ], - Symbol(pluginC) => Array [ - Symbol(pluginA), - ], - } - `); + Map { + Symbol(pluginA) => Array [], + Symbol(pluginB) => Array [ + Symbol(pluginA), + ], + Symbol(pluginC) => Array [ + Symbol(pluginA), + ], + } + `); }); }); @@ -264,7 +264,7 @@ describe('PluginsService', () => { jest.runAllTimers(); // setup plugins await expect(promise).rejects.toMatchInlineSnapshot( - `[Error: Setup lifecycle of "pluginA" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]` + `[Error: Setup lifecycle of "pluginA" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.]` ); }); }); @@ -344,7 +344,7 @@ describe('PluginsService', () => { jest.runAllTimers(); await expect(promise).rejects.toMatchInlineSnapshot( - `[Error: Start lifecycle of "pluginA" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]` + `[Error: Start lifecycle of "pluginA" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.]` ); }); }); @@ -366,4 +366,124 @@ describe('PluginsService', () => { expect(pluginCInstance.stop).toHaveBeenCalled(); }); }); + + describe('asynchronous plugins', () => { + let consoleSpy: jest.SpyInstance; + + beforeEach(() => { + consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined); + }); + + afterEach(() => { + consoleSpy.mockRestore(); + }); + + const runScenario = async ({ + production, + asyncSetup, + asyncStart, + }: { + production: boolean; + asyncSetup: boolean; + asyncStart: boolean; + }) => { + const coreContext = coreMock.createCoreContext({ production }); + + const syncPlugin = { id: 'sync-plugin', plugin: createManifest('sync-plugin') }; + mockPluginInitializers.set( + 'sync-plugin', + jest.fn(() => ({ + setup: jest.fn(() => 'setup-sync'), + start: jest.fn(() => 'start-sync'), + stop: jest.fn(), + })) + ); + + const asyncPlugin = { id: 'async-plugin', plugin: createManifest('async-plugin') }; + mockPluginInitializers.set( + 'async-plugin', + jest.fn(() => ({ + setup: jest.fn(() => (asyncSetup ? Promise.resolve('setup-async') : 'setup-sync')), + start: jest.fn(() => (asyncStart ? Promise.resolve('start-async') : 'start-sync')), + stop: jest.fn(), + })) + ); + + const pluginsService = new PluginsService(coreContext, [syncPlugin, asyncPlugin]); + + await pluginsService.setup(mockSetupDeps); + await pluginsService.start(mockStartDeps); + }; + + it('logs a warning if a plugin returns a promise from its setup contract in dev mode', async () => { + await runScenario({ + production: false, + asyncSetup: true, + asyncStart: false, + }); + + expect(consoleSpy.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); + + it('does not log warnings if a plugin returns a promise from its setup contract in prod mode', async () => { + await runScenario({ + production: true, + asyncSetup: true, + asyncStart: false, + }); + + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + it('logs a warning if a plugin returns a promise from its start contract in dev mode', async () => { + await runScenario({ + production: false, + asyncSetup: false, + asyncStart: true, + }); + + expect(consoleSpy.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); + + it('does not log warnings if a plugin returns a promise from its start contract in prod mode', async () => { + await runScenario({ + production: true, + asyncSetup: false, + asyncStart: true, + }); + + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + it('logs multiple warnings if both `setup` and `start` return promises', async () => { + await runScenario({ + production: false, + asyncSetup: true, + asyncStart: true, + }); + + expect(consoleSpy.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + Array [ + "Plugin async-plugin is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); + }); }); diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts index 7a10ce1cdfc77..57fbe4cbecd12 100644 --- a/src/core/public/plugins/plugins_service.ts +++ b/src/core/public/plugins/plugins_service.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { withTimeout } from '@kbn/std'; +import { withTimeout, isPromise } from '@kbn/std'; import { PluginName, PluginOpaqueId } from '../../server'; import { CoreService } from '../../types'; import { CoreContext } from '../core_system'; @@ -98,16 +98,29 @@ export class PluginsService implements CoreService ); - const contract = await withTimeout({ - promise: plugin.setup( - createPluginSetupContext(this.coreContext, deps, plugin), - pluginDepContracts - ), - timeout: 30 * Sec, - errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`, - }); - contracts.set(pluginName, contract); + let contract: unknown; + const contractOrPromise = plugin.setup( + createPluginSetupContext(this.coreContext, deps, plugin), + pluginDepContracts + ); + if (isPromise(contractOrPromise)) { + if (this.coreContext.env.mode.dev) { + // eslint-disable-next-line no-console + console.log( + `Plugin ${pluginName} is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.` + ); + } + + contract = await withTimeout({ + promise: contractOrPromise, + timeout: 10 * Sec, + errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`, + }); + } else { + contract = contractOrPromise; + } + contracts.set(pluginName, contract); this.satupPlugins.push(pluginName); } @@ -132,14 +145,28 @@ export class PluginsService implements CoreService ); - const contract = await withTimeout({ - promise: plugin.start( - createPluginStartContext(this.coreContext, deps, plugin), - pluginDepContracts - ), - timeout: 30 * Sec, - errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`, - }); + let contract: unknown; + const contractOrPromise = plugin.start( + createPluginStartContext(this.coreContext, deps, plugin), + pluginDepContracts + ); + if (isPromise(contractOrPromise)) { + if (this.coreContext.env.mode.dev) { + // eslint-disable-next-line no-console + console.log( + `Plugin ${pluginName} is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.` + ); + } + + contract = await withTimeout({ + promise: contractOrPromise, + timeout: 10 * Sec, + errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`, + }); + } else { + contract = contractOrPromise; + } + contracts.set(pluginName, contract); } diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 75ed9aa5f150f..99579ada8ec58 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -194,6 +194,16 @@ export type AppUpdatableFields = Pick Partial | undefined; +// @public @deprecated +export interface AsyncPlugin { + // (undocumented) + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + // (undocumented) + start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + // (undocumented) + stop?(): void; +} + // @public export interface Capabilities { [key: string]: Record>; @@ -990,15 +1000,15 @@ export { PackageInfo } // @public export interface Plugin { // (undocumented) - setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; // (undocumented) - start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + start(core: CoreStart, plugins: TPluginsStart): TStart; // (undocumented) stop?(): void; } // @public -export type PluginInitializer = (core: PluginInitializerContext) => Plugin; +export type PluginInitializer = (core: PluginInitializerContext) => Plugin | AsyncPlugin; // @public export interface PluginInitializerContext { diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 382a694bd2e41..6f478004c204e 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -235,6 +235,7 @@ export { export { DiscoveredPlugin, Plugin, + AsyncPlugin, PluginConfigDescriptor, PluginConfigSchema, PluginInitializer, diff --git a/src/core/server/plugins/integration_tests/plugins_service.test.ts b/src/core/server/plugins/integration_tests/plugins_service.test.ts index dda947972737a..a29fb01fbc009 100644 --- a/src/core/server/plugins/integration_tests/plugins_service.test.ts +++ b/src/core/server/plugins/integration_tests/plugins_service.test.ts @@ -20,7 +20,7 @@ import { config } from '../plugins_config'; import { loggingSystemMock } from '../../logging/logging_system.mock'; import { environmentServiceMock } from '../../environment/environment_service.mock'; import { coreMock } from '../../mocks'; -import { Plugin } from '../types'; +import { AsyncPlugin } from '../types'; import { PluginWrapper } from '../plugin'; describe('PluginsService', () => { @@ -138,7 +138,7 @@ describe('PluginsService', () => { expect(startDependenciesResolved).toBe(false); return pluginStartContract; }, - } as Plugin); + } as AsyncPlugin); jest.doMock( join(pluginPath, 'server'), diff --git a/src/core/server/plugins/plugin.test.ts b/src/core/server/plugins/plugin.test.ts index 68fdfdf62c30b..c90d2e804225c 100644 --- a/src/core/server/plugins/plugin.test.ts +++ b/src/core/server/plugins/plugin.test.ts @@ -100,7 +100,7 @@ test('`constructor` correctly initializes plugin instance', () => { expect(plugin.optionalPlugins).toEqual(['some-optional-dep']); }); -test('`setup` fails if `plugin` initializer is not exported', async () => { +test('`setup` fails if `plugin` initializer is not exported', () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); const plugin = new PluginWrapper({ @@ -115,14 +115,14 @@ test('`setup` fails if `plugin` initializer is not exported', async () => { ), }); - await expect( + expect(() => plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}) - ).rejects.toMatchInlineSnapshot( - `[Error: Plugin "some-plugin-id" does not export "plugin" definition (plugin-without-initializer-path).]` + ).toThrowErrorMatchingInlineSnapshot( + `"Plugin \\"some-plugin-id\\" does not export \\"plugin\\" definition (plugin-without-initializer-path)."` ); }); -test('`setup` fails if plugin initializer is not a function', async () => { +test('`setup` fails if plugin initializer is not a function', () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); const plugin = new PluginWrapper({ @@ -137,14 +137,14 @@ test('`setup` fails if plugin initializer is not a function', async () => { ), }); - await expect( + expect(() => plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}) - ).rejects.toMatchInlineSnapshot( - `[Error: Definition of plugin "some-plugin-id" should be a function (plugin-with-wrong-initializer-path).]` + ).toThrowErrorMatchingInlineSnapshot( + `"Definition of plugin \\"some-plugin-id\\" should be a function (plugin-with-wrong-initializer-path)."` ); }); -test('`setup` fails if initializer does not return object', async () => { +test('`setup` fails if initializer does not return object', () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); const plugin = new PluginWrapper({ @@ -161,14 +161,14 @@ test('`setup` fails if initializer does not return object', async () => { mockPluginInitializer.mockReturnValue(null); - await expect( + expect(() => plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}) - ).rejects.toMatchInlineSnapshot( - `[Error: Initializer for plugin "some-plugin-id" is expected to return plugin instance, but returned "null".]` + ).toThrowErrorMatchingInlineSnapshot( + `"Initializer for plugin \\"some-plugin-id\\" is expected to return plugin instance, but returned \\"null\\"."` ); }); -test('`setup` fails if object returned from initializer does not define `setup` function', async () => { +test('`setup` fails if object returned from initializer does not define `setup` function', () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); const plugin = new PluginWrapper({ @@ -186,10 +186,10 @@ test('`setup` fails if object returned from initializer does not define `setup` const mockPluginInstance = { run: jest.fn() }; mockPluginInitializer.mockReturnValue(mockPluginInstance); - await expect( + expect(() => plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}) - ).rejects.toMatchInlineSnapshot( - `[Error: Instance of plugin "some-plugin-id" does not define "setup" function.]` + ).toThrowErrorMatchingInlineSnapshot( + `"Instance of plugin \\"some-plugin-id\\" does not define \\"setup\\" function."` ); }); @@ -223,7 +223,7 @@ test('`setup` initializes plugin and calls appropriate lifecycle hook', async () expect(mockPluginInstance.setup).toHaveBeenCalledWith(setupContext, setupDependencies); }); -test('`start` fails if setup is not called first', async () => { +test('`start` fails if setup is not called first', () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); const plugin = new PluginWrapper({ @@ -238,7 +238,7 @@ test('`start` fails if setup is not called first', async () => { ), }); - await expect(plugin.start({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( + expect(() => plugin.start({} as any, {} as any)).toThrowErrorMatchingInlineSnapshot( `"Plugin \\"some-plugin-id\\" can't be started since it isn't set up."` ); }); diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts index 83b3fb53689a7..ca7f11e28de75 100644 --- a/src/core/server/plugins/plugin.ts +++ b/src/core/server/plugins/plugin.ts @@ -10,11 +10,13 @@ import { join } from 'path'; import typeDetect from 'type-detect'; import { Subject } from 'rxjs'; import { first } from 'rxjs/operators'; +import { isPromise } from '@kbn/std'; import { isConfigSchema } from '@kbn/config-schema'; import { Logger } from '../logging'; import { Plugin, + AsyncPlugin, PluginInitializerContext, PluginManifest, PluginInitializer, @@ -49,7 +51,9 @@ export class PluginWrapper< private readonly log: Logger; private readonly initializerContext: PluginInitializerContext; - private instance?: Plugin; + private instance?: + | Plugin + | AsyncPlugin; private readonly startDependencies$ = new Subject<[CoreStart, TPluginsStart, TStart]>(); public readonly startDependencies = this.startDependencies$.pipe(first()).toPromise(); @@ -83,9 +87,11 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `setup` function. */ - public async setup(setupContext: CoreSetup, plugins: TPluginsSetup) { + public setup( + setupContext: CoreSetup, + plugins: TPluginsSetup + ): TSetup | Promise { this.instance = this.createPluginInstance(); - return this.instance.setup(setupContext, plugins); } @@ -96,14 +102,21 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `start` function. */ - public async start(startContext: CoreStart, plugins: TPluginsStart) { + public start(startContext: CoreStart, plugins: TPluginsStart): TStart | Promise { if (this.instance === undefined) { throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`); } - const startContract = await this.instance.start(startContext, plugins); - this.startDependencies$.next([startContext, plugins, startContract]); - return startContract; + const startContract = this.instance.start(startContext, plugins); + if (isPromise(startContract)) { + return startContract.then((resolvedContract) => { + this.startDependencies$.next([startContext, plugins, resolvedContract]); + return resolvedContract; + }); + } else { + this.startDependencies$.next([startContext, plugins, startContract]); + return startContract; + } } /** diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts index 1b5994c40c041..5c38deeb5cf6e 100644 --- a/src/core/server/plugins/plugins_system.test.ts +++ b/src/core/server/plugins/plugins_system.test.ts @@ -25,7 +25,6 @@ import { PluginsSystem } from './plugins_system'; import { coreMock } from '../mocks'; import { Logger } from '../logging'; -const logger = loggingSystemMock.create(); function createPlugin( id: string, { @@ -34,8 +33,8 @@ function createPlugin( server = true, ui = true, }: { required?: string[]; optional?: string[]; server?: boolean; ui?: boolean } = {} -) { - return new PluginWrapper({ +): PluginWrapper { + return new PluginWrapper({ path: 'some-path', manifest: { id, @@ -53,27 +52,27 @@ function createPlugin( }); } +const setupDeps = coreMock.createInternalSetup(); +const startDeps = coreMock.createInternalStart(); + let pluginsSystem: PluginsSystem; -const configService = configServiceMock.create(); -configService.atPath.mockReturnValue(new BehaviorSubject({ initialize: true })); +let configService: ReturnType; +let logger: ReturnType; let env: Env; let coreContext: CoreContext; -const setupDeps = coreMock.createInternalSetup(); -const startDeps = coreMock.createInternalStart(); - beforeEach(() => { + logger = loggingSystemMock.create(); env = Env.createDefault(REPO_ROOT, getEnvOptions()); + configService = configServiceMock.create(); + configService.atPath.mockReturnValue(new BehaviorSubject({ initialize: true })); + coreContext = { coreId: Symbol(), env, logger, configService: configService as any }; pluginsSystem = new PluginsSystem(coreContext); }); -afterEach(() => { - jest.clearAllMocks(); -}); - test('can be setup even without plugins', async () => { const pluginsSetup = await pluginsSystem.setupPlugins(setupDeps); @@ -208,7 +207,7 @@ test('correctly orders plugins and returns exposed values for "setup" and "start start: { 'order-2': 'started-as-2' }, }, ], - ] as Array<[PluginWrapper, Contracts]>); + ] as Array<[PluginWrapper, Contracts]>); const setupContextMap = new Map(); const startContextMap = new Map(); @@ -434,7 +433,7 @@ describe('setup', () => { afterAll(() => { jest.useRealTimers(); }); - it('throws timeout error if "setup" was not completed in 30 sec.', async () => { + it('throws timeout error if "setup" was not completed in 10 sec.', async () => { const plugin: PluginWrapper = createPlugin('timeout-setup'); jest.spyOn(plugin, 'setup').mockImplementation(() => new Promise((i) => i)); pluginsSystem.addPlugin(plugin); @@ -444,7 +443,7 @@ describe('setup', () => { jest.runAllTimers(); await expect(promise).rejects.toMatchInlineSnapshot( - `[Error: Setup lifecycle of "timeout-setup" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]` + `[Error: Setup lifecycle of "timeout-setup" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.]` ); }); @@ -471,8 +470,8 @@ describe('start', () => { afterAll(() => { jest.useRealTimers(); }); - it('throws timeout error if "start" was not completed in 30 sec.', async () => { - const plugin: PluginWrapper = createPlugin('timeout-start'); + it('throws timeout error if "start" was not completed in 10 sec.', async () => { + const plugin = createPlugin('timeout-start'); jest.spyOn(plugin, 'setup').mockResolvedValue({}); jest.spyOn(plugin, 'start').mockImplementation(() => new Promise((i) => i)); @@ -485,7 +484,7 @@ describe('start', () => { jest.runAllTimers(); await expect(promise).rejects.toMatchInlineSnapshot( - `[Error: Start lifecycle of "timeout-start" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]` + `[Error: Start lifecycle of "timeout-start" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.]` ); }); @@ -505,3 +504,120 @@ describe('start', () => { expect(log.info).toHaveBeenCalledWith(`Starting [2] plugins: [order-1,order-0]`); }); }); + +describe('asynchronous plugins', () => { + const runScenario = async ({ + production, + asyncSetup, + asyncStart, + }: { + production: boolean; + asyncSetup: boolean; + asyncStart: boolean; + }) => { + env = Env.createDefault( + REPO_ROOT, + getEnvOptions({ + cliArgs: { + dev: !production, + envName: production ? 'production' : 'development', + }, + }) + ); + coreContext = { coreId: Symbol(), env, logger, configService: configService as any }; + pluginsSystem = new PluginsSystem(coreContext); + + const syncPlugin = createPlugin('sync-plugin'); + jest.spyOn(syncPlugin, 'setup').mockReturnValue('setup-sync'); + jest.spyOn(syncPlugin, 'start').mockReturnValue('start-sync'); + pluginsSystem.addPlugin(syncPlugin); + + const asyncPlugin = createPlugin('async-plugin'); + jest + .spyOn(asyncPlugin, 'setup') + .mockReturnValue(asyncSetup ? Promise.resolve('setup-async') : 'setup-sync'); + jest + .spyOn(asyncPlugin, 'start') + .mockReturnValue(asyncStart ? Promise.resolve('start-async') : 'start-sync'); + pluginsSystem.addPlugin(asyncPlugin); + + await pluginsSystem.setupPlugins(setupDeps); + await pluginsSystem.startPlugins(startDeps); + }; + + it('logs a warning if a plugin returns a promise from its setup contract in dev mode', async () => { + await runScenario({ + production: false, + asyncSetup: true, + asyncStart: false, + }); + + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); + + it('does not log warnings if a plugin returns a promise from its setup contract in prod mode', async () => { + await runScenario({ + production: true, + asyncSetup: true, + asyncStart: false, + }); + + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn).not.toHaveBeenCalled(); + }); + + it('logs a warning if a plugin returns a promise from its start contract in dev mode', async () => { + await runScenario({ + production: false, + asyncSetup: false, + asyncStart: true, + }); + + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); + + it('does not log warnings if a plugin returns a promise from its start contract in prod mode', async () => { + await runScenario({ + production: true, + asyncSetup: false, + asyncStart: true, + }); + + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn).not.toHaveBeenCalled(); + }); + + it('logs multiple warnings if both `setup` and `start` return promises', async () => { + await runScenario({ + production: false, + asyncSetup: true, + asyncStart: true, + }); + + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + Array [ + "Plugin async-plugin is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); +}); diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index 1b5e3bbb06e71..b7b8c297ea571 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { withTimeout } from '@kbn/std'; +import { withTimeout, isPromise } from '@kbn/std'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { PluginWrapper } from './plugin'; @@ -94,14 +94,25 @@ export class PluginsSystem { return depContracts; }, {} as Record); - const contract = await withTimeout({ - promise: plugin.setup( - createPluginSetupContext(this.coreContext, deps, plugin), - pluginDepContracts - ), - timeout: 30 * Sec, - errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`, - }); + let contract: unknown; + const contractOrPromise = plugin.setup( + createPluginSetupContext(this.coreContext, deps, plugin), + pluginDepContracts + ); + if (isPromise(contractOrPromise)) { + if (this.coreContext.env.mode.dev) { + this.log.warn( + `Plugin ${pluginName} is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.` + ); + } + contract = await withTimeout({ + promise: contractOrPromise, + timeout: 10 * Sec, + errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`, + }); + } else { + contract = contractOrPromise; + } contracts.set(pluginName, contract); this.satupPlugins.push(pluginName); @@ -132,14 +143,25 @@ export class PluginsSystem { return depContracts; }, {} as Record); - const contract = await withTimeout({ - promise: plugin.start( - createPluginStartContext(this.coreContext, deps, plugin), - pluginDepContracts - ), - timeout: 30 * Sec, - errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`, - }); + let contract: unknown; + const contractOrPromise = plugin.start( + createPluginStartContext(this.coreContext, deps, plugin), + pluginDepContracts + ); + if (isPromise(contractOrPromise)) { + if (this.coreContext.env.mode.dev) { + this.log.warn( + `Plugin ${pluginName} is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.` + ); + } + contract = await withTimeout({ + promise: contractOrPromise, + timeout: 10 * Sec, + errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`, + }); + } else { + contract = contractOrPromise; + } contracts.set(pluginName, contract); } diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index 91ccc2dedf272..45db98201b758 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -242,6 +242,23 @@ export interface Plugin< TStart = void, TPluginsSetup extends object = object, TPluginsStart extends object = object +> { + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; + start(core: CoreStart, plugins: TPluginsStart): TStart; + stop?(): void; +} + +/** + * A plugin with asynchronous lifecycle methods. + * + * @deprecated Asynchronous lifecycles are deprecated, and should be migrated to sync {@link Plugin | plugin} + * @public + */ +export interface AsyncPlugin< + TSetup = void, + TStart = void, + TPluginsSetup extends object = object, + TPluginsStart extends object = object > { setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; @@ -383,4 +400,8 @@ export type PluginInitializer< TStart, TPluginsSetup extends object = object, TPluginsStart extends object = object -> = (core: PluginInitializerContext) => Plugin; +> = ( + core: PluginInitializerContext +) => + | Plugin + | AsyncPlugin; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index f3191c5625f8d..09207608908a4 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -203,6 +203,16 @@ export interface AssistantAPIClientParams extends GenericParams { path: '/_migration/assistance'; } +// @public @deprecated +export interface AsyncPlugin { + // (undocumented) + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + // (undocumented) + start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + // (undocumented) + stop?(): void; +} + // @public (undocumented) export interface Authenticated extends AuthResultParams { // (undocumented) @@ -1815,9 +1825,9 @@ export { PackageInfo } // @public export interface Plugin { // (undocumented) - setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; // (undocumented) - start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + start(core: CoreStart, plugins: TPluginsStart): TStart; // (undocumented) stop?(): void; } @@ -1836,7 +1846,7 @@ export interface PluginConfigDescriptor { export type PluginConfigSchema = Type; // @public -export type PluginInitializer = (core: PluginInitializerContext) => Plugin; +export type PluginInitializer = (core: PluginInitializerContext) => Plugin | AsyncPlugin; // @public export interface PluginInitializerContext { @@ -3141,9 +3151,9 @@ export const validBodyOutput: readonly ["data", "stream"]; // Warnings were encountered during analysis: // // src/core/server/http/router/response.ts:306:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:263:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:263:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:371:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" +// src/core/server/plugins/types.ts:280:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:280:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:283:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:388:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" ``` diff --git a/src/plugins/apm_oss/server/plugin.ts b/src/plugins/apm_oss/server/plugin.ts index fc3d105da5024..e504d5f0b9a9f 100644 --- a/src/plugins/apm_oss/server/plugin.ts +++ b/src/plugins/apm_oss/server/plugin.ts @@ -8,7 +8,6 @@ import { Plugin, CoreSetup, PluginInitializerContext } from 'src/core/server'; import { Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; import { APMOSSConfig } from './'; import { HomeServerPluginSetup, TutorialProvider } from '../../home/server'; import { tutorialProvider } from './tutorial'; @@ -17,10 +16,10 @@ export class APMOSSPlugin implements Plugin { constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; } - public async setup(core: CoreSetup, plugins: { home: HomeServerPluginSetup }) { + public setup(core: CoreSetup, plugins: { home: HomeServerPluginSetup }) { const config$ = this.initContext.config.create(); - const config = await config$.pipe(take(1)).toPromise(); + const config = this.initContext.config.get(); const apmTutorialProvider = tutorialProvider({ indexPatternTitle: config.indexPattern, @@ -35,6 +34,7 @@ export class APMOSSPlugin implements Plugin { plugins.home.tutorials.registerTutorial(apmTutorialProvider); return { + config, config$, getRegisteredTutorialProvider: () => apmTutorialProvider, }; @@ -45,6 +45,7 @@ export class APMOSSPlugin implements Plugin { } export interface APMOSSPluginSetup { + config: APMOSSConfig; config$: Observable; getRegisteredTutorialProvider(): TutorialProvider; } diff --git a/src/plugins/console/server/plugin.ts b/src/plugins/console/server/plugin.ts index b2f43b315aa9b..a5f1ca6107600 100644 --- a/src/plugins/console/server/plugin.ts +++ b/src/plugins/console/server/plugin.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import { first } from 'rxjs/operators'; import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; import { ProxyConfigCollection } from './lib'; @@ -28,7 +27,7 @@ export class ConsoleServerPlugin implements Plugin { this.log = this.ctx.logger.get(); } - async setup({ http, capabilities, getStartServices, elasticsearch }: CoreSetup) { + setup({ http, capabilities, getStartServices, elasticsearch }: CoreSetup) { capabilities.registerProvider(() => ({ dev_tools: { show: true, @@ -36,8 +35,8 @@ export class ConsoleServerPlugin implements Plugin { }, })); - const config = await this.ctx.config.create().pipe(first()).toPromise(); - const globalConfig = await this.ctx.config.legacy.globalConfig$.pipe(first()).toPromise(); + const config = this.ctx.config.get(); + const globalConfig = this.ctx.config.legacy.get(); const proxyPathFilters = config.proxyFilter.map((str: string) => new RegExp(str)); this.esLegacyConfigService.setup(elasticsearch.legacy.config$); diff --git a/src/plugins/inspector/public/plugin.tsx b/src/plugins/inspector/public/plugin.tsx index 6aee8b75757c2..93ffaa93cd80e 100644 --- a/src/plugins/inspector/public/plugin.tsx +++ b/src/plugins/inspector/public/plugin.tsx @@ -56,7 +56,7 @@ export class InspectorPublicPlugin implements Plugin { constructor(initializerContext: PluginInitializerContext) {} - public async setup(core: CoreSetup) { + public setup(core: CoreSetup) { this.views = new InspectorViewRegistry(); this.views.register(getRequestsViewDescription()); diff --git a/src/plugins/legacy_export/server/plugin.ts b/src/plugins/legacy_export/server/plugin.ts index 3433d076ee800..ac38f300bd02b 100644 --- a/src/plugins/legacy_export/server/plugin.ts +++ b/src/plugins/legacy_export/server/plugin.ts @@ -7,16 +7,13 @@ */ import { Plugin, CoreSetup, PluginInitializerContext } from 'kibana/server'; -import { first } from 'rxjs/operators'; import { registerRoutes } from './routes'; export class LegacyExportPlugin implements Plugin<{}, {}> { constructor(private readonly initContext: PluginInitializerContext) {} - public async setup({ http }: CoreSetup) { - const globalConfig = await this.initContext.config.legacy.globalConfig$ - .pipe(first()) - .toPromise(); + public setup({ http }: CoreSetup) { + const globalConfig = this.initContext.config.legacy.get(); const router = http.createRouter(); registerRoutes( diff --git a/src/plugins/maps_legacy/server/index.ts b/src/plugins/maps_legacy/server/index.ts index 00d51da501834..4f35c1c1e5fc1 100644 --- a/src/plugins/maps_legacy/server/index.ts +++ b/src/plugins/maps_legacy/server/index.ts @@ -8,7 +8,6 @@ import { Plugin, PluginConfigDescriptor } from 'kibana/server'; import { CoreSetup, PluginInitializerContext } from 'src/core/server'; -import { Observable } from 'rxjs'; import { configSchema, MapsLegacyConfig } from '../config'; import { getUiSettings } from './ui_settings'; @@ -30,7 +29,7 @@ export const config: PluginConfigDescriptor = { }; export interface MapsLegacyPluginSetup { - config$: Observable; + config: MapsLegacyConfig; } export class MapsLegacyPlugin implements Plugin { @@ -43,10 +42,9 @@ export class MapsLegacyPlugin implements Plugin { public setup(core: CoreSetup) { core.uiSettings.register(getUiSettings()); - // @ts-ignore - const config$ = this._initializerContext.config.create(); + const pluginConfig = this._initializerContext.config.get(); return { - config$, + config: pluginConfig, }; } diff --git a/src/plugins/presentation_util/public/plugin.ts b/src/plugins/presentation_util/public/plugin.ts index 15efbf38e7b93..6f74198bb56ab 100644 --- a/src/plugins/presentation_util/public/plugin.ts +++ b/src/plugins/presentation_util/public/plugin.ts @@ -31,10 +31,10 @@ export class PresentationUtilPlugin return {}; } - public async start( + public start( coreStart: CoreStart, startPlugins: PresentationUtilPluginStartDeps - ): Promise { + ): PresentationUtilPluginStart { pluginServices.setRegistry(registry.start({ coreStart, startPlugins })); return { diff --git a/src/plugins/region_map/public/plugin.ts b/src/plugins/region_map/public/plugin.ts index d5d57da400a51..a3a2331cf8f76 100644 --- a/src/plugins/region_map/public/plugin.ts +++ b/src/plugins/region_map/public/plugin.ts @@ -79,7 +79,7 @@ export class RegionMapPlugin implements Plugin { this.logger = this.initializerContext.logger.get(); } - public async setup(core: CoreSetup) { - const config = await this.initializerContext.config - .create() - .pipe(first()) - .toPromise(); + public setup(core: CoreSetup) { + const config = this.initializerContext.config.get(); const collectorSet = new CollectorSet({ logger: this.logger.get('collector-set'), maximumWaitTimeForAllCollectorsInS: config.maximumWaitTimeForAllCollectorsInS, }); - const globalConfig = await this.initializerContext.config.legacy.globalConfig$ - .pipe(first()) - .toPromise(); + const globalConfig = this.initializerContext.config.legacy.get(); const router = core.http.createRouter(); setupRoutes({ diff --git a/src/plugins/vis_type_table/public/plugin.ts b/src/plugins/vis_type_table/public/plugin.ts index 4792ceefde536..0a9d477c26691 100644 --- a/src/plugins/vis_type_table/public/plugin.ts +++ b/src/plugins/vis_type_table/public/plugin.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { PluginInitializerContext, CoreSetup, CoreStart, AsyncPlugin } from 'kibana/public'; import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; import { VisualizationsSetup } from '../../visualizations/public'; import { UsageCollectionSetup } from '../../usage_collection/public'; @@ -34,8 +34,7 @@ export interface TablePluginStartDependencies { /** @internal */ export class TableVisPlugin - implements - Plugin, void, TablePluginSetupDependencies, TablePluginStartDependencies> { + implements AsyncPlugin { initializerContext: PluginInitializerContext; constructor(initializerContext: PluginInitializerContext) { diff --git a/src/plugins/vis_type_timelion/server/index.ts b/src/plugins/vis_type_timelion/server/index.ts index e31aae7fcdda7..1dcb7263c4818 100644 --- a/src/plugins/vis_type_timelion/server/index.ts +++ b/src/plugins/vis_type_timelion/server/index.ts @@ -8,7 +8,7 @@ import { PluginConfigDescriptor, PluginInitializerContext } from '../../../../src/core/server'; import { configSchema, ConfigSchema } from '../config'; -import { Plugin } from './plugin'; +import { TimelionPlugin } from './plugin'; export { PluginSetupContract } from './plugin'; @@ -25,4 +25,4 @@ export const config: PluginConfigDescriptor = { ], }; export const plugin = (initializerContext: PluginInitializerContext) => - new Plugin(initializerContext); + new TimelionPlugin(initializerContext); diff --git a/src/plugins/vis_type_timelion/server/plugin.ts b/src/plugins/vis_type_timelion/server/plugin.ts index 2bb8f7214f904..c1800a09ba35c 100644 --- a/src/plugins/vis_type_timelion/server/plugin.ts +++ b/src/plugins/vis_type_timelion/server/plugin.ts @@ -7,13 +7,12 @@ */ import { i18n } from '@kbn/i18n'; -import { first } from 'rxjs/operators'; import { TypeOf, schema } from '@kbn/config-schema'; import { RecursiveReadonly } from '@kbn/utility-types'; import { deepFreeze } from '@kbn/std'; import type { PluginStart, DataRequestHandlerContext } from '../../../../src/plugins/data/server'; -import { CoreSetup, PluginInitializerContext } from '../../../../src/core/server'; +import { CoreSetup, PluginInitializerContext, Plugin } from '../../../../src/core/server'; import { configSchema } from '../config'; import loadFunctions from './lib/load_functions'; import { functionsRoute } from './routes/functions'; @@ -39,16 +38,12 @@ export interface TimelionPluginStartDeps { /** * Represents Timelion Plugin instance that will be managed by the Kibana plugin system. */ -export class Plugin { +export class TimelionPlugin + implements Plugin, void, TimelionPluginStartDeps> { constructor(private readonly initializerContext: PluginInitializerContext) {} - public async setup( - core: CoreSetup - ): Promise> { - const config = await this.initializerContext.config - .create>() - .pipe(first()) - .toPromise(); + public setup(core: CoreSetup): RecursiveReadonly { + const config = this.initializerContext.config.get>(); const configManager = new ConfigManager(this.initializerContext.config); diff --git a/src/plugins/vis_type_timeseries/public/plugin.ts b/src/plugins/vis_type_timeseries/public/plugin.ts index 59ae89300705e..6900630ffa971 100644 --- a/src/plugins/vis_type_timeseries/public/plugin.ts +++ b/src/plugins/vis_type_timeseries/public/plugin.ts @@ -43,14 +43,14 @@ export interface MetricsPluginStartDependencies { } /** @internal */ -export class MetricsPlugin implements Plugin, void> { +export class MetricsPlugin implements Plugin { initializerContext: PluginInitializerContext; constructor(initializerContext: PluginInitializerContext) { this.initializerContext = initializerContext; } - public async setup( + public setup( core: CoreSetup, { expressions, visualizations, charts, visualize }: MetricsPluginSetupDependencies ) { diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index a01af7484ea99..7cc70f31589c7 100644 --- a/src/plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -54,14 +54,14 @@ export interface VegaPluginStartDependencies { } /** @internal */ -export class VegaPlugin implements Plugin, void> { +export class VegaPlugin implements Plugin { initializerContext: PluginInitializerContext; constructor(initializerContext: PluginInitializerContext) { this.initializerContext = initializerContext; } - public async setup( + public setup( core: CoreSetup, { inspector, data, expressions, visualizations, mapsLegacy }: VegaPluginSetupDependencies ) { diff --git a/src/plugins/vis_type_vislib/public/plugin.ts b/src/plugins/vis_type_vislib/public/plugin.ts index b266a681f8031..9d329c92bede0 100644 --- a/src/plugins/vis_type_vislib/public/plugin.ts +++ b/src/plugins/vis_type_vislib/public/plugin.ts @@ -46,7 +46,7 @@ export class VisTypeVislibPlugin Plugin { constructor(public initializerContext: PluginInitializerContext) {} - public async setup( + public setup( core: VisTypeVislibCoreSetup, { expressions, visualizations, charts }: VisTypeVislibPluginSetupDependencies ) { diff --git a/src/plugins/vis_type_xy/public/plugin.ts b/src/plugins/vis_type_xy/public/plugin.ts index 5be971a085d3c..75a2f4fb6895c 100644 --- a/src/plugins/vis_type_xy/public/plugin.ts +++ b/src/plugins/vis_type_xy/public/plugin.ts @@ -59,7 +59,7 @@ export class VisTypeXyPlugin VisTypeXyPluginSetupDependencies, VisTypeXyPluginStartDependencies > { - public async setup( + public setup( core: VisTypeXyCoreSetup, { expressions, visualizations, charts, usageCollection }: VisTypeXyPluginSetupDependencies ) { diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 1cad0ca7ca396..3d82e6c60a1b6 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -84,7 +84,7 @@ export class VisualizePlugin constructor(private initializerContext: PluginInitializerContext) {} - public async setup( + public setup( core: CoreSetup, { home, urlForwarding, data }: VisualizePluginSetupDependencies ) { diff --git a/test/plugin_functional/plugins/app_link_test/public/plugin.ts b/test/plugin_functional/plugins/app_link_test/public/plugin.ts index 7f92cdccd7243..8d75cb09469bc 100644 --- a/test/plugin_functional/plugins/app_link_test/public/plugin.ts +++ b/test/plugin_functional/plugins/app_link_test/public/plugin.ts @@ -10,7 +10,7 @@ import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; import { renderApp } from './app'; export class CoreAppLinkPlugin implements Plugin { - public async setup(core: CoreSetup, deps: {}) { + public setup(core: CoreSetup, deps: {}) { core.application.register({ id: 'applink_start', title: 'AppLink Start', diff --git a/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx b/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx index 6a167b17befd1..48c8d85b21dac 100644 --- a/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx +++ b/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx @@ -42,7 +42,7 @@ export class CorePluginBPlugin }; } - public async start(core: CoreStart, deps: {}) { + public start(core: CoreStart, deps: {}) { return { sendSystemRequest: async (asSystemRequest: boolean) => { const response = await core.http.post('/core_plugin_b/system_request', { diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 9797a55fa0e3d..8fbacc71d30cb 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -6,9 +6,7 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { first } from 'rxjs/operators'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { Observable } from 'rxjs'; import { PluginInitializerContext, Plugin, @@ -136,11 +134,9 @@ const includedHiddenTypes = [ ALERT_SAVED_OBJECT_TYPE, ]; -export class ActionsPlugin implements Plugin, PluginStartContract> { - private readonly config: Promise; - +export class ActionsPlugin implements Plugin { private readonly logger: Logger; - private actionsConfig?: ActionsConfig; + private readonly actionsConfig: ActionsConfig; private taskRunnerFactory?: TaskRunnerFactory; private actionTypeRegistry?: ActionTypeRegistry; private actionExecutor?: ActionExecutor; @@ -151,20 +147,20 @@ export class ActionsPlugin implements Plugin, Plugi private isESOUsingEphemeralEncryptionKey?: boolean; private readonly telemetryLogger: Logger; private readonly preconfiguredActions: PreConfiguredAction[]; - private readonly kibanaIndexConfig: Observable<{ kibana: { index: string } }>; + private readonly kibanaIndexConfig: { kibana: { index: string } }; constructor(initContext: PluginInitializerContext) { - this.config = initContext.config.create().pipe(first()).toPromise(); + this.actionsConfig = initContext.config.get(); this.logger = initContext.logger.get('actions'); this.telemetryLogger = initContext.logger.get('usage'); this.preconfiguredActions = []; - this.kibanaIndexConfig = initContext.config.legacy.globalConfig$; + this.kibanaIndexConfig = initContext.config.legacy.get(); } - public async setup( + public setup( core: CoreSetup, plugins: ActionsPluginsSetup - ): Promise { + ): PluginSetupContract { this.licenseState = new LicenseState(plugins.licensing.license$); this.isESOUsingEphemeralEncryptionKey = plugins.encryptedSavedObjects.usingEphemeralEncryptionKey; @@ -190,7 +186,6 @@ export class ActionsPlugin implements Plugin, Plugi // get executions count const taskRunnerFactory = new TaskRunnerFactory(actionExecutor); - this.actionsConfig = (await this.config) as ActionsConfig; const actionsConfigUtils = getActionsConfigurationUtilities(this.actionsConfig); for (const preconfiguredId of Object.keys(this.actionsConfig.preconfigured)) { @@ -229,20 +224,18 @@ export class ActionsPlugin implements Plugin, Plugi ); } - this.kibanaIndexConfig.subscribe((config) => { - core.http.registerRouteHandlerContext( - 'actions', - this.createRouteHandlerContext(core, config.kibana.index) + core.http.registerRouteHandlerContext( + 'actions', + this.createRouteHandlerContext(core, this.kibanaIndexConfig.kibana.index) + ); + if (usageCollection) { + initializeActionsTelemetry( + this.telemetryLogger, + plugins.taskManager, + core, + this.kibanaIndexConfig.kibana.index ); - if (usageCollection) { - initializeActionsTelemetry( - this.telemetryLogger, - plugins.taskManager, - core, - config.kibana.index - ); - } - }); + } // Routes const router = core.http.createRouter(); @@ -304,7 +297,7 @@ export class ActionsPlugin implements Plugin, Plugi request ); - const kibanaIndex = (await kibanaIndexConfig.pipe(first()).toPromise()).kibana.index; + const kibanaIndex = kibanaIndexConfig.kibana.index; return new ActionsClient({ unsecuredSavedObjectsClient, diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index e2840dbdf5ef7..49fded8649c46 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -61,7 +61,7 @@ export class APMPlugin implements Plugin { this.initContext = initContext; } - public async setup( + public setup( core: CoreSetup, plugins: { apmOss: APMOSSPluginSetup; @@ -98,7 +98,10 @@ export class APMPlugin implements Plugin { }); } - this.currentConfig = await mergedConfig$.pipe(take(1)).toPromise(); + this.currentConfig = mergeConfigs( + plugins.apmOss.config, + this.initContext.config.get() + ); if ( plugins.taskManager && diff --git a/x-pack/plugins/beats_management/server/plugin.ts b/x-pack/plugins/beats_management/server/plugin.ts index 6a814f68a67f4..3093d5d9b8d29 100644 --- a/x-pack/plugins/beats_management/server/plugin.ts +++ b/x-pack/plugins/beats_management/server/plugin.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { take } from 'rxjs/operators'; -import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server'; +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext, Logger } from 'src/core/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { SecurityPluginSetup } from '../../security/server'; import { LicensingPluginStart } from '../../licensing/server'; @@ -27,14 +26,17 @@ interface StartDeps { } export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDeps> { + private readonly logger: Logger; private securitySetup?: SecurityPluginSetup; private beatsLibs?: CMServerLibs; constructor( private readonly initializerContext: PluginInitializerContext - ) {} + ) { + this.logger = initializerContext.logger.get(); + } - public async setup(core: CoreSetup, { features, security }: SetupDeps) { + public setup(core: CoreSetup, { features, security }: SetupDeps) { this.securitySetup = security; const router = core.http.createRouter(); @@ -64,8 +66,8 @@ export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDep return {}; } - public async start({ elasticsearch }: CoreStart, { licensing }: StartDeps) { - const config = await this.initializerContext.config.create().pipe(take(1)).toPromise(); + public start({ elasticsearch }: CoreStart, { licensing }: StartDeps) { + const config = this.initializerContext.config.get(); const logger = this.initializerContext.logger.get(); const kibanaVersion = this.initializerContext.env.packageInfo.version; @@ -78,7 +80,9 @@ export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDep kibanaVersion, }); - await this.beatsLibs.database.putTemplate(INDEX_NAMES.BEATS, beatsIndexTemplate); + this.beatsLibs.database.putTemplate(INDEX_NAMES.BEATS, beatsIndexTemplate).catch((e) => { + this.logger.error(`Error create beats template: ${e.message}`); + }); return {}; } diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts index 7387ae1a203c2..345f6099009fc 100644 --- a/x-pack/plugins/canvas/server/plugin.ts +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; import { CoreSetup, PluginInitializerContext, Plugin, Logger, CoreStart } from 'src/core/server'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { BfetchServerSetup } from 'src/plugins/bfetch/server'; @@ -34,7 +33,7 @@ export class CanvasPlugin implements Plugin { this.logger = initializerContext.logger.get(); } - public async setup(coreSetup: CoreSetup, plugins: PluginsSetup) { + public setup(coreSetup: CoreSetup, plugins: PluginsSetup) { coreSetup.savedObjects.registerType(customElementType); coreSetup.savedObjects.registerType(workpadType); coreSetup.savedObjects.registerType(workpadTemplateType); @@ -84,9 +83,7 @@ export class CanvasPlugin implements Plugin { ); // we need the kibana index provided by global config for the Canvas usage collector - const globalConfig = await this.initializerContext.config.legacy.globalConfig$ - .pipe(first()) - .toPromise(); + const globalConfig = this.initializerContext.config.legacy.get(); registerCanvasUsageCollector(plugins.usageCollection, globalConfig.kibana.index); setupInterpreter(plugins.expressions); diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index 589093461a5e0..8b4fdc73dab44 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { first, map } from 'rxjs/operators'; import { IContextProvider, KibanaRequest, Logger, PluginInitializerContext } from 'kibana/server'; import { CoreSetup, CoreStart } from 'src/core/server'; @@ -38,8 +37,8 @@ import { createCaseClient } from './client'; import { registerConnectors } from './connectors'; import type { CasesRequestHandlerContext } from './types'; -function createConfig$(context: PluginInitializerContext) { - return context.config.create().pipe(map((config) => config)); +function createConfig(context: PluginInitializerContext) { + return context.config.get(); } export interface PluginsSetup { @@ -60,7 +59,7 @@ export class CasePlugin { } public async setup(core: CoreSetup, plugins: PluginsSetup) { - const config = await createConfig$(this.initializerContext).pipe(first()).toPromise(); + const config = createConfig(this.initializerContext); if (!config.enabled) { return; @@ -118,7 +117,7 @@ export class CasePlugin { }); } - public async start(core: CoreStart) { + public start(core: CoreStart) { this.log.debug(`Starting Case Workflow`); this.alertsService!.initialize(core.elasticsearch.client); diff --git a/x-pack/plugins/cloud/public/plugin.ts b/x-pack/plugins/cloud/public/plugin.ts index eeb295b264f60..4c12aa3d92b47 100644 --- a/x-pack/plugins/cloud/public/plugin.ts +++ b/x-pack/plugins/cloud/public/plugin.ts @@ -45,7 +45,7 @@ export class CloudPlugin implements Plugin { this.isCloudEnabled = false; } - public async setup(core: CoreSetup, { home }: CloudSetupDependencies) { + public setup(core: CoreSetup, { home }: CloudSetupDependencies) { const { id, resetPasswordUrl, deploymentUrl } = this.config; this.isCloudEnabled = getIsCloudEnabled(id); diff --git a/x-pack/plugins/cloud/server/plugin.ts b/x-pack/plugins/cloud/server/plugin.ts index 55ed72ca01957..6abfb864d1cd0 100644 --- a/x-pack/plugins/cloud/server/plugin.ts +++ b/x-pack/plugins/cloud/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; -import { Observable } from 'rxjs'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; import { CloudConfigType } from './config'; @@ -28,25 +26,24 @@ export interface CloudSetup { export class CloudPlugin implements Plugin { private readonly logger: Logger; - private readonly config$: Observable; + private readonly config: CloudConfigType; constructor(private readonly context: PluginInitializerContext) { this.logger = this.context.logger.get(); - this.config$ = this.context.config.create(); + this.config = this.context.config.get(); } - public async setup(core: CoreSetup, { usageCollection }: PluginsSetup) { + public setup(core: CoreSetup, { usageCollection }: PluginsSetup) { this.logger.debug('Setting up Cloud plugin'); - const config = await this.config$.pipe(first()).toPromise(); - const isCloudEnabled = getIsCloudEnabled(config.id); + const isCloudEnabled = getIsCloudEnabled(this.config.id); registerCloudUsageCollector(usageCollection, { isCloudEnabled }); return { - cloudId: config.id, + cloudId: this.config.id, isCloudEnabled, apm: { - url: config.apm?.url, - secretToken: config.apm?.secret_token, + url: this.config.apm?.url, + secretToken: this.config.apm?.secret_token, }, }; } diff --git a/x-pack/plugins/code/server/plugin.ts b/x-pack/plugins/code/server/plugin.ts index c9197a30b5214..eb7481d12387d 100644 --- a/x-pack/plugins/code/server/plugin.ts +++ b/x-pack/plugins/code/server/plugin.ts @@ -5,22 +5,18 @@ * 2.0. */ -import { first } from 'rxjs/operators'; import { TypeOf } from '@kbn/config-schema'; -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext, Plugin } from 'src/core/server'; import { CodeConfigSchema } from './config'; /** * Represents Code Plugin instance that will be managed by the Kibana plugin system. */ -export class CodePlugin { +export class CodePlugin implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} public async setup() { - const config = await this.initializerContext.config - .create>() - .pipe(first()) - .toPromise(); + const config = this.initializerContext.config.get>(); if (config && Object.keys(config).length > 0) { this.initializerContext.logger diff --git a/x-pack/plugins/encrypted_saved_objects/server/index.ts b/x-pack/plugins/encrypted_saved_objects/server/index.ts index 8097c22cfbabc..53b020e5b8241 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/index.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/index.ts @@ -7,7 +7,7 @@ import { PluginInitializerContext } from 'src/core/server'; import { ConfigSchema } from './config'; -import { Plugin } from './plugin'; +import { EncryptedSavedObjectsPlugin } from './plugin'; export { EncryptedSavedObjectTypeRegistration, EncryptionError } from './crypto'; export { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart } from './plugin'; @@ -15,4 +15,4 @@ export { EncryptedSavedObjectsClient } from './saved_objects'; export const config = { schema: ConfigSchema }; export const plugin = (initializerContext: PluginInitializerContext) => - new Plugin(initializerContext); + new EncryptedSavedObjectsPlugin(initializerContext); diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts index 2324c31b13d00..823a6b0afa9dc 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Plugin } from './plugin'; +import { EncryptedSavedObjectsPlugin } from './plugin'; import { ConfigSchema } from './config'; import { coreMock } from 'src/core/server/mocks'; @@ -13,12 +13,12 @@ import { securityMock } from '../../security/server/mocks'; describe('EncryptedSavedObjects Plugin', () => { describe('setup()', () => { - it('exposes proper contract', async () => { - const plugin = new Plugin( + it('exposes proper contract', () => { + const plugin = new EncryptedSavedObjectsPlugin( coreMock.createPluginInitializerContext(ConfigSchema.validate({}, { dist: true })) ); - await expect(plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() })) - .resolves.toMatchInlineSnapshot(` + expect(plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() })) + .toMatchInlineSnapshot(` Object { "createMigration": [Function], "registerType": [Function], @@ -29,14 +29,14 @@ describe('EncryptedSavedObjects Plugin', () => { }); describe('start()', () => { - it('exposes proper contract', async () => { - const plugin = new Plugin( + it('exposes proper contract', () => { + const plugin = new EncryptedSavedObjectsPlugin( coreMock.createPluginInitializerContext(ConfigSchema.validate({}, { dist: true })) ); - await plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() }); + plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() }); const startContract = plugin.start(); - await expect(startContract).toMatchInlineSnapshot(` + expect(startContract).toMatchInlineSnapshot(` Object { "getClient": [Function], "isEncryptionError": [Function], diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts index bfc757accaa82..e846b133c26e0 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts @@ -5,9 +5,8 @@ * 2.0. */ -import { first, map } from 'rxjs/operators'; import nodeCrypto from '@elastic/node-crypto'; -import { Logger, PluginInitializerContext, CoreSetup } from 'src/core/server'; +import { Logger, PluginInitializerContext, CoreSetup, Plugin } from 'src/core/server'; import { TypeOf } from '@kbn/config-schema'; import { SecurityPluginSetup } from '../../security/server'; import { createConfig, ConfigSchema } from './config'; @@ -40,7 +39,9 @@ export interface EncryptedSavedObjectsPluginStart { /** * Represents EncryptedSavedObjects Plugin instance that will be managed by the Kibana plugin system. */ -export class Plugin { +export class EncryptedSavedObjectsPlugin + implements + Plugin { private readonly logger: Logger; private savedObjectsSetup!: ClientInstanciator; @@ -48,17 +49,11 @@ export class Plugin { this.logger = this.initializerContext.logger.get(); } - public async setup( - core: CoreSetup, - deps: PluginsSetup - ): Promise { - const config = await this.initializerContext.config - .create>() - .pipe( - map((rawConfig) => createConfig(rawConfig, this.initializerContext.logger.get('config'))) - ) - .pipe(first()) - .toPromise(); + public setup(core: CoreSetup, deps: PluginsSetup): EncryptedSavedObjectsPluginSetup { + const config = createConfig( + this.initializerContext.config.get>(), + this.initializerContext.logger.get('config') + ); const auditLogger = new EncryptedSavedObjectsAuditLogger( deps.security?.audit.getLogger('encryptedSavedObjects') ); diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index 4ea8ef2c089e4..569479f921cdd 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { Plugin, PluginInitializerContext, @@ -66,19 +64,19 @@ export interface RouteDependencies { } export class EnterpriseSearchPlugin implements Plugin { - private config: Observable; - private logger: Logger; + private readonly config: ConfigType; + private readonly logger: Logger; constructor(initializerContext: PluginInitializerContext) { - this.config = initializerContext.config.create(); + this.config = initializerContext.config.get(); this.logger = initializerContext.logger.get(); } - public async setup( + public setup( { capabilities, http, savedObjects, getStartServices }: CoreSetup, { usageCollection, security, features }: PluginsSetup ) { - const config = await this.config.pipe(first()).toPromise(); + const config = this.config; const log = this.logger; /** diff --git a/x-pack/plugins/event_log/server/plugin.ts b/x-pack/plugins/event_log/server/plugin.ts index 04be4ce67c12d..9cc874735cc0e 100644 --- a/x-pack/plugins/event_log/server/plugin.ts +++ b/x-pack/plugins/event_log/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { CoreSetup, CoreStart, @@ -24,7 +22,6 @@ import type { IEventLogConfig, IEventLogService, IEventLogger, - IEventLogConfig$, IEventLogClientService, } from './types'; import { findRoute } from './routes'; @@ -48,32 +45,29 @@ interface PluginStartDeps { } export class Plugin implements CorePlugin { - private readonly config$: IEventLogConfig$; + private readonly config: IEventLogConfig; private systemLogger: Logger; private eventLogService?: EventLogService; private esContext?: EsContext; private eventLogger?: IEventLogger; - private globalConfig$: Observable; + private globalConfig: SharedGlobalConfig; private eventLogClientService?: EventLogClientService; private savedObjectProviderRegistry: SavedObjectProviderRegistry; private kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; constructor(private readonly context: PluginInitializerContext) { this.systemLogger = this.context.logger.get(); - this.config$ = this.context.config.create(); - this.globalConfig$ = this.context.config.legacy.globalConfig$; + this.config = this.context.config.get(); + this.globalConfig = this.context.config.legacy.get(); this.savedObjectProviderRegistry = new SavedObjectProviderRegistry(); this.kibanaVersion = this.context.env.packageInfo.version; } - async setup(core: CoreSetup): Promise { - const globalConfig = await this.globalConfig$.pipe(first()).toPromise(); - const kibanaIndex = globalConfig.kibana.index; + setup(core: CoreSetup): IEventLogService { + const kibanaIndex = this.globalConfig.kibana.index; this.systemLogger.debug('setting up plugin'); - const config = await this.config$.pipe(first()).toPromise(); - this.esContext = createEsContext({ logger: this.systemLogger, // TODO: get index prefix from config.get(kibana.index) @@ -85,7 +79,7 @@ export class Plugin implements CorePlugin { + start(core: CoreStart, { spaces }: PluginStartDeps): IEventLogClientService { this.systemLogger.debug('starting plugin'); if (!this.esContext) throw new Error('esContext not initialized'); diff --git a/x-pack/plugins/event_log/server/types.ts b/x-pack/plugins/event_log/server/types.ts index 786f5ba587d26..0e5e62b591290 100644 --- a/x-pack/plugins/event_log/server/types.ts +++ b/x-pack/plugins/event_log/server/types.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; import { schema, TypeOf } from '@kbn/config-schema'; import type { IRouter, KibanaRequest, RequestHandlerContext } from 'src/core/server'; @@ -25,7 +24,6 @@ export const ConfigSchema = schema.object({ }); export type IEventLogConfig = TypeOf; -export type IEventLogConfig$ = Observable>; // the object exposed by plugin.setup() export interface IEventLogService { diff --git a/x-pack/plugins/features/server/index.ts b/x-pack/plugins/features/server/index.ts index 111f294b6ad55..0890274fed950 100644 --- a/x-pack/plugins/features/server/index.ts +++ b/x-pack/plugins/features/server/index.ts @@ -6,7 +6,7 @@ */ import { PluginInitializerContext } from '../../../../src/core/server'; -import { Plugin } from './plugin'; +import { FeaturesPlugin } from './plugin'; // These exports are part of public Features plugin contract, any change in signature of exported // functions or removal of exports should be considered as a breaking change. Ideally we should @@ -25,4 +25,4 @@ export { export { PluginSetupContract, PluginStartContract } from './plugin'; export const plugin = (initializerContext: PluginInitializerContext) => - new Plugin(initializerContext); + new FeaturesPlugin(initializerContext); diff --git a/x-pack/plugins/features/server/plugin.test.ts b/x-pack/plugins/features/server/plugin.test.ts index 4462edeed9510..0de03e54e1f79 100644 --- a/x-pack/plugins/features/server/plugin.test.ts +++ b/x-pack/plugins/features/server/plugin.test.ts @@ -6,7 +6,7 @@ */ import { coreMock, savedObjectsServiceMock } from 'src/core/server/mocks'; -import { Plugin } from './plugin'; +import { FeaturesPlugin } from './plugin'; describe('Features Plugin', () => { let initContext: ReturnType; @@ -31,7 +31,7 @@ describe('Features Plugin', () => { }); it('returns OSS + registered kibana features', async () => { - const plugin = new Plugin(initContext); + const plugin = new FeaturesPlugin(initContext); const { registerKibanaFeature } = await plugin.setup(coreSetup, {}); registerKibanaFeature({ id: 'baz', @@ -58,7 +58,7 @@ describe('Features Plugin', () => { }); it('returns OSS + registered kibana features with timelion when available', async () => { - const plugin = new Plugin(initContext); + const plugin = new FeaturesPlugin(initContext); const { registerKibanaFeature: registerFeature } = await plugin.setup(coreSetup, { visTypeTimelion: { uiEnabled: true }, }); @@ -88,7 +88,7 @@ describe('Features Plugin', () => { }); it('registers kibana features with not hidden saved objects types', async () => { - const plugin = new Plugin(initContext); + const plugin = new FeaturesPlugin(initContext); await plugin.setup(coreSetup, {}); const { getKibanaFeatures } = plugin.start(coreStart); @@ -101,7 +101,7 @@ describe('Features Plugin', () => { }); it('returns registered elasticsearch features', async () => { - const plugin = new Plugin(initContext); + const plugin = new FeaturesPlugin(initContext); const { registerElasticsearchFeature } = await plugin.setup(coreSetup, {}); registerElasticsearchFeature({ id: 'baz', @@ -123,7 +123,7 @@ describe('Features Plugin', () => { }); it('registers a capabilities provider', async () => { - const plugin = new Plugin(initContext); + const plugin = new FeaturesPlugin(initContext); await plugin.setup(coreSetup, {}); expect(coreSetup.capabilities.registerProvider).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index e96c257516b98..6a9fd1da826a6 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -12,6 +12,7 @@ import { CoreStart, SavedObjectsServiceStart, Logger, + Plugin, PluginInitializerContext, } from '../../../../src/core/server'; import { Capabilities as UICapabilities } from '../../../../src/core/server'; @@ -59,7 +60,9 @@ interface TimelionSetupContract { /** * Represents Features Plugin instance that will be managed by the Kibana plugin system. */ -export class Plugin { +export class FeaturesPlugin + implements + Plugin, RecursiveReadonly> { private readonly logger: Logger; private readonly featureRegistry: FeatureRegistry = new FeatureRegistry(); private isTimelionEnabled: boolean = false; @@ -68,10 +71,10 @@ export class Plugin { this.logger = this.initializerContext.logger.get(); } - public async setup( + public setup( core: CoreSetup, { visTypeTimelion }: { visTypeTimelion?: TimelionSetupContract } - ): Promise> { + ): RecursiveReadonly { this.isTimelionEnabled = visTypeTimelion !== undefined && visTypeTimelion.uiEnabled; defineRoutes({ diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts index fce8e89a6573d..50e647e271ecc 100644 --- a/x-pack/plugins/fleet/public/plugin.ts +++ b/x-pack/plugins/fleet/public/plugin.ts @@ -155,7 +155,7 @@ export class FleetPlugin implements Plugin { + public start(core: CoreStart): FleetStart { let successPromise: ReturnType; return { diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 1aa6b42611a34..7378d45e1bb3a 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -12,7 +12,7 @@ import { CoreStart, ElasticsearchServiceStart, Logger, - Plugin, + AsyncPlugin, PluginInitializerContext, SavedObjectsServiceStart, HttpServiceSetup, @@ -169,7 +169,7 @@ export interface FleetStartContract { } export class FleetPlugin - implements Plugin { + implements AsyncPlugin { private licensing$!: Observable; private config$: Observable; private cloud: CloudSetup | undefined; diff --git a/x-pack/plugins/global_search/server/plugin.ts b/x-pack/plugins/global_search/server/plugin.ts index 8d560d9a0f553..d7c06a92f70e0 100644 --- a/x-pack/plugins/global_search/server/plugin.ts +++ b/x-pack/plugins/global_search/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server'; import { LicensingPluginStart } from '../../licensing/server'; import { LicenseChecker, ILicenseChecker } from '../common/license_checker'; @@ -33,20 +31,19 @@ export class GlobalSearchPlugin GlobalSearchPluginSetupDeps, GlobalSearchPluginStartDeps > { - private readonly config$: Observable; + private readonly config: GlobalSearchConfigType; private readonly searchService = new SearchService(); private searchServiceStart?: SearchServiceStart; private licenseChecker?: ILicenseChecker; constructor(context: PluginInitializerContext) { - this.config$ = context.config.create(); + this.config = context.config.get(); } - public async setup(core: CoreSetup<{}, GlobalSearchPluginStart>) { - const config = await this.config$.pipe(take(1)).toPromise(); + public setup(core: CoreSetup<{}, GlobalSearchPluginStart>) { const { registerResultProvider } = this.searchService.setup({ basePath: core.http.basePath, - config, + config: this.config, }); registerRoutes(core.http.createRouter()); diff --git a/x-pack/plugins/graph/server/plugin.ts b/x-pack/plugins/graph/server/plugin.ts index 5c13756842039..32dac5fba86f9 100644 --- a/x-pack/plugins/graph/server/plugin.ts +++ b/x-pack/plugins/graph/server/plugin.ts @@ -20,7 +20,7 @@ import { graphWorkspace } from './saved_objects'; export class GraphPlugin implements Plugin { private licenseState: LicenseState | null = null; - public async setup( + public setup( core: CoreSetup, { licensing, diff --git a/x-pack/plugins/index_lifecycle_management/server/plugin.ts b/x-pack/plugins/index_lifecycle_management/server/plugin.ts index 532cf253c7f89..95793c0cad465 100644 --- a/x-pack/plugins/index_lifecycle_management/server/plugin.ts +++ b/x-pack/plugins/index_lifecycle_management/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { CoreSetup, @@ -52,22 +50,19 @@ const indexLifecycleDataEnricher = async ( }; export class IndexLifecycleManagementServerPlugin implements Plugin { - private readonly config$: Observable; + private readonly config: IndexLifecycleManagementConfig; private readonly license: License; private readonly logger: Logger; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); - this.config$ = initializerContext.config.create(); + this.config = initializerContext.config.get(); this.license = new License(); } - async setup( - { http }: CoreSetup, - { licensing, indexManagement, features }: Dependencies - ): Promise { + setup({ http }: CoreSetup, { licensing, indexManagement, features }: Dependencies): void { const router = http.createRouter(); - const config = await this.config$.pipe(first()).toPromise(); + const config = this.config; this.license.setup( { diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index e91e085207cb7..99555fa56acd5 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -8,8 +8,7 @@ import { Server } from '@hapi/hapi'; import { schema, TypeOf } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; -import { Observable } from 'rxjs'; -import { CoreSetup, PluginInitializerContext } from 'src/core/server'; +import { CoreSetup, PluginInitializerContext, Plugin } from 'src/core/server'; import { InfraStaticSourceConfiguration } from '../common/http_api/source_api'; import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view'; import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view'; @@ -79,22 +78,15 @@ export interface InfraPluginSetup { ) => void; } -export class InfraServerPlugin { - private config$: Observable; - public config = {} as InfraConfig; +export class InfraServerPlugin implements Plugin { + public config: InfraConfig; public libs: InfraBackendLibs | undefined; constructor(context: PluginInitializerContext) { - this.config$ = context.config.create(); + this.config = context.config.get(); } - async setup(core: CoreSetup, plugins: InfraServerPluginSetupDeps) { - await new Promise((resolve) => { - this.config$.subscribe((configValue) => { - this.config = configValue; - resolve(); - }); - }); + setup(core: CoreSetup, plugins: InfraServerPluginSetupDeps) { const framework = new KibanaFramework(core, this.config, plugins); const sources = new InfraSources({ config: this.config, diff --git a/x-pack/plugins/licensing/public/plugin.ts b/x-pack/plugins/licensing/public/plugin.ts index 0207f79310273..1db463a47dbf0 100644 --- a/x-pack/plugins/licensing/public/plugin.ts +++ b/x-pack/plugins/licensing/public/plugin.ts @@ -123,7 +123,7 @@ export class LicensingPlugin implements Plugin { private stop$ = new Subject(); private readonly logger: Logger; - private readonly config$: Observable; + private readonly config: LicenseConfigType; private loggingSubscription?: Subscription; private featureUsage = new FeatureUsageService(); @@ -92,13 +91,12 @@ export class LicensingPlugin implements Plugin(); + this.config = this.context.config.get(); } - public async setup(core: CoreSetup<{}, LicensingPluginStart>) { + public setup(core: CoreSetup<{}, LicensingPluginStart>) { this.logger.debug('Setting up Licensing plugin'); - const config = await this.config$.pipe(take(1)).toPromise(); - const pollingFrequency = config.api_polling_frequency; + const pollingFrequency = this.config.api_polling_frequency; async function callAsInternalUser( ...args: Parameters @@ -225,7 +223,7 @@ export class LicensingPlugin implements Plugin> => { - return context.config.create().pipe(map((config) => config)); -}; diff --git a/x-pack/plugins/lists/server/plugin.ts b/x-pack/plugins/lists/server/plugin.ts index bc064e236b658..b79d6a0b89a57 100644 --- a/x-pack/plugins/lists/server/plugin.ts +++ b/x-pack/plugins/lists/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; import { Logger, Plugin, PluginInitializerContext } from 'kibana/server'; import type { CoreSetup, CoreStart } from 'src/core/server'; @@ -23,7 +22,6 @@ import type { ListsRequestHandlerContext, PluginsStart, } from './types'; -import { createConfig$ } from './create_config'; import { getSpaceId } from './get_space_id'; import { getUser } from './get_user'; import { initSavedObjects } from './saved_objects'; @@ -32,17 +30,17 @@ import { ExceptionListClient } from './services/exception_lists/exception_list_c export class ListPlugin implements Plugin, ListsPluginStart, {}, PluginsStart> { private readonly logger: Logger; + private readonly config: ConfigType; private spaces: SpacesServiceStart | undefined | null; - private config: ConfigType | undefined | null; private security: SecurityPluginStart | undefined | null; constructor(private readonly initializerContext: PluginInitializerContext) { this.logger = this.initializerContext.logger.get(); + this.config = this.initializerContext.config.get(); } public async setup(core: CoreSetup): Promise { - const config = await createConfig$(this.initializerContext).pipe(first()).toPromise(); - this.config = config; + const { config } = this; initSavedObjects(core.savedObjects); diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index 786e35212ec7b..7440b6ee1e1df 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; -import { take } from 'rxjs/operators'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; // @ts-ignore @@ -134,12 +133,11 @@ export class MapsPlugin implements Plugin { } // @ts-ignore - async setup(core: CoreSetup, plugins: SetupDeps) { + setup(core: CoreSetup, plugins: SetupDeps) { const { usageCollection, home, licensing, features, mapsLegacy } = plugins; - // @ts-ignore + const mapsLegacyConfig = mapsLegacy.config; const config$ = this._initializerContext.config.create(); - const mapsLegacyConfig = await mapsLegacy.config$.pipe(take(1)).toPromise(); - const currentConfig = await config$.pipe(take(1)).toPromise(); + const currentConfig = this._initializerContext.config.get(); // @ts-ignore const mapsEnabled = currentConfig.enabled; diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index 3d3671ac0a6a4..b950b064774b1 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -49,7 +49,7 @@ export class MonitoringPlugin Plugin { constructor(private initializerContext: PluginInitializerContext) {} - public async setup( + public setup( core: CoreSetup, plugins: MonitoringSetupPluginDependencies ) { diff --git a/x-pack/plugins/monitoring/server/index.ts b/x-pack/plugins/monitoring/server/index.ts index 012c050cd3fa8..97e572d15327c 100644 --- a/x-pack/plugins/monitoring/server/index.ts +++ b/x-pack/plugins/monitoring/server/index.ts @@ -7,13 +7,15 @@ import { TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext, PluginConfigDescriptor } from '../../../../src/core/server'; -import { Plugin } from './plugin'; +import { MonitoringPlugin } from './plugin'; import { configSchema } from './config'; import { deprecations } from './deprecations'; export { KibanaSettingsCollector } from './kibana_monitoring/collectors'; export { MonitoringConfig } from './config'; -export const plugin = (initContext: PluginInitializerContext) => new Plugin(initContext); +export { MonitoringPluginSetup, IBulkUploader } from './types'; + +export const plugin = (initContext: PluginInitializerContext) => new MonitoringPlugin(initContext); export const config: PluginConfigDescriptor> = { schema: configSchema, deprecations, diff --git a/x-pack/plugins/monitoring/server/plugin.test.ts b/x-pack/plugins/monitoring/server/plugin.test.ts index 2a5138d0d8880..08224980a558f 100644 --- a/x-pack/plugins/monitoring/server/plugin.test.ts +++ b/x-pack/plugins/monitoring/server/plugin.test.ts @@ -6,16 +6,9 @@ */ import { coreMock } from 'src/core/server/mocks'; -import { Plugin } from './plugin'; -import { combineLatest } from 'rxjs'; +import { MonitoringPlugin } from './plugin'; import { AlertsFactory } from './alerts'; -jest.mock('rxjs', () => ({ - // @ts-ignore - ...jest.requireActual('rxjs'), - combineLatest: jest.fn(), -})); - jest.mock('./es_client/instantiate_client', () => ({ instantiateClient: jest.fn().mockImplementation(() => ({ cluster: {}, @@ -32,30 +25,11 @@ jest.mock('./kibana_monitoring/collectors', () => ({ registerCollectors: jest.fn(), })); -describe('Monitoring plugin', () => { - const initializerContext = { - logger: { - get: jest.fn().mockImplementation(() => ({ - info: jest.fn(), - })), - }, - config: { - create: jest.fn().mockImplementation(() => ({ - pipe: jest.fn().mockImplementation(() => ({ - toPromise: jest.fn(), - })), - })), - legacy: { - globalConfig$: {}, - }, - }, - env: { - packageInfo: { - version: '1.0.0', - }, - }, - }; +jest.mock('./config', () => ({ + createConfig: (config: any) => config, +})); +describe('Monitoring plugin', () => { const coreSetup = coreMock.createSetup(); coreSetup.http.getServerInfo.mockReturnValue({ port: 5601 } as any); coreSetup.status.overall$.subscribe = jest.fn(); @@ -71,7 +45,6 @@ describe('Monitoring plugin', () => { }, }; - let config = {}; const defaultConfig = { ui: { elasticsearch: {}, @@ -83,20 +56,7 @@ describe('Monitoring plugin', () => { }, }; - beforeEach(() => { - config = defaultConfig; - (combineLatest as jest.Mock).mockImplementation(() => { - return { - pipe: jest.fn().mockImplementation(() => { - return { - toPromise: jest.fn().mockImplementation(() => { - return [config, 2]; - }), - }; - }), - }; - }); - }); + const initializerContext = coreMock.createPluginInitializerContext(defaultConfig); afterEach(() => { (setupPlugins.alerts.registerType as jest.Mock).mockReset(); @@ -104,14 +64,14 @@ describe('Monitoring plugin', () => { }); it('always create the bulk uploader', async () => { - const plugin = new Plugin(initializerContext as any); + const plugin = new MonitoringPlugin(initializerContext as any); await plugin.setup(coreSetup, setupPlugins as any); expect(coreSetup.status.overall$.subscribe).toHaveBeenCalled(); }); it('should register all alerts', async () => { const alerts = AlertsFactory.getAll(); - const plugin = new Plugin(initializerContext as any); + const plugin = new MonitoringPlugin(initializerContext as any); await plugin.setup(coreSetup as any, setupPlugins as any); expect(setupPlugins.alerts.registerType).toHaveBeenCalledTimes(alerts.length); }); diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 6fd9e7534ac65..654c3de7d81a9 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -6,8 +6,6 @@ */ import Boom from '@hapi/boom'; -import { combineLatest } from 'rxjs'; -import { first, map } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { has, get } from 'lodash'; import { TypeOf } from '@kbn/config-schema'; @@ -21,6 +19,7 @@ import { CoreStart, CustomHttpResponseOptions, ResponseError, + Plugin, } from 'kibana/server'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { @@ -43,6 +42,7 @@ import { AlertsFactory } from './alerts'; import { MonitoringCore, MonitoringLicenseService, + MonitoringPluginSetup, LegacyShimDependencies, IBulkUploader, PluginsSetup, @@ -66,7 +66,8 @@ const wrapError = (error: any): CustomHttpResponseOptions => { }; }; -export class Plugin { +export class MonitoringPlugin + implements Plugin { private readonly initializerContext: PluginInitializerContext; private readonly log: Logger; private readonly getLogger: (...scopes: string[]) => Logger; @@ -82,15 +83,9 @@ export class Plugin { this.getLogger = (...scopes: string[]) => initializerContext.logger.get(LOGGING_TAG, ...scopes); } - async setup(core: CoreSetup, plugins: PluginsSetup) { - const [config, legacyConfig] = await combineLatest([ - this.initializerContext.config - .create>() - .pipe(map((rawConfig) => createConfig(rawConfig))), - this.initializerContext.config.legacy.globalConfig$, - ]) - .pipe(first()) - .toPromise(); + setup(core: CoreSetup, plugins: PluginsSetup) { + const config = createConfig(this.initializerContext.config.get>()); + const legacyConfig = this.initializerContext.config.legacy.get(); const router = core.http.createRouter(); this.legacyShimDependencies = { diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index 0fd30189c5415..bb0b616d37eac 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -94,6 +94,10 @@ export interface IBulkUploader { stop: () => void; } +export interface MonitoringPluginSetup { + getKibanaStats: IBulkUploader['getKibanaStats']; +} + export interface LegacyRequest { logger: Logger; getLogger: (...scopes: string[]) => Logger; diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index 62a330442fc29..a5843d1c4ade1 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -6,7 +6,6 @@ */ import { PluginInitializerContext, Plugin, CoreSetup } from 'src/core/server'; -import { take } from 'rxjs/operators'; import { ObservabilityConfig } from '.'; import { bootstrapAnnotations, @@ -28,10 +27,8 @@ export class ObservabilityPlugin implements Plugin { this.initContext = initContext; } - public async setup(core: CoreSetup, plugins: {}): Promise { - const config$ = this.initContext.config.create(); - - const config = await config$.pipe(take(1)).toPromise(); + public setup(core: CoreSetup, plugins: {}): ObservabilityPluginSetup { + const config = this.initContext.config.get(); let annotationsApiPromise: Promise | undefined; diff --git a/x-pack/plugins/osquery/server/create_config.ts b/x-pack/plugins/osquery/server/create_config.ts index 19859ab05e6a9..d52f299a692cf 100644 --- a/x-pack/plugins/osquery/server/create_config.ts +++ b/x-pack/plugins/osquery/server/create_config.ts @@ -5,14 +5,10 @@ * 2.0. */ -import { map } from 'rxjs/operators'; import { PluginInitializerContext } from 'kibana/server'; -import { Observable } from 'rxjs'; import { ConfigType } from './config'; -export const createConfig$ = ( - context: PluginInitializerContext -): Observable> => { - return context.config.create().pipe(map((config) => config)); +export const createConfig = (context: PluginInitializerContext): Readonly => { + return context.config.get(); }; diff --git a/x-pack/plugins/osquery/server/plugin.ts b/x-pack/plugins/osquery/server/plugin.ts index 77509275431e9..c30f4ac057ec0 100644 --- a/x-pack/plugins/osquery/server/plugin.ts +++ b/x-pack/plugins/osquery/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; import { PluginInitializerContext, CoreSetup, @@ -14,7 +13,7 @@ import { Logger, } from '../../../../src/core/server'; -import { createConfig$ } from './create_config'; +import { createConfig } from './create_config'; import { OsqueryPluginSetup, OsqueryPluginStart, SetupPlugins, StartPlugins } from './types'; import { defineRoutes } from './routes'; import { osquerySearchStrategyProvider } from './search_strategy/osquery'; @@ -26,9 +25,9 @@ export class OsqueryPlugin implements Plugin, plugins: SetupPlugins) { + public setup(core: CoreSetup, plugins: SetupPlugins) { this.logger.debug('osquery: Setup'); - const config = await createConfig$(this.initializerContext).pipe(first()).toPromise(); + const config = createConfig(this.initializerContext); if (!config.enabled) { return {}; diff --git a/x-pack/plugins/painless_lab/server/plugin.ts b/x-pack/plugins/painless_lab/server/plugin.ts index aefb5429a1b13..996adfdc13f64 100644 --- a/x-pack/plugins/painless_lab/server/plugin.ts +++ b/x-pack/plugins/painless_lab/server/plugin.ts @@ -22,7 +22,7 @@ export class PainlessLabServerPlugin implements Plugin { this.license = new License(); } - async setup({ http }: CoreSetup, { licensing }: Dependencies) { + setup({ http }: CoreSetup, { licensing }: Dependencies) { const router = http.createRouter(); this.license.setup( diff --git a/x-pack/plugins/remote_clusters/server/plugin.ts b/x-pack/plugins/remote_clusters/server/plugin.ts index ba78cf411e369..2b8d9afe979e8 100644 --- a/x-pack/plugins/remote_clusters/server/plugin.ts +++ b/x-pack/plugins/remote_clusters/server/plugin.ts @@ -8,8 +8,6 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { PLUGIN } from '../common/constants'; import { Dependencies, LicenseStatus, RouteDependencies } from './types'; @@ -29,17 +27,16 @@ export class RemoteClustersServerPlugin implements Plugin { licenseStatus: LicenseStatus; log: Logger; - config$: Observable; + config: ConfigType; constructor({ logger, config }: PluginInitializerContext) { this.log = logger.get(); - this.config$ = config.create(); + this.config = config.get(); this.licenseStatus = { valid: false }; } - async setup({ http }: CoreSetup, { features, licensing, cloud }: Dependencies) { + setup({ http }: CoreSetup, { features, licensing, cloud }: Dependencies) { const router = http.createRouter(); - const config = await this.config$.pipe(first()).toPromise(); const routeDependencies: RouteDependencies = { router, @@ -89,7 +86,7 @@ export class RemoteClustersServerPlugin }); return { - isUiEnabled: config.ui.enabled, + isUiEnabled: this.config.ui.enabled, }; } diff --git a/x-pack/plugins/searchprofiler/server/plugin.ts b/x-pack/plugins/searchprofiler/server/plugin.ts index cebcbb1a0dc92..ed85febac9a45 100644 --- a/x-pack/plugins/searchprofiler/server/plugin.ts +++ b/x-pack/plugins/searchprofiler/server/plugin.ts @@ -21,7 +21,7 @@ export class SearchProfilerServerPlugin implements Plugin { this.licenseStatus = { valid: false }; } - async setup({ http }: CoreSetup, { licensing }: AppServerPluginDependencies) { + setup({ http }: CoreSetup, { licensing }: AppServerPluginDependencies) { const router = http.createRouter(); profileRoute.register({ router, diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index 6026e42676c57..66b916ac7f70f 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -15,7 +15,7 @@ import type { import { ConfigSchema } from './config'; import { securityConfigDeprecationProvider } from './config_deprecations'; import { - Plugin, + SecurityPlugin, SecurityPluginSetup, SecurityPluginStart, PluginSetupDependencies, @@ -51,4 +51,4 @@ export const plugin: PluginInitializer< RecursiveReadonly, RecursiveReadonly, PluginSetupDependencies -> = (initializerContext: PluginInitializerContext) => new Plugin(initializerContext); +> = (initializerContext: PluginInitializerContext) => new SecurityPlugin(initializerContext); diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 84fc410c72cd0..d57951ecb5b1d 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -8,7 +8,7 @@ import { of } from 'rxjs'; import { ByteSizeValue } from '@kbn/config-schema'; import { ConfigSchema } from './config'; -import { Plugin, PluginSetupDependencies, PluginStartDependencies } from './plugin'; +import { SecurityPlugin, PluginSetupDependencies, PluginStartDependencies } from './plugin'; import { coreMock } from '../../../../src/core/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks'; @@ -16,13 +16,13 @@ import { taskManagerMock } from '../../task_manager/server/mocks'; import { licensingMock } from '../../licensing/server/mocks'; describe('Security Plugin', () => { - let plugin: Plugin; + let plugin: SecurityPlugin; let mockCoreSetup: ReturnType; let mockCoreStart: ReturnType; let mockSetupDependencies: PluginSetupDependencies; let mockStartDependencies: PluginStartDependencies; beforeEach(() => { - plugin = new Plugin( + plugin = new SecurityPlugin( coreMock.createPluginInitializerContext( ConfigSchema.validate({ session: { idleTimeout: 1500 }, diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 3156af7e930bd..cccfa7de6d177 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -8,6 +8,7 @@ import { combineLatest, Subscription } from 'rxjs'; import { map } from 'rxjs/operators'; import { TypeOf } from '@kbn/config-schema'; +import { RecursiveReadonly } from '@kbn/utility-types'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { SecurityOssPluginSetup } from 'src/plugins/security_oss/server'; import { @@ -16,6 +17,7 @@ import { KibanaRequest, Logger, PluginInitializerContext, + Plugin, } from '../../../../src/core/server'; import { SpacesPluginSetup, SpacesPluginStart } from '../../spaces/server'; import { PluginSetupContract as FeaturesSetupContract } from '../../features/server'; @@ -101,7 +103,13 @@ export interface PluginStartDependencies { /** * Represents Security Plugin instance that will be managed by the Kibana plugin system. */ -export class Plugin { +export class SecurityPlugin + implements + Plugin< + RecursiveReadonly, + RecursiveReadonly, + PluginSetupDependencies + > { private readonly logger: Logger; private authorizationSetup?: AuthorizationServiceSetup; private auditSetup?: AuditServiceSetup; diff --git a/x-pack/plugins/security_solution/server/config.ts b/x-pack/plugins/security_solution/server/config.ts index 3791c63f662ae..4658e6774b726 100644 --- a/x-pack/plugins/security_solution/server/config.ts +++ b/x-pack/plugins/security_solution/server/config.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; import { schema, TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext } from '../../../../src/core/server'; import { SIGNALS_INDEX_KEY, DEFAULT_SIGNALS_INDEX } from '../common/constants'; @@ -38,9 +37,7 @@ export const configSchema = schema.object({ validateArtifactDownloads: schema.boolean({ defaultValue: true }), }); -export const createConfig$ = (context: PluginInitializerContext) => - context.config.create>(); +export const createConfig = (context: PluginInitializerContext) => + context.config.get>(); -export type ConfigType = ReturnType extends Observable - ? T - : ReturnType; +export type ConfigType = TypeOf; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 0d5d83582b42b..8c35fd2ce8f8b 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -6,7 +6,6 @@ */ import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import LRU from 'lru-cache'; @@ -47,7 +46,7 @@ import { isNotificationAlertExecutor } from './lib/detection_engine/notification import { ManifestTask } from './endpoint/lib/artifacts'; import { initSavedObjects, savedObjectTypes } from './saved_objects'; import { AppClientFactory } from './client'; -import { createConfig$, ConfigType } from './config'; +import { createConfig, ConfigType } from './config'; import { initUiSettings } from './ui_settings'; import { APP_ID, @@ -119,8 +118,7 @@ const securitySubPlugins = [ export class Plugin implements IPlugin { private readonly logger: Logger; - private readonly config$: Observable; - private config?: ConfigType; + private readonly config: ConfigType; private context: PluginInitializerContext; private appClientFactory: AppClientFactory; private setupPlugins?: SetupPlugins; @@ -137,7 +135,7 @@ export class Plugin implements IPlugin({ max: 3, maxAge: 1000 * 60 * 5 }); @@ -146,13 +144,12 @@ export class Plugin implements IPlugin, plugins: SetupPlugins) { + public setup(core: CoreSetup, plugins: SetupPlugins) { this.logger.debug('plugin setup'); this.setupPlugins = plugins; - const config = await this.config$.pipe(first()).toPromise(); - this.config = config; - const globalConfig = await this.context.config.legacy.globalConfig$.pipe(first()).toPromise(); + const config = this.config; + const globalConfig = this.context.config.legacy.get(); initSavedObjects(core.savedObjects); initUiSettings(core.uiSettings); diff --git a/x-pack/plugins/snapshot_restore/server/plugin.ts b/x-pack/plugins/snapshot_restore/server/plugin.ts index 9d4614cf60294..c93b5dbc4c36d 100644 --- a/x-pack/plugins/snapshot_restore/server/plugin.ts +++ b/x-pack/plugins/snapshot_restore/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { CoreSetup, @@ -43,14 +42,11 @@ export class SnapshotRestoreServerPlugin implements Plugin this.license = new License(); } - public async setup( + public setup( { http, getStartServices }: CoreSetup, { licensing, features, security, cloud }: Dependencies - ): Promise { - const pluginConfig = await this.context.config - .create() - .pipe(first()) - .toPromise(); + ): void { + const pluginConfig = this.context.config.get(); if (!pluginConfig.enabled) { return; diff --git a/x-pack/plugins/spaces/server/index.ts b/x-pack/plugins/spaces/server/index.ts index 9d2a075dc35f9..fb6c00c2f6f48 100644 --- a/x-pack/plugins/spaces/server/index.ts +++ b/x-pack/plugins/spaces/server/index.ts @@ -7,7 +7,7 @@ import type { PluginConfigDescriptor, PluginInitializerContext } from '../../../../src/core/server'; import { ConfigSchema, spacesConfigDeprecationProvider } from './config'; -import { Plugin } from './plugin'; +import { SpacesPlugin } from './plugin'; // These exports are part of public Spaces plugin contract, any change in signature of exported // functions or removal of exports should be considered as a breaking change. Ideally we should @@ -32,4 +32,4 @@ export const config: PluginConfigDescriptor = { deprecations: spacesConfigDeprecationProvider, }; export const plugin = (initializerContext: PluginInitializerContext) => - new Plugin(initializerContext); + new SpacesPlugin(initializerContext); diff --git a/x-pack/plugins/spaces/server/plugin.test.ts b/x-pack/plugins/spaces/server/plugin.test.ts index d576858c98e36..d1bf4d51700ba 100644 --- a/x-pack/plugins/spaces/server/plugin.test.ts +++ b/x-pack/plugins/spaces/server/plugin.test.ts @@ -9,7 +9,7 @@ import { CoreSetup } from 'src/core/server'; import { coreMock } from 'src/core/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks'; import { licensingMock } from '../../licensing/server/mocks'; -import { Plugin, PluginsStart } from './plugin'; +import { SpacesPlugin, PluginsStart } from './plugin'; import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/server/mocks'; describe('Spaces Plugin', () => { @@ -20,7 +20,7 @@ describe('Spaces Plugin', () => { const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); - const plugin = new Plugin(initializerContext); + const plugin = new SpacesPlugin(initializerContext); const spacesSetup = plugin.setup(core, { features, licensing }); expect(spacesSetup).toMatchInlineSnapshot(` Object { @@ -43,7 +43,7 @@ describe('Spaces Plugin', () => { const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); - const plugin = new Plugin(initializerContext); + const plugin = new SpacesPlugin(initializerContext); plugin.setup(core, { features, licensing }); @@ -59,7 +59,7 @@ describe('Spaces Plugin', () => { const usageCollection = usageCollectionPluginMock.createSetupContract(); - const plugin = new Plugin(initializerContext); + const plugin = new SpacesPlugin(initializerContext); plugin.setup(core, { features, licensing, usageCollection }); @@ -72,7 +72,7 @@ describe('Spaces Plugin', () => { const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); - const plugin = new Plugin(initializerContext); + const plugin = new SpacesPlugin(initializerContext); plugin.setup(core, { features, licensing }); @@ -99,7 +99,7 @@ describe('Spaces Plugin', () => { const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); - const plugin = new Plugin(initializerContext); + const plugin = new SpacesPlugin(initializerContext); plugin.setup(coreSetup, { features, licensing }); const coreStart = coreMock.createStart(); diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index d9d32dd68c95d..4b26b1016d530 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -13,6 +13,7 @@ import { CoreStart, Logger, PluginInitializerContext, + Plugin, } from '../../../../src/core/server'; import { PluginSetupContract as FeaturesPluginSetup, @@ -62,7 +63,8 @@ export interface SpacesPluginStart { spacesService: SpacesServiceStart; } -export class Plugin { +export class SpacesPlugin + implements Plugin { private readonly config$: Observable; private readonly kibanaIndexConfig$: Observable<{ kibana: { index: string } }>; diff --git a/x-pack/plugins/stack_alerts/server/plugin.ts b/x-pack/plugins/stack_alerts/server/plugin.ts index 261d3d51aeb80..1343c46ecdd72 100644 --- a/x-pack/plugins/stack_alerts/server/plugin.ts +++ b/x-pack/plugins/stack_alerts/server/plugin.ts @@ -19,10 +19,7 @@ export class AlertingBuiltinsPlugin this.logger = ctx.logger.get(); } - public async setup( - core: CoreSetup, - { alerts, features }: StackAlertsDeps - ): Promise { + public setup(core: CoreSetup, { alerts, features }: StackAlertsDeps) { features.registerKibanaFeature(BUILT_IN_ALERTS_FEATURE); registerBuiltInAlertTypes({ @@ -34,6 +31,6 @@ export class AlertingBuiltinsPlugin }); } - public async start(): Promise {} - public async stop(): Promise {} + public start() {} + public stop() {} } diff --git a/x-pack/plugins/task_manager/server/plugin.test.ts b/x-pack/plugins/task_manager/server/plugin.test.ts index 77031d4764968..0a879ce92cba6 100644 --- a/x-pack/plugins/task_manager/server/plugin.test.ts +++ b/x-pack/plugins/task_manager/server/plugin.test.ts @@ -39,7 +39,7 @@ describe('TaskManagerPlugin', () => { pluginInitializerContext.env.instanceUuid = ''; const taskManagerPlugin = new TaskManagerPlugin(pluginInitializerContext); - expect(taskManagerPlugin.setup(coreMock.createSetup())).rejects.toEqual( + expect(() => taskManagerPlugin.setup(coreMock.createSetup())).toThrow( new Error(`TaskManager is unable to start as Kibana has no valid UUID assigned to it.`) ); }); diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index 62b8b75a38d73..149d111b08f02 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -6,7 +6,7 @@ */ import { combineLatest, Observable, Subject } from 'rxjs'; -import { first, map, distinctUntilChanged } from 'rxjs/operators'; +import { map, distinctUntilChanged } from 'rxjs/operators'; import { PluginInitializerContext, Plugin, @@ -46,7 +46,7 @@ export class TaskManagerPlugin implements Plugin { private taskPollingLifecycle?: TaskPollingLifecycle; private taskManagerId?: string; - private config?: TaskManagerConfig; + private config: TaskManagerConfig; private logger: Logger; private definitions: TaskTypeDictionary; private middleware: Middleware = createInitialMiddleware(); @@ -56,15 +56,11 @@ export class TaskManagerPlugin constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; this.logger = initContext.logger.get(); + this.config = initContext.config.get(); this.definitions = new TaskTypeDictionary(this.logger); } - public async setup(core: CoreSetup): Promise { - this.config = await this.initContext.config - .create() - .pipe(first()) - .toPromise(); - + public setup(core: CoreSetup): TaskManagerSetupContract { this.elasticsearchAndSOAvailability$ = getElasticsearchAndSOAvailability(core.status.core$); setupSavedObjects(core.savedObjects, this.config); diff --git a/x-pack/plugins/triggers_actions_ui/server/plugin.ts b/x-pack/plugins/triggers_actions_ui/server/plugin.ts index f7c7e48d93d08..3933751105cb4 100644 --- a/x-pack/plugins/triggers_actions_ui/server/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/server/plugin.ts @@ -30,7 +30,7 @@ export class TriggersActionsPlugin implements Plugin this.data = getService(); } - public async setup(core: CoreSetup, plugins: PluginsSetup): Promise { + public setup(core: CoreSetup, plugins: PluginsSetup): void { const router = core.http.createRouter(); registerDataService({ logger: this.logger, @@ -42,7 +42,7 @@ export class TriggersActionsPlugin implements Plugin createHealthRoute(this.logger, router, BASE_ROUTE, plugins.alerts !== undefined); } - public async start(): Promise { + public start(): PluginStartContract { return { data: this.data, }; diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index d7c0a465dd3e0..8bbbecf8108fe 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -50,10 +50,7 @@ export class UptimePlugin implements Plugin { constructor(_context: PluginInitializerContext) {} - public async setup( - core: CoreSetup, - plugins: ClientPluginsSetup - ): Promise { + public setup(core: CoreSetup, plugins: ClientPluginsSetup): void { if (plugins.home) { plugins.home.featureCatalogue.register({ id: PLUGIN.ID, diff --git a/x-pack/plugins/watcher/server/plugin.ts b/x-pack/plugins/watcher/server/plugin.ts index 220a814835e75..ceade131fc5af 100644 --- a/x-pack/plugins/watcher/server/plugin.ts +++ b/x-pack/plugins/watcher/server/plugin.ts @@ -47,7 +47,7 @@ export class WatcherServerPlugin implements Plugin { this.log = ctx.logger.get(); } - async setup({ http, getStartServices }: CoreSetup, { licensing, features }: Dependencies) { + setup({ http, getStartServices }: CoreSetup, { licensing, features }: Dependencies) { const router = http.createRouter(); const routeDependencies: RouteDependencies = { router, diff --git a/x-pack/plugins/xpack_legacy/server/plugin.ts b/x-pack/plugins/xpack_legacy/server/plugin.ts index 9bd42171c75d5..ffef7117bbbd8 100644 --- a/x-pack/plugins/xpack_legacy/server/plugin.ts +++ b/x-pack/plugins/xpack_legacy/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; - import { CoreStart, CoreSetup, @@ -23,11 +21,9 @@ interface SetupPluginDeps { export class XpackLegacyPlugin implements Plugin { constructor(private readonly initContext: PluginInitializerContext) {} - public async setup(core: CoreSetup, { usageCollection }: SetupPluginDeps) { + public setup(core: CoreSetup, { usageCollection }: SetupPluginDeps) { const router = core.http.createRouter(); - const globalConfig = await this.initContext.config.legacy.globalConfig$ - .pipe(first()) - .toPromise(); + const globalConfig = this.initContext.config.legacy.get(); const serverInfo = core.http.getServerInfo(); registerSettingsRoute({ From d152723bb1a1ea788e489dc29970fca23ae46702 Mon Sep 17 00:00:00 2001 From: Daniil Date: Mon, 8 Feb 2021 14:13:20 +0200 Subject: [PATCH 37/44] [Data Table] Add unit tests (#90173) * Move formatting columns into response handler * Use shared csv export * Cleanup files * Fix type * Fix translation * Filter out non-dimension values * Add unit tests for tableVisResponseHandler * Add unit tests for createFormattedTable * Add unit tests for addPercentageColumn * Add unit tests for usePagination * Add unit tests for useUiState * Add unit tests for table visualization * Add unit tests for TableVisBasic * Add unit tests for cell * Update license Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../table_vis_basic.test.tsx.snap | 115 +++++++++ .../table_vis_cell.test.tsx.snap | 13 ++ .../components/table_vis_basic.test.tsx | 130 +++++++++++ .../public/components/table_vis_cell.test.tsx | 36 +++ .../components/table_visualization.test.tsx | 69 ++++++ .../utils/add_percentage_column.test.ts | 79 +++++++ .../utils/create_formatted_table.test.ts | 218 ++++++++++++++++++ .../utils/table_vis_response_handler.test.ts | 171 ++++++++++++++ .../utils/table_vis_response_handler.ts | 5 +- .../public/utils/use/use_pagination.test.ts | 119 ++++++++++ .../public/utils/use/use_ui_state.test.ts | 163 +++++++++++++ 11 files changed, 1115 insertions(+), 3 deletions(-) create mode 100644 src/plugins/vis_type_table/public/components/__snapshots__/table_vis_basic.test.tsx.snap create mode 100644 src/plugins/vis_type_table/public/components/__snapshots__/table_vis_cell.test.tsx.snap create mode 100644 src/plugins/vis_type_table/public/components/table_vis_basic.test.tsx create mode 100644 src/plugins/vis_type_table/public/components/table_vis_cell.test.tsx create mode 100644 src/plugins/vis_type_table/public/components/table_visualization.test.tsx create mode 100644 src/plugins/vis_type_table/public/utils/add_percentage_column.test.ts create mode 100644 src/plugins/vis_type_table/public/utils/create_formatted_table.test.ts create mode 100644 src/plugins/vis_type_table/public/utils/table_vis_response_handler.test.ts create mode 100644 src/plugins/vis_type_table/public/utils/use/use_pagination.test.ts create mode 100644 src/plugins/vis_type_table/public/utils/use/use_ui_state.test.ts diff --git a/src/plugins/vis_type_table/public/components/__snapshots__/table_vis_basic.test.tsx.snap b/src/plugins/vis_type_table/public/components/__snapshots__/table_vis_basic.test.tsx.snap new file mode 100644 index 0000000000000..85cf9422630d6 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/__snapshots__/table_vis_basic.test.tsx.snap @@ -0,0 +1,115 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TableVisBasic should init data grid 1`] = ` + + + +`; + +exports[`TableVisBasic should init data grid with title provided - for split mode 1`] = ` + + +

    + My data table +

    +
    + +
    +`; + +exports[`TableVisBasic should render the toolbar 1`] = ` + + , + "showColumnSelector": false, + "showFullScreenSelector": false, + "showSortSelector": false, + "showStyleSelector": false, + } + } + /> + +`; diff --git a/src/plugins/vis_type_table/public/components/__snapshots__/table_vis_cell.test.tsx.snap b/src/plugins/vis_type_table/public/components/__snapshots__/table_vis_cell.test.tsx.snap new file mode 100644 index 0000000000000..b380b85f7f356 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/__snapshots__/table_vis_cell.test.tsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`table vis cell should return a cell component with data in scope 1`] = ` +
    +`; diff --git a/src/plugins/vis_type_table/public/components/table_vis_basic.test.tsx b/src/plugins/vis_type_table/public/components/table_vis_basic.test.tsx new file mode 100644 index 0000000000000..0fb74a41b5df0 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_basic.test.tsx @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { TableVisBasic } from './table_vis_basic'; +import { FormattedColumn, TableVisConfig, TableVisUiState } from '../types'; +import { DatatableColumn } from 'src/plugins/expressions'; +import { createTableVisCell } from './table_vis_cell'; +import { createGridColumns } from './table_vis_columns'; + +jest.mock('./table_vis_columns', () => ({ + createGridColumns: jest.fn(() => []), +})); +jest.mock('./table_vis_cell', () => ({ + createTableVisCell: jest.fn(() => () => {}), +})); + +describe('TableVisBasic', () => { + const props = { + fireEvent: jest.fn(), + table: { + columns: [], + rows: [], + formattedColumns: { + test: { + formattedTotal: 100, + } as FormattedColumn, + }, + }, + visConfig: {} as TableVisConfig, + uiStateProps: { + sort: { + columnIndex: null, + direction: null, + }, + columnsWidth: [], + setColumnsWidth: jest.fn(), + setSort: jest.fn(), + }, + }; + + it('should init data grid', () => { + const comp = shallow(); + expect(comp).toMatchSnapshot(); + }); + + it('should init data grid with title provided - for split mode', () => { + const title = 'My data table'; + const comp = shallow(); + expect(comp).toMatchSnapshot(); + }); + + it('should render the toolbar', () => { + const comp = shallow( + + ); + expect(comp).toMatchSnapshot(); + }); + + it('should sort rows by column and pass the sorted rows for consumers', () => { + const uiStateProps = { + ...props.uiStateProps, + sort: { + columnIndex: 1, + direction: 'desc', + } as TableVisUiState['sort'], + }; + const table = { + columns: [{ id: 'first' }, { id: 'second' }] as DatatableColumn[], + rows: [ + { first: 1, second: 2 }, + { first: 3, second: 4 }, + { first: 5, second: 6 }, + ], + formattedColumns: {}, + }; + const sortedRows = [ + { first: 5, second: 6 }, + { first: 3, second: 4 }, + { first: 1, second: 2 }, + ]; + const comp = shallow( + + ); + expect(createTableVisCell).toHaveBeenCalledWith(sortedRows, table.formattedColumns); + expect(createGridColumns).toHaveBeenCalledWith( + table.columns, + sortedRows, + table.formattedColumns, + uiStateProps.columnsWidth, + props.fireEvent + ); + + const { onSort } = comp.find('EuiDataGrid').prop('sorting'); + // sort the first column + onSort([{ id: 'first', direction: 'asc' }]); + expect(uiStateProps.setSort).toHaveBeenCalledWith({ columnIndex: 0, direction: 'asc' }); + // sort the second column - should erase the first column sorting since there is only one level sorting available + onSort([ + { id: 'first', direction: 'asc' }, + { id: 'second', direction: 'desc' }, + ]); + expect(uiStateProps.setSort).toHaveBeenCalledWith({ columnIndex: 1, direction: 'desc' }); + }); + + it('should pass renderFooterCellValue for the total row', () => { + const comp = shallow( + + ); + const renderFooterCellValue: (props: any) => void = comp + .find('EuiDataGrid') + .prop('renderFooterCellValue'); + expect(renderFooterCellValue).toEqual(expect.any(Function)); + expect(renderFooterCellValue({ columnId: 'test' })).toEqual(100); + }); +}); diff --git a/src/plugins/vis_type_table/public/components/table_vis_cell.test.tsx b/src/plugins/vis_type_table/public/components/table_vis_cell.test.tsx new file mode 100644 index 0000000000000..322ceacbe002e --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_cell.test.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiDataGridCellValueElementProps } from '@elastic/eui'; +import { createTableVisCell } from './table_vis_cell'; +import { FormattedColumns } from '../types'; + +describe('table vis cell', () => { + it('should return a cell component with data in scope', () => { + const rows = [{ first: 1, second: 2 }]; + const formattedColumns = ({ + second: { + formatter: { + convert: jest.fn(), + }, + }, + } as unknown) as FormattedColumns; + const Cell = createTableVisCell(rows, formattedColumns); + const cellProps = { + rowIndex: 0, + columnId: 'second', + } as EuiDataGridCellValueElementProps; + + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + expect(formattedColumns.second.formatter.convert).toHaveBeenLastCalledWith(2, 'html'); + }); +}); diff --git a/src/plugins/vis_type_table/public/components/table_visualization.test.tsx b/src/plugins/vis_type_table/public/components/table_visualization.test.tsx new file mode 100644 index 0000000000000..3d169531f5757 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_visualization.test.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +jest.mock('../utils', () => ({ + useUiState: jest.fn(() => 'uiState'), +})); + +import React from 'react'; +import { shallow } from 'enzyme'; +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { coreMock } from '../../../../core/public/mocks'; +import { TableVisConfig, TableVisData } from '../types'; +import TableVisualizationComponent from './table_visualization'; +import { useUiState } from '../utils'; + +describe('TableVisualizationComponent', () => { + const coreStartMock = coreMock.createStart(); + const handlers = ({ + done: jest.fn(), + uiState: 'uiState', + event: 'event', + } as unknown) as IInterpreterRenderHandlers; + const visData: TableVisData = { + table: { + columns: [], + rows: [], + formattedColumns: {}, + }, + tables: [], + }; + const visConfig = ({} as unknown) as TableVisConfig; + + it('should render the basic table', () => { + const comp = shallow( + + ); + expect(useUiState).toHaveBeenLastCalledWith(handlers.uiState); + expect(comp.find('.tbvChart__splitColumns').exists()).toBeFalsy(); + expect(comp.find('.tbvChart__split').exists()).toBeTruthy(); + }); + + it('should render split table', () => { + const comp = shallow( + + ); + expect(useUiState).toHaveBeenLastCalledWith(handlers.uiState); + expect(comp.find('.tbvChart__splitColumns').exists()).toBeTruthy(); + expect(comp.find('.tbvChart__split').exists()).toBeFalsy(); + expect(comp.find('[data-test-subj="tbvChart"]').children().prop('tables')).toEqual([]); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/add_percentage_column.test.ts b/src/plugins/vis_type_table/public/utils/add_percentage_column.test.ts new file mode 100644 index 0000000000000..0280637acc099 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/add_percentage_column.test.ts @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +jest.mock('../services', () => ({ + getFormatService: jest.fn(() => ({ + deserialize: jest.fn(() => 'formatter'), + })), +})); + +import { FieldFormat } from 'src/plugins/data/public'; +import { TableContext } from '../types'; +import { addPercentageColumn } from './add_percentage_column'; + +describe('', () => { + const table: TableContext = { + columns: [ + { id: 'col-0-1', name: 'Count', meta: { type: 'number' } }, + { id: 'col-1-5', name: 'category.keyword: Descending', meta: { type: 'string' } }, + { id: 'col-1-2', name: 'Gender', meta: { type: 'string' } }, + ], + rows: [ + { 'col-0-1': 1, 'col-1-5': "Women's Clothing", 'col-1-2': 'Men' }, + { 'col-0-1': 6, 'col-1-5': "Women's Clothing", 'col-1-2': 'Men' }, + ], + formattedColumns: { + 'col-0-1': { + sumTotal: 7, + title: 'Count', + filterable: false, + formatter: {} as FieldFormat, + }, + }, + }; + + it('should dnot add percentage column if it was not found', () => { + const output = addPercentageColumn(table, 'Extra'); + expect(output).toBe(table); + }); + + it('should add a brand new percentage column into table based on data', () => { + const output = addPercentageColumn(table, 'Count'); + const expectedColumns = [ + table.columns[0], + { + id: 'col-0-1-percents', + meta: { + params: { + id: 'percent', + }, + type: 'number', + }, + name: 'Count percentages', + }, + table.columns[1], + table.columns[2], + ]; + const expectedRows = [ + { ...table.rows[0], 'col-0-1-percents': 0.14285714285714285 }, + { ...table.rows[1], 'col-0-1-percents': 0.8571428571428571 }, + ]; + expect(output).toEqual({ + columns: expectedColumns, + rows: expectedRows, + formattedColumns: { + ...table.formattedColumns, + 'col-0-1-percents': { + filterable: false, + formatter: 'formatter', + title: 'Count percentages', + }, + }, + }); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/create_formatted_table.test.ts b/src/plugins/vis_type_table/public/utils/create_formatted_table.test.ts new file mode 100644 index 0000000000000..0a9c7320d4359 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/create_formatted_table.test.ts @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const mockDeserialize = jest.fn(() => ({})); + +jest.mock('../services', () => ({ + getFormatService: jest.fn(() => ({ + deserialize: mockDeserialize, + })), +})); + +import { Datatable } from 'src/plugins/expressions'; +import { AggTypes } from '../../common'; +import { TableVisConfig } from '../types'; +import { createFormattedTable } from './create_formatted_table'; + +const visConfig: TableVisConfig = { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + showToolbar: false, + showTotal: false, + totalFunc: AggTypes.SUM, + percentageCol: '', + title: 'My data table', + dimensions: { + buckets: [ + { + accessor: 1, + aggType: 'terms', + format: { id: 'string' }, + label: 'category_keyword: Descending', + params: {}, + }, + ], + metrics: [ + { accessor: 0, aggType: 'count', format: { id: 'number' }, label: 'Count', params: {} }, + ], + }, +}; + +describe('createFormattedTable', () => { + const table: Datatable = { + columns: [ + { id: 'col-0-1', name: 'Count', meta: { type: 'number' } }, + { id: 'col-1-5', name: 'category.keyword: Descending', meta: { type: 'string' } }, + { id: 'col-1-2', name: 'Gender', meta: { type: 'string' } }, + ], + rows: [ + { 'col-0-1': 1, 'col-1-5': "Women's Clothing", 'col-1-2': 'Men' }, + { 'col-0-1': 6, 'col-1-5': "Women's Clothing", 'col-1-2': 'Men' }, + ], + type: 'datatable', + }; + + it('should create formatted columns from data response and flter out non-dimension columns', () => { + const output = createFormattedTable(table, visConfig); + + // column to split is filtered out of real data representing + expect(output.columns).toEqual([table.columns[0], table.columns[1]]); + expect(output.rows).toEqual(table.rows); + expect(output.formattedColumns).toEqual({ + 'col-0-1': { + filterable: false, + formatter: {}, + title: 'Count', + }, + 'col-1-5': { + filterable: true, + formatter: {}, + title: 'category.keyword: Descending', + }, + }); + }); + + it('should add total sum to numeric columns', () => { + mockDeserialize.mockImplementationOnce(() => ({ + allowsNumericalAggregations: true, + convert: jest.fn((number) => number), + })); + const output = createFormattedTable(table, visConfig); + + expect(output.formattedColumns).toEqual({ + 'col-0-1': { + filterable: false, + formatter: { + allowsNumericalAggregations: true, + convert: expect.any(Function), + }, + title: 'Count', + sumTotal: 7, + total: 7, + formattedTotal: 7, + }, + 'col-1-5': { + filterable: true, + formatter: {}, + title: 'category.keyword: Descending', + }, + }); + }); + + it('should add total average to numeric columns', () => { + mockDeserialize.mockImplementationOnce(() => ({ + allowsNumericalAggregations: true, + convert: jest.fn((number) => number), + })); + const output = createFormattedTable(table, { ...visConfig, totalFunc: AggTypes.AVG }); + + expect(output.formattedColumns).toEqual({ + 'col-0-1': { + filterable: false, + formatter: { + allowsNumericalAggregations: true, + convert: expect.any(Function), + }, + title: 'Count', + sumTotal: 7, + total: 3.5, + formattedTotal: 3.5, + }, + 'col-1-5': { + filterable: true, + formatter: {}, + title: 'category.keyword: Descending', + }, + }); + }); + + it('should find min value as total', () => { + mockDeserialize.mockImplementationOnce(() => ({ + allowsNumericalAggregations: true, + convert: jest.fn((number) => number), + })); + const output = createFormattedTable(table, { ...visConfig, totalFunc: AggTypes.MIN }); + + expect(output.formattedColumns).toEqual({ + 'col-0-1': { + filterable: false, + formatter: { + allowsNumericalAggregations: true, + convert: expect.any(Function), + }, + title: 'Count', + sumTotal: 7, + total: 1, + formattedTotal: 1, + }, + 'col-1-5': { + filterable: true, + formatter: {}, + title: 'category.keyword: Descending', + }, + }); + }); + + it('should find max value as total', () => { + mockDeserialize.mockImplementationOnce(() => ({ + allowsNumericalAggregations: true, + convert: jest.fn((number) => number), + })); + const output = createFormattedTable(table, { ...visConfig, totalFunc: AggTypes.MAX }); + + expect(output.formattedColumns).toEqual({ + 'col-0-1': { + filterable: false, + formatter: { + allowsNumericalAggregations: true, + convert: expect.any(Function), + }, + title: 'Count', + sumTotal: 7, + total: 6, + formattedTotal: 6, + }, + 'col-1-5': { + filterable: true, + formatter: {}, + title: 'category.keyword: Descending', + }, + }); + }); + + it('should add rows count as total', () => { + mockDeserialize.mockImplementationOnce(() => ({ + allowsNumericalAggregations: true, + convert: jest.fn((number) => number), + })); + const output = createFormattedTable(table, { ...visConfig, totalFunc: AggTypes.COUNT }); + + expect(output.formattedColumns).toEqual({ + 'col-0-1': { + filterable: false, + formatter: { + allowsNumericalAggregations: true, + convert: expect.any(Function), + }, + title: 'Count', + sumTotal: 7, + total: 2, + formattedTotal: 2, + }, + 'col-1-5': { + filterable: true, + formattedTotal: 2, + formatter: {}, + sumTotal: "0Women's ClothingWomen's Clothing", + title: 'category.keyword: Descending', + total: 2, + }, + }); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/table_vis_response_handler.test.ts b/src/plugins/vis_type_table/public/utils/table_vis_response_handler.test.ts new file mode 100644 index 0000000000000..8adc535e802f0 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/table_vis_response_handler.test.ts @@ -0,0 +1,171 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const mockConverter = jest.fn((name) => `By ${name}`); + +jest.mock('../services', () => ({ + getFormatService: jest.fn(() => ({ + deserialize: jest.fn(() => ({ + convert: mockConverter, + })), + })), +})); + +jest.mock('./create_formatted_table', () => ({ + createFormattedTable: jest.fn((data) => ({ + ...data, + formattedColumns: {}, + })), +})); + +jest.mock('./add_percentage_column', () => ({ + addPercentageColumn: jest.fn((data, column) => ({ + ...data, + percentage: `${column} with percentage`, + })), +})); + +import { Datatable } from 'src/plugins/expressions'; +import { SchemaConfig } from 'src/plugins/visualizations/public'; +import { AggTypes } from '../../common'; +import { TableGroup, TableVisConfig } from '../types'; +import { addPercentageColumn } from './add_percentage_column'; +import { createFormattedTable } from './create_formatted_table'; +import { tableVisResponseHandler } from './table_vis_response_handler'; + +const visConfig: TableVisConfig = { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + showToolbar: false, + showTotal: false, + totalFunc: AggTypes.AVG, + percentageCol: '', + title: 'My data table', + dimensions: { + buckets: [], + metrics: [], + }, +}; + +describe('tableVisResponseHandler', () => { + describe('basic table', () => { + const input: Datatable = { + columns: [], + rows: [], + type: 'datatable', + }; + + it('should create formatted table for basic usage', () => { + const output = tableVisResponseHandler(input, visConfig); + + expect(output.direction).toBeUndefined(); + expect(output.tables.length).toEqual(0); + expect(addPercentageColumn).not.toHaveBeenCalled(); + expect(createFormattedTable).toHaveBeenCalledWith(input, visConfig); + expect(output.table).toEqual({ + ...input, + formattedColumns: {}, + }); + }); + + it('should add a percentage column if it is set', () => { + const output = tableVisResponseHandler(input, { ...visConfig, percentageCol: 'Count' }); + expect(output.table).toEqual({ + ...input, + formattedColumns: {}, + percentage: 'Count with percentage', + }); + }); + }); + + describe('split table', () => { + const input: Datatable = { + columns: [ + { id: 'col-0-1', name: 'Count', meta: { type: 'number' } }, + { id: 'col-1-2', name: 'Gender', meta: { type: 'string' } }, + ], + rows: [ + { 'col-0-1': 1, 'col-1-2': 'Men' }, + { 'col-0-1': 3, 'col-1-2': 'Women' }, + { 'col-0-1': 6, 'col-1-2': 'Men' }, + ], + type: 'datatable', + }; + const split: SchemaConfig[] = [ + { + accessor: 1, + label: 'Split', + format: {}, + params: {}, + aggType: 'terms', + }, + ]; + const expectedOutput: TableGroup[] = [ + { + title: 'By Men: Gender', + table: { + columns: input.columns, + rows: [input.rows[0], input.rows[2]], + formattedColumns: {}, + }, + }, + { + title: 'By Women: Gender', + table: { + columns: input.columns, + rows: [input.rows[1]], + formattedColumns: {}, + }, + }, + ]; + + it('should split data by row', () => { + const output = tableVisResponseHandler(input, { + ...visConfig, + dimensions: { ...visConfig.dimensions, splitRow: split }, + }); + + expect(output.direction).toEqual('row'); + expect(output.table).toBeUndefined(); + expect(output.tables).toEqual(expectedOutput); + }); + + it('should split data by column', () => { + const output = tableVisResponseHandler(input, { + ...visConfig, + dimensions: { ...visConfig.dimensions, splitColumn: split }, + }); + + expect(output.direction).toEqual('column'); + expect(output.table).toBeUndefined(); + expect(output.tables).toEqual(expectedOutput); + }); + + it('should add percentage columns to each table', () => { + const output = tableVisResponseHandler(input, { + ...visConfig, + percentageCol: 'Count', + dimensions: { ...visConfig.dimensions, splitColumn: split }, + }); + + expect(output.direction).toEqual('column'); + expect(output.table).toBeUndefined(); + expect(output.tables).toEqual([ + { + ...expectedOutput[0], + table: { ...expectedOutput[0].table, percentage: 'Count with percentage' }, + }, + { + ...expectedOutput[1], + table: { ...expectedOutput[1].table, percentage: 'Count with percentage' }, + }, + ]); + }); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/table_vis_response_handler.ts b/src/plugins/vis_type_table/public/utils/table_vis_response_handler.ts index e0919671135ea..69521c20cddfe 100644 --- a/src/plugins/vis_type_table/public/utils/table_vis_response_handler.ts +++ b/src/plugins/vis_type_table/public/utils/table_vis_response_handler.ts @@ -27,7 +27,6 @@ export function tableVisResponseHandler(input: Datatable, visConfig: TableVisCon const splitColumnIndex = split[0].accessor; const splitColumnFormatter = getFormatService().deserialize(split[0].format); const splitColumn = input.columns[splitColumnIndex]; - const columns = input.columns.filter((c, idx) => idx !== splitColumnIndex); const splitMap: { [key: string]: number } = {}; let splitIndex = 0; @@ -39,7 +38,7 @@ export function tableVisResponseHandler(input: Datatable, visConfig: TableVisCon const tableGroup: TableGroup = { title: `${splitColumnFormatter.convert(splitValue)}: ${splitColumn.name}`, table: { - columns, + columns: input.columns, rows: [], formattedColumns: {}, }, @@ -53,7 +52,7 @@ export function tableVisResponseHandler(input: Datatable, visConfig: TableVisCon }); tables.forEach((tg) => { - tg.table = createFormattedTable({ ...tg.table, columns: input.columns }, visConfig); + tg.table = createFormattedTable(tg.table, visConfig); if (visConfig.percentageCol) { tg.table = addPercentageColumn(tg.table, visConfig.percentageCol); diff --git a/src/plugins/vis_type_table/public/utils/use/use_pagination.test.ts b/src/plugins/vis_type_table/public/utils/use/use_pagination.test.ts new file mode 100644 index 0000000000000..3d0b58aa6c8a3 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/use/use_pagination.test.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { AggTypes } from '../../../common'; +import { usePagination } from './use_pagination'; + +describe('usePagination', () => { + const visParams = { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + showToolbar: false, + showTotal: false, + totalFunc: AggTypes.SUM, + percentageCol: '', + title: 'My data table', + }; + + it('should set up pagination on init', () => { + const { result } = renderHook(() => usePagination(visParams, 15)); + + expect(result.current).toEqual({ + pageIndex: 0, + pageSize: 10, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); + + it('should skip setting the pagination if perPage is not set', () => { + const { result } = renderHook(() => usePagination({ ...visParams, perPage: '' }, 15)); + + expect(result.current).toBeUndefined(); + }); + + it('should change the page via callback', () => { + const { result } = renderHook(() => usePagination(visParams, 15)); + + act(() => { + // change the page to the next one + result.current?.onChangePage(1); + }); + + expect(result.current).toEqual({ + pageIndex: 1, + pageSize: 10, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); + + it('should change items per page via callback', () => { + const { result } = renderHook(() => usePagination(visParams, 15)); + + act(() => { + // change the page to the next one + result.current?.onChangeItemsPerPage(20); + }); + + expect(result.current).toEqual({ + pageIndex: 0, + pageSize: 20, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); + + it('should change the page when props were changed', () => { + const { result, rerender } = renderHook( + (props) => usePagination(props.visParams, props.rowCount), + { + initialProps: { + visParams, + rowCount: 15, + }, + } + ); + const updatedParams = { ...visParams, perPage: 5 }; + + // change items per page count + rerender({ visParams: updatedParams, rowCount: 15 }); + + expect(result.current).toEqual({ + pageIndex: 0, + pageSize: 5, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + + act(() => { + // change the page to the last one - 3 + result.current?.onChangePage(3); + }); + + expect(result.current).toEqual({ + pageIndex: 3, + pageSize: 5, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + + // decrease the rows count + rerender({ visParams: updatedParams, rowCount: 10 }); + + // should switch to the last available page + expect(result.current).toEqual({ + pageIndex: 1, + pageSize: 5, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/use/use_ui_state.test.ts b/src/plugins/vis_type_table/public/utils/use/use_ui_state.test.ts new file mode 100644 index 0000000000000..be1f9d3a10cf7 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/use/use_ui_state.test.ts @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import type { PersistedState } from 'src/plugins/visualizations/public'; +import { TableVisUiState } from '../../types'; +import { useUiState } from './use_ui_state'; + +describe('useUiState', () => { + let uiState: PersistedState; + + beforeEach(() => { + uiState = { + get: jest.fn(), + on: jest.fn(), + off: jest.fn(), + set: jest.fn(), + } as any; + }); + + it("should init default columnsWidth & sort if uiState doesn't have it set", () => { + const { result } = renderHook(() => useUiState(uiState)); + + expect(result.current).toEqual({ + columnsWidth: [], + sort: { + columnIndex: null, + direction: null, + }, + setColumnsWidth: expect.any(Function), + setSort: expect.any(Function), + }); + }); + + it('should subscribe on uiState changes and update local state', async () => { + const { result, unmount, waitForNextUpdate } = renderHook(() => useUiState(uiState)); + + expect(uiState.on).toHaveBeenCalledWith('change', expect.any(Function)); + // @ts-expect-error + const updateOnChange = uiState.on.mock.calls[0][1]; + + uiState.getChanges = jest.fn(() => ({ + vis: { + params: { + sort: { + columnIndex: 1, + direction: 'asc', + }, + colWidth: [], + }, + }, + })); + + act(() => { + updateOnChange(); + }); + + await waitForNextUpdate(); + + // should update local state with new values + expect(result.current).toEqual({ + columnsWidth: [], + sort: { + columnIndex: 1, + direction: 'asc', + }, + setColumnsWidth: expect.any(Function), + setSort: expect.any(Function), + }); + + act(() => { + updateOnChange(); + }); + + // should skip setting the state again if it is equal + expect(result.current).toEqual({ + columnsWidth: [], + sort: { + columnIndex: 1, + direction: 'asc', + }, + setColumnsWidth: expect.any(Function), + setSort: expect.any(Function), + }); + + unmount(); + + expect(uiState.off).toHaveBeenCalledWith('change', updateOnChange); + }); + + describe('updating uiState through callbacks', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + it('should update the uiState with new sort', async () => { + const { result } = renderHook(() => useUiState(uiState)); + const newSort: TableVisUiState['sort'] = { + columnIndex: 5, + direction: 'desc', + }; + + act(() => { + result.current.setSort(newSort); + }); + + expect(result.current.sort).toEqual(newSort); + + jest.runAllTimers(); + + expect(uiState.set).toHaveBeenCalledTimes(1); + expect(uiState.set).toHaveBeenCalledWith('vis.params.sort', newSort); + }); + + it('should update the uiState with new columns width', async () => { + const { result } = renderHook(() => useUiState(uiState)); + const col1 = { colIndex: 0, width: 300 }; + const col2 = { colIndex: 1, width: 100 }; + + // set width of a column + act(() => { + result.current.setColumnsWidth(col1); + }); + + expect(result.current.columnsWidth).toEqual([col1]); + + jest.runAllTimers(); + + expect(uiState.set).toHaveBeenCalledTimes(1); + expect(uiState.set).toHaveBeenLastCalledWith('vis.params.colWidth', [col1]); + + // set width of another column + act(() => { + result.current.setColumnsWidth(col2); + }); + + jest.runAllTimers(); + + expect(uiState.set).toHaveBeenCalledTimes(2); + expect(uiState.set).toHaveBeenLastCalledWith('vis.params.colWidth', [col1, col2]); + + const updatedCol1 = { colIndex: 0, width: 200 }; + // update width of existing column + act(() => { + result.current.setColumnsWidth(updatedCol1); + }); + + jest.runAllTimers(); + + expect(uiState.set).toHaveBeenCalledTimes(3); + expect(uiState.set).toHaveBeenCalledWith('vis.params.colWidth', [updatedCol1, col2]); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + }); +}); From 4a1946b7ae980181b3becb021baec2f18f9373ed Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 8 Feb 2021 13:48:18 +0100 Subject: [PATCH 38/44] [Lens] Retain column config (#90048) --- .../visualization.test.tsx | 34 +++++++++++++++++++ .../datatable_visualization/visualization.tsx | 12 ++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx index 25275ba8e2249..2a6228f16867d 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -108,6 +108,40 @@ describe('Datatable Visualization', () => { expect(suggestions.length).toBeGreaterThan(0); }); + it('should retain width and hidden config from existing state', () => { + const suggestions = datatableVisualization.getSuggestions({ + state: { + layerId: 'first', + columns: [ + { columnId: 'col1', width: 123 }, + { columnId: 'col2', hidden: true }, + ], + sorting: { + columnId: 'col1', + direction: 'asc', + }, + }, + table: { + isMultiRow: true, + layerId: 'first', + changeType: 'initial', + columns: [numCol('col1'), strCol('col2'), strCol('col3')], + }, + keptLayerIds: [], + }); + + expect(suggestions.length).toBeGreaterThan(0); + expect(suggestions[0].state.columns).toEqual([ + { columnId: 'col1', width: 123 }, + { columnId: 'col2', hidden: true }, + { columnId: 'col3' }, + ]); + expect(suggestions[0].state.sorting).toEqual({ + columnId: 'col1', + direction: 'asc', + }); + }); + it('should not make suggestions when the table is unchanged', () => { const suggestions = datatableVisualization.getSuggestions({ state: { diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index 77fda43c37fef..9625a814c7958 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -98,6 +98,12 @@ export const datatableVisualization: Visualization ) { return []; } + const oldColumnSettings: Record = {}; + if (state) { + state.columns.forEach((column) => { + oldColumnSettings[column.columnId] = column; + }); + } const title = table.changeType === 'unchanged' ? i18n.translate('xpack.lens.datatable.suggestionLabel', { @@ -126,8 +132,12 @@ export const datatableVisualization: Visualization // table with >= 10 columns will have a score of 0.4, fewer columns reduce score score: (Math.min(table.columns.length, 10) / 10) * 0.4, state: { + ...(state || {}), layerId: table.layerId, - columns: table.columns.map((col) => ({ columnId: col.columnId })), + columns: table.columns.map((col) => ({ + ...(oldColumnSettings[col.columnId] || {}), + columnId: col.columnId, + })), }, previewIcon: LensIconChartDatatable, // tables are hidden from suggestion bar, but used for drag & drop and chart switching From be9f7c3dc99658d315dd21963aaf243a34a85e2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Mon, 8 Feb 2021 14:37:32 +0100 Subject: [PATCH 39/44] [APM] Export ProcessorEvent type (#90540) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/apm/server/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index f0524e67d324c..da3afd03513f0 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -97,3 +97,4 @@ export const plugin = (initContext: PluginInitializerContext) => new APMPlugin(initContext); export { APMPlugin, APMPluginSetup } from './plugin'; +export type { ProcessorEvent } from '../common/processor_event'; From d201ed756f79bae54e1d4b03c44ecdf3ea3bdda6 Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Mon, 8 Feb 2021 13:40:41 +0000 Subject: [PATCH 40/44] [APM-UI][E2E] use githubNotify step (#90514) --- .ci/end2end.groovy | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.ci/end2end.groovy b/.ci/end2end.groovy index 025836a90204c..a89ff166bf32e 100644 --- a/.ci/end2end.groovy +++ b/.ci/end2end.groovy @@ -121,9 +121,15 @@ pipeline { } def notifyStatus(String description, String status) { - withGithubNotify.notify('end2end-for-apm-ui', description, status, getBlueoceanTabURL('pipeline')) + notify(context: 'end2end-for-apm-ui', description: description, status: status, targetUrl: getBlueoceanTabURL('pipeline')) } def notifyTestStatus(String description, String status) { - withGithubNotify.notify('end2end-for-apm-ui', description, status, getBlueoceanTabURL('tests')) + notify(context: 'end2end-for-apm-ui', description: description, status: status, targetUrl: getBlueoceanTabURL('tests')) +} + +def notify(Map args = [:]) { + retryWithSleep(retries: 2, seconds: 5, backoff: true) { + githubNotify(context: args.context, description: args.description, status: args.status, targetUrl: args.targetUrl) + } } From 3d2373325c76dc595d53f78209ff98b86e57ff81 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Mon, 8 Feb 2021 15:34:19 +0100 Subject: [PATCH 41/44] [Discover] Inline state modifying function to react (#89543) --- .../public/application/angular/discover.js | 111 +--------- .../application/angular/discover_legacy.html | 11 - .../angular/doc_table/actions/columns.ts | 61 ++++++ .../components/create_discover_directive.ts | 10 - .../application/components/discover.test.tsx | 25 +-- .../application/components/discover.tsx | 205 ++++++++++-------- .../components/discover_topnav.test.tsx | 68 ++++++ .../components/discover_topnav.tsx | 71 ++++++ .../sidebar/discover_index_pattern.test.tsx | 40 ++-- .../sidebar/discover_index_pattern.tsx | 60 ++++- .../sidebar/discover_sidebar.test.tsx | 8 +- .../components/sidebar/discover_sidebar.tsx | 107 ++------- .../discover_sidebar_responsive.test.tsx | 22 +- .../sidebar/discover_sidebar_responsive.tsx | 26 ++- .../public/application/components/types.ts | 55 +---- .../application/helpers/popularize_field.ts | 4 +- 16 files changed, 480 insertions(+), 404 deletions(-) create mode 100644 src/plugins/discover/public/application/components/discover_topnav.test.tsx create mode 100644 src/plugins/discover/public/application/components/discover_topnav.tsx diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index b22bb6dc71342..af63485507d05 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -22,7 +22,6 @@ import { syncQueryStateWithUrl, } from '../../../../data/public'; import { getSortArray } from './doc_table'; -import * as columnActions from './doc_table/actions/columns'; import indexTemplateLegacy from './discover_legacy.html'; import { addHelpMenuToAppChrome } from '../components/help_menu/help_menu_util'; import { discoverResponseHandler } from './response_handler'; @@ -43,13 +42,9 @@ import { setBreadcrumbsTitle, } from '../helpers/breadcrumbs'; import { validateTimeRange } from '../helpers/validate_time_range'; -import { popularizeField } from '../helpers/popularize_field'; -import { getSwitchIndexPatternAppState } from '../helpers/get_switch_index_pattern_app_state'; import { addFatalError } from '../../../../kibana_legacy/public'; -import { METRIC_TYPE } from '@kbn/analytics'; import { DEFAULT_COLUMNS_SETTING, - MODIFY_COLUMNS_ON_SWITCH, SAMPLE_SIZE_SETTING, SEARCH_FIELDS_FROM_SOURCE, SEARCH_ON_PAGE_LOAD_SETTING, @@ -69,12 +64,10 @@ const { chrome, data, history: getHistory, - indexPatterns, filterManager, timefilter, toastNotifications, uiSettings: config, - trackUiMetric, } = getServices(); const fetchStatuses = { @@ -292,21 +285,6 @@ function discoverController($route, $scope, Promise) { } ); - $scope.setIndexPattern = async (id) => { - const nextIndexPattern = await indexPatterns.get(id); - if (nextIndexPattern) { - const nextAppState = getSwitchIndexPatternAppState( - $scope.indexPattern, - nextIndexPattern, - $scope.state.columns, - $scope.state.sort, - config.get(MODIFY_COLUMNS_ON_SWITCH), - $scope.useNewFieldsApi - ); - await setAppState(nextAppState); - } - }; - // update data source when filters update subscriptions.add( subscribeWithScope( @@ -327,6 +305,7 @@ function discoverController($route, $scope, Promise) { sampleSize: config.get(SAMPLE_SIZE_SETTING), timefield: getTimeField(), savedSearch: savedSearch, + services, indexPatternList: $route.current.locals.savedObjects.ip.list, config: config, setHeaderActionMenu: getHeaderActionMenuMounter(), @@ -340,18 +319,8 @@ function discoverController($route, $scope, Promise) { requests: new RequestAdapter(), }); - $scope.timefilterUpdateHandler = (ranges) => { - timefilter.setTime({ - from: moment(ranges.from).toISOString(), - to: moment(ranges.to).toISOString(), - mode: 'absolute', - }); - }; $scope.minimumVisibleRows = 50; $scope.fetchStatus = fetchStatuses.UNINITIALIZED; - $scope.showSaveQuery = capabilities.discover.saveQuery; - $scope.showTimeCol = - !config.get('doc_table:hideTimeColumn', false) && $scope.indexPattern.timeFieldName; let abortController; $scope.$on('$destroy', () => { @@ -495,12 +464,6 @@ function discoverController($route, $scope, Promise) { ) ); - $scope.changeInterval = (interval) => { - if (interval) { - setAppState({ interval }); - } - }; - $scope.$watchMulti( ['rows', 'fetchStatus'], (function updateResultState() { @@ -606,19 +569,6 @@ function discoverController($route, $scope, Promise) { } }; - $scope.updateSavedQueryId = (newSavedQueryId) => { - if (newSavedQueryId) { - setAppState({ savedQuery: newSavedQueryId }); - } else { - // remove savedQueryId from state - const state = { - ...appStateContainer.getState(), - }; - delete state.savedQuery; - appStateContainer.set(state); - } - }; - function getDimensions(aggs, timeRange) { const [metric, agg] = aggs; agg.params.timeRange = timeRange; @@ -752,65 +702,6 @@ function discoverController($route, $scope, Promise) { return Promise.resolve(); }; - $scope.setSortOrder = function setSortOrder(sort) { - setAppState({ sort }); - }; - - // TODO: On array fields, negating does not negate the combination, rather all terms - $scope.filterQuery = function (field, values, operation) { - const { indexPattern } = $scope; - - popularizeField(indexPattern, field.name, indexPatterns); - const newFilters = esFilters.generateFilters( - filterManager, - field, - values, - operation, - $scope.indexPattern.id - ); - if (trackUiMetric) { - trackUiMetric(METRIC_TYPE.CLICK, 'filter_added'); - } - return filterManager.addFilters(newFilters); - }; - - $scope.addColumn = function addColumn(columnName) { - const { indexPattern, useNewFieldsApi } = $scope; - if (capabilities.discover.save) { - popularizeField(indexPattern, columnName, indexPatterns); - } - const columns = columnActions.addColumn($scope.state.columns, columnName, useNewFieldsApi); - setAppState({ columns }); - }; - - $scope.removeColumn = function removeColumn(columnName) { - const { indexPattern, useNewFieldsApi } = $scope; - if (capabilities.discover.save) { - popularizeField(indexPattern, columnName, indexPatterns); - } - const columns = columnActions.removeColumn($scope.state.columns, columnName, useNewFieldsApi); - // The state's sort property is an array of [sortByColumn,sortDirection] - const sort = $scope.state.sort.length - ? $scope.state.sort.filter((subArr) => subArr[0] !== columnName) - : []; - setAppState({ columns, sort }); - }; - - $scope.moveColumn = function moveColumn(columnName, newIndex) { - const columns = columnActions.moveColumn($scope.state.columns, columnName, newIndex); - setAppState({ columns }); - }; - - $scope.setColumns = function setColumns(columns) { - // remove first element of columns if it's the configured timeFieldName, which is prepended automatically - const actualColumns = - $scope.indexPattern.timeFieldName && $scope.indexPattern.timeFieldName === columns[0] - ? columns.slice(1) - : columns; - $scope.state = { ...$scope.state, columns: actualColumns }; - setAppState({ columns: actualColumns }); - }; - async function setupVisualization() { // If no timefield has been specified we don't create a histogram of messages if (!getTimeField()) return; diff --git a/src/plugins/discover/public/application/angular/discover_legacy.html b/src/plugins/discover/public/application/angular/discover_legacy.html index 83a9cf23c85f3..dc18b7929318b 100644 --- a/src/plugins/discover/public/application/angular/discover_legacy.html +++ b/src/plugins/discover/public/application/angular/discover_legacy.html @@ -8,27 +8,16 @@ hits="hits" index-pattern="indexPattern" minimum-visible-rows="minimumVisibleRows" - on-add-column="addColumn" - on-add-filter="filterQuery" - on-move-column="moveColumn" - on-change-interval="changeInterval" - on-remove-column="removeColumn" - on-set-columns="setColumns" on-skip-bottom-button-click="onSkipBottomButtonClick" - on-sort="setSortOrder" opts="opts" reset-query="resetQuery" result-state="resultState" rows="rows" search-source="searchSource" - set-index-pattern="setIndexPattern" - show-save-query="showSaveQuery" state="state" - time-filter-update-handler="timefilterUpdateHandler" time-range="timeRange" top-nav-menu="topNavMenu" update-query="handleRefresh" - update-saved-query-id="updateSavedQueryId" use-new-fields-api="useNewFieldsApi" unmapped-fields-config="unmappedFieldsConfig" > diff --git a/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts b/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts index 946f11024360f..53ced59b17c5d 100644 --- a/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts +++ b/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts @@ -5,6 +5,10 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import { Capabilities } from 'kibana/public'; +import { popularizeField } from '../../../helpers/popularize_field'; +import { IndexPattern, IndexPatternsContract } from '../../../../kibana_services'; +import { AppState } from '../../discover_state'; /** * Helper function to provide a fallback to a single _source column if the given array of columns @@ -47,3 +51,60 @@ export function moveColumn(columns: string[], columnName: string, newIndex: numb modifiedColumns.splice(newIndex, 0, columnName); // insert before new index return modifiedColumns; } + +export function getStateColumnActions({ + capabilities, + indexPattern, + indexPatterns, + useNewFieldsApi, + setAppState, + state, +}: { + capabilities: Capabilities; + indexPattern: IndexPattern; + indexPatterns: IndexPatternsContract; + useNewFieldsApi: boolean; + setAppState: (state: Partial) => void; + state: AppState; +}) { + function onAddColumn(columnName: string) { + if (capabilities.discover.save) { + popularizeField(indexPattern, columnName, indexPatterns); + } + const columns = addColumn(state.columns || [], columnName, useNewFieldsApi); + setAppState({ columns }); + } + + function onRemoveColumn(columnName: string) { + if (capabilities.discover.save) { + popularizeField(indexPattern, columnName, indexPatterns); + } + const columns = removeColumn(state.columns || [], columnName, useNewFieldsApi); + // The state's sort property is an array of [sortByColumn,sortDirection] + const sort = + state.sort && state.sort.length + ? state.sort.filter((subArr) => subArr[0] !== columnName) + : []; + setAppState({ columns, sort }); + } + + function onMoveColumn(columnName: string, newIndex: number) { + const columns = moveColumn(state.columns || [], columnName, newIndex); + setAppState({ columns }); + } + + function onSetColumns(columns: string[]) { + // remove first element of columns if it's the configured timeFieldName, which is prepended automatically + const actualColumns = + indexPattern.timeFieldName && indexPattern.timeFieldName === columns[0] + ? columns.slice(1) + : columns; + setAppState({ columns: actualColumns }); + } + return { + onAddColumn, + onRemoveColumn, + onMoveColumn, + onSetColumns, + }; +} diff --git a/src/plugins/discover/public/application/components/create_discover_directive.ts b/src/plugins/discover/public/application/components/create_discover_directive.ts index 2a88c1b713132..8d1360aeaddad 100644 --- a/src/plugins/discover/public/application/components/create_discover_directive.ts +++ b/src/plugins/discover/public/application/components/create_discover_directive.ts @@ -5,7 +5,6 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import { Discover } from './discover'; export function createDiscoverDirective(reactDirective: any) { @@ -18,24 +17,15 @@ export function createDiscoverDirective(reactDirective: any) { ['hits', { watchDepth: 'reference' }], ['indexPattern', { watchDepth: 'reference' }], ['minimumVisibleRows', { watchDepth: 'reference' }], - ['onAddColumn', { watchDepth: 'reference' }], - ['onAddFilter', { watchDepth: 'reference' }], - ['onChangeInterval', { watchDepth: 'reference' }], - ['onMoveColumn', { watchDepth: 'reference' }], - ['onRemoveColumn', { watchDepth: 'reference' }], - ['onSetColumns', { watchDepth: 'reference' }], ['onSkipBottomButtonClick', { watchDepth: 'reference' }], - ['onSort', { watchDepth: 'reference' }], ['opts', { watchDepth: 'reference' }], ['resetQuery', { watchDepth: 'reference' }], ['resultState', { watchDepth: 'reference' }], ['rows', { watchDepth: 'reference' }], ['savedSearch', { watchDepth: 'reference' }], ['searchSource', { watchDepth: 'reference' }], - ['setIndexPattern', { watchDepth: 'reference' }], ['showSaveQuery', { watchDepth: 'reference' }], ['state', { watchDepth: 'reference' }], - ['timefilterUpdateHandler', { watchDepth: 'reference' }], ['timeRange', { watchDepth: 'reference' }], ['topNavMenu', { watchDepth: 'reference' }], ['updateQuery', { watchDepth: 'reference' }], diff --git a/src/plugins/discover/public/application/components/discover.test.tsx b/src/plugins/discover/public/application/components/discover.test.tsx index bb0014f4278a1..f0f11558abd65 100644 --- a/src/plugins/discover/public/application/components/discover.test.tsx +++ b/src/plugins/discover/public/application/components/discover.test.tsx @@ -11,6 +11,7 @@ import { shallowWithIntl } from '@kbn/test/jest'; import { Discover } from './discover'; import { esHits } from '../../__mocks__/es_hits'; import { indexPatternMock } from '../../__mocks__/index_pattern'; +import { DiscoverServices } from '../../build_services'; import { GetStateReturn } from '../angular/discover_state'; import { savedSearchMock } from '../../__mocks__/saved_search'; import { createSearchSourceMock } from '../../../../data/common/search/search_source/mocks'; @@ -46,7 +47,14 @@ jest.mock('../../kibana_services', () => { function getProps(indexPattern: IndexPattern): DiscoverProps { const searchSourceMock = createSearchSourceMock({}); - const state = ({} as unknown) as GetStateReturn; + const services = ({ + capabilities: { + discover: { + save: true, + }, + }, + uiSettings: mockUiSettings, + } as unknown) as DiscoverServices; return { fetch: jest.fn(), @@ -56,14 +64,7 @@ function getProps(indexPattern: IndexPattern): DiscoverProps { hits: esHits.length, indexPattern, minimumVisibleRows: 10, - onAddColumn: jest.fn(), - onAddFilter: jest.fn(), - onChangeInterval: jest.fn(), - onMoveColumn: jest.fn(), - onRemoveColumn: jest.fn(), - onSetColumns: jest.fn(), onSkipBottomButtonClick: jest.fn(), - onSort: jest.fn(), opts: { config: mockUiSettings, data: dataPluginMock.createStartContract(), @@ -74,20 +75,18 @@ function getProps(indexPattern: IndexPattern): DiscoverProps { navigateTo: jest.fn(), sampleSize: 10, savedSearch: savedSearchMock, - setAppState: jest.fn(), setHeaderActionMenu: jest.fn(), - stateContainer: state, timefield: indexPattern.timeFieldName || '', + setAppState: jest.fn(), + services, + stateContainer: {} as GetStateReturn, }, resetQuery: jest.fn(), resultState: 'ready', rows: esHits, searchSource: searchSourceMock, - setIndexPattern: jest.fn(), state: { columns: [] }, - timefilterUpdateHandler: jest.fn(), updateQuery: jest.fn(), - updateSavedQueryId: jest.fn(), }; } diff --git a/src/plugins/discover/public/application/components/discover.tsx b/src/plugins/discover/public/application/components/discover.tsx index baee0623f0b5a..99baa30e18c7a 100644 --- a/src/plugins/discover/public/application/components/discover.tsx +++ b/src/plugins/discover/public/application/components/discover.tsx @@ -5,9 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import './discover.scss'; -import React, { useState, useRef, useMemo } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import { EuiButtonEmpty, EuiButtonIcon, @@ -21,37 +20,34 @@ import { EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { METRIC_TYPE } from '@kbn/analytics'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import classNames from 'classnames'; import { HitsCounter } from './hits_counter'; import { TimechartHeader } from './timechart_header'; -import { getServices } from '../../kibana_services'; import { DiscoverHistogram, DiscoverUninitialized } from '../angular/directives'; import { DiscoverNoResults } from './no_results'; import { LoadingSpinner } from './loading_spinner/loading_spinner'; -import { DocTableLegacy, DocTableLegacyProps } from '../angular/doc_table/create_doc_table_react'; +import { DocTableLegacy } from '../angular/doc_table/create_doc_table_react'; import { SkipBottomButton } from './skip_bottom_button'; -import { search } from '../../../../data/public'; -import { - DiscoverSidebarResponsive, - DiscoverSidebarResponsiveProps, -} from './sidebar/discover_sidebar_responsive'; +import { esFilters, IndexPatternField, search } from '../../../../data/public'; +import { DiscoverSidebarResponsive } from './sidebar'; import { DiscoverProps } from './types'; import { getDisplayedColumns } from '../helpers/columns'; import { SortPairArr } from '../angular/doc_table/lib/get_sort'; -import { DiscoverGrid, DiscoverGridProps } from './discover_grid/discover_grid'; import { SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; +import { popularizeField } from '../helpers/popularize_field'; +import { getStateColumnActions } from '../angular/doc_table/actions/columns'; +import { DocViewFilterFn } from '../doc_views/doc_views_types'; +import { DiscoverGrid } from './discover_grid/discover_grid'; +import { DiscoverTopNav } from './discover_topnav'; import { ElasticSearchHit } from '../doc_views/doc_views_types'; -import { getTopNavLinks } from './top_nav/get_top_nav_links'; -const DocTableLegacyMemoized = React.memo((props: DocTableLegacyProps) => ( - -)); -const SidebarMemoized = React.memo((props: DiscoverSidebarResponsiveProps) => ( - -)); - -const DataGridMemoized = React.memo((props: DiscoverGridProps) => ); +const DocTableLegacyMemoized = React.memo(DocTableLegacy); +const SidebarMemoized = React.memo(DiscoverSidebarResponsive); +const DataGridMemoized = React.memo(DiscoverGrid); +const TopNavMemoized = React.memo(DiscoverTopNav); export function Discover({ fetch, @@ -62,25 +58,15 @@ export function Discover({ hits, indexPattern, minimumVisibleRows, - onAddColumn, - onAddFilter, - onChangeInterval, - onMoveColumn, - onRemoveColumn, - onSetColumns, onSkipBottomButtonClick, - onSort, opts, resetQuery, resultState, rows, searchSource, - setIndexPattern, state, - timefilterUpdateHandler, timeRange, updateQuery, - updateSavedQueryId, unmappedFieldsConfig, }: DiscoverProps) { const [expandedDoc, setExpandedDoc] = useState(undefined); @@ -92,28 +78,9 @@ export function Discover({ }; const [toggleOn, toggleChart] = useState(true); + const { savedSearch, indexPatternList, config, services, data, setAppState } = opts; + const { trackUiMetric, capabilities, indexPatterns } = services; const [isSidebarClosed, setIsSidebarClosed] = useState(false); - const services = useMemo(() => getServices(), []); - const topNavMenu = useMemo( - () => - getTopNavLinks({ - getFieldCounts: opts.getFieldCounts, - indexPattern, - inspectorAdapters: opts.inspectorAdapters, - navigateTo: opts.navigateTo, - savedSearch: opts.savedSearch, - services, - state: opts.stateContainer, - onOpenInspector: () => { - // prevent overlapping - setExpandedDoc(undefined); - }, - }), - [indexPattern, opts, services] - ); - const { TopNavMenu } = services.navigation.ui; - const { trackUiMetric } = services; - const { savedSearch, indexPatternList, config } = opts; const bucketAggConfig = opts.chartAggConfigs?.aggs[1]; const bucketInterval = bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig) @@ -123,6 +90,95 @@ export function Discover({ const isLegacy = services.uiSettings.get('doc_table:legacy'); const useNewFieldsApi = !services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); + const { onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useMemo( + () => + getStateColumnActions({ + capabilities, + indexPattern, + indexPatterns, + setAppState, + state, + useNewFieldsApi, + }), + [capabilities, indexPattern, indexPatterns, setAppState, state, useNewFieldsApi] + ); + + const onOpenInspector = useCallback(() => { + // prevent overlapping + setExpandedDoc(undefined); + }, [setExpandedDoc]); + + const onSort = useCallback( + (sort: string[][]) => { + setAppState({ sort }); + }, + [setAppState] + ); + + const onAddFilter = useCallback( + (field: IndexPatternField | string, values: string, operation: '+' | '-') => { + const fieldName = typeof field === 'string' ? field : field.name; + popularizeField(indexPattern, fieldName, indexPatterns); + const newFilters = esFilters.generateFilters( + opts.filterManager, + field, + values, + operation, + String(indexPattern.id) + ); + if (trackUiMetric) { + trackUiMetric(METRIC_TYPE.CLICK, 'filter_added'); + } + return opts.filterManager.addFilters(newFilters); + }, + [opts, indexPattern, indexPatterns, trackUiMetric] + ); + + const onChangeInterval = useCallback( + (interval: string) => { + if (interval) { + setAppState({ interval }); + } + }, + [setAppState] + ); + + const timefilterUpdateHandler = useCallback( + (ranges: { from: number; to: number }) => { + data.query.timefilter.timefilter.setTime({ + from: moment(ranges.from).toISOString(), + to: moment(ranges.to).toISOString(), + mode: 'absolute', + }); + }, + [data] + ); + + const onBackToTop = useCallback(() => { + if (scrollableDesktop && scrollableDesktop.current) { + scrollableDesktop.current.focus(); + } + // Only the desktop one needs to target a specific container + if (!isMobile() && scrollableDesktop.current) { + scrollableDesktop.current.scrollTo(0, 0); + } else if (window) { + window.scrollTo(0, 0); + } + }, [scrollableDesktop]); + + const onResize = useCallback( + (colSettings: { columnId: string; width: number }) => { + const grid = { ...state.grid } || {}; + const newColumns = { ...grid.columns } || {}; + newColumns[colSettings.columnId] = { + width: colSettings.width, + }; + const newGrid = { ...grid, columns: newColumns }; + opts.setAppState({ grid: newGrid }); + }, + [opts, state] + ); + const columns = useMemo(() => { if (!state.columns) { return []; @@ -132,20 +188,12 @@ export function Discover({ return ( -

    @@ -154,16 +202,19 @@ export function Discover({ { - if (scrollableDesktop && scrollableDesktop.current) { - scrollableDesktop.current.focus(); - } - // Only the desktop one needs to target a specific container - if (!isMobile() && scrollableDesktop.current) { - scrollableDesktop.current.scrollTo(0, 0); - } else if (window) { - window.scrollTo(0, 0); - } - }} + onBackToTop={onBackToTop} onFilter={onAddFilter} onMoveColumn={onMoveColumn} onRemoveColumn={onRemoveColumn} @@ -352,19 +393,11 @@ export function Discover({ services={services} settings={state.grid} onAddColumn={onAddColumn} - onFilter={onAddFilter} + onFilter={onAddFilter as DocViewFilterFn} onRemoveColumn={onRemoveColumn} onSetColumns={onSetColumns} onSort={onSort} - onResize={(colSettings: { columnId: string; width: number }) => { - const grid = { ...state.grid } || {}; - const newColumns = { ...grid.columns } || {}; - newColumns[colSettings.columnId] = { - width: colSettings.width, - }; - const newGrid = { ...grid, columns: newColumns }; - opts.setAppState({ grid: newGrid }); - }} + onResize={onResize} />

    )} diff --git a/src/plugins/discover/public/application/components/discover_topnav.test.tsx b/src/plugins/discover/public/application/components/discover_topnav.test.tsx new file mode 100644 index 0000000000000..3f12386281059 --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_topnav.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { indexPatternMock } from '../../__mocks__/index_pattern'; +import { DiscoverServices } from '../../build_services'; +import { AppState, GetStateReturn } from '../angular/discover_state'; +import { savedSearchMock } from '../../__mocks__/saved_search'; +import { dataPluginMock } from '../../../../data/public/mocks'; +import { createFilterManagerMock } from '../../../../data/public/query/filter_manager/filter_manager.mock'; +import { uiSettingsMock as mockUiSettings } from '../../__mocks__/ui_settings'; +import { IndexPatternAttributes } from '../../../../data/common/index_patterns'; +import { SavedObject } from '../../../../../core/types'; +import { DiscoverTopNav, DiscoverTopNavProps } from './discover_topnav'; +import { RequestAdapter } from '../../../../inspector/common/adapters/request'; +import { TopNavMenu } from '../../../../navigation/public'; + +function getProps(): DiscoverTopNavProps { + const state = ({} as unknown) as AppState; + const services = ({ + navigation: { + ui: { TopNavMenu }, + }, + capabilities: { + discover: { + save: true, + }, + }, + uiSettings: mockUiSettings, + } as unknown) as DiscoverServices; + const indexPattern = indexPatternMock; + return { + indexPattern: indexPatternMock, + opts: { + config: mockUiSettings, + data: dataPluginMock.createStartContract(), + filterManager: createFilterManagerMock(), + getFieldCounts: jest.fn(), + indexPatternList: (indexPattern as unknown) as Array>, + inspectorAdapters: { requests: {} as RequestAdapter }, + navigateTo: jest.fn(), + sampleSize: 10, + savedSearch: savedSearchMock, + services, + setAppState: jest.fn(), + setHeaderActionMenu: jest.fn(), + stateContainer: {} as GetStateReturn, + timefield: indexPattern.timeFieldName || '', + }, + state, + updateQuery: jest.fn(), + onOpenInspector: jest.fn(), + }; +} + +describe('Discover topnav component', () => { + test('setHeaderActionMenu was called', () => { + const props = getProps(); + mountWithIntl(); + expect(props.opts.setHeaderActionMenu).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/discover/public/application/components/discover_topnav.tsx b/src/plugins/discover/public/application/components/discover_topnav.tsx new file mode 100644 index 0000000000000..69a1433b6505c --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_topnav.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React, { useMemo } from 'react'; +import { DiscoverProps } from './types'; +import { getTopNavLinks } from './top_nav/get_top_nav_links'; + +export type DiscoverTopNavProps = Pick< + DiscoverProps, + 'indexPattern' | 'updateQuery' | 'state' | 'opts' +> & { onOpenInspector: () => void }; + +export const DiscoverTopNav = ({ + indexPattern, + opts, + onOpenInspector, + state, + updateQuery, +}: DiscoverTopNavProps) => { + const showDatePicker = useMemo(() => indexPattern.isTimeBased(), [indexPattern]); + const { TopNavMenu } = opts.services.navigation.ui; + const topNavMenu = useMemo( + () => + getTopNavLinks({ + getFieldCounts: opts.getFieldCounts, + indexPattern, + inspectorAdapters: opts.inspectorAdapters, + navigateTo: opts.navigateTo, + savedSearch: opts.savedSearch, + services: opts.services, + state: opts.stateContainer, + onOpenInspector, + }), + [indexPattern, opts, onOpenInspector] + ); + + const updateSavedQueryId = (newSavedQueryId: string | undefined) => { + const { appStateContainer, setAppState } = opts.stateContainer; + if (newSavedQueryId) { + setAppState({ savedQuery: newSavedQueryId }); + } else { + // remove savedQueryId from state + const newState = { + ...appStateContainer.getState(), + }; + delete newState.savedQuery; + appStateContainer.set(newState); + } + }; + return ( + + ); +}; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx index 7178eccfec4b6..73de3b14f88f6 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx @@ -7,39 +7,44 @@ */ import React from 'react'; +import { act } from 'react-dom/test-utils'; import { shallowWithIntl as shallow } from '@kbn/test/jest'; - -// @ts-ignore import { ShallowWrapper } from 'enzyme'; import { ChangeIndexPattern } from './change_indexpattern'; import { SavedObject } from 'kibana/server'; -import { DiscoverIndexPattern } from './discover_index_pattern'; +import { DiscoverIndexPattern, DiscoverIndexPatternProps } from './discover_index_pattern'; import { EuiSelectable } from '@elastic/eui'; -import { IIndexPattern } from 'src/plugins/data/public'; +import { IndexPattern } from 'src/plugins/data/public'; +import { configMock } from '../../../__mocks__/config'; +import { indexPatternsMock } from '../../../__mocks__/index_patterns'; const indexPattern = { - id: 'test1', + id: 'the-index-pattern-id-first', title: 'test1 title', -} as IIndexPattern; +} as IndexPattern; const indexPattern1 = { - id: 'test1', + id: 'the-index-pattern-id-first', attributes: { title: 'test1 title', }, } as SavedObject; const indexPattern2 = { - id: 'test2', + id: 'the-index-pattern-id', attributes: { title: 'test2 title', }, } as SavedObject; const defaultProps = { + config: configMock, indexPatternList: [indexPattern1, indexPattern2], selectedIndexPattern: indexPattern, - setIndexPattern: jest.fn(async () => {}), + state: {}, + setAppState: jest.fn(), + useNewFieldsApi: true, + indexPatterns: indexPatternsMock, }; function getIndexPatternPickerList(instance: ShallowWrapper) { @@ -63,11 +68,11 @@ function selectIndexPatternPickerOption(instance: ShallowWrapper, selectedLabel: describe('DiscoverIndexPattern', () => { test('Invalid props dont cause an exception', () => { - const props = { + const props = ({ indexPatternList: null, selectedIndexPattern: null, setIndexPattern: jest.fn(), - } as any; + } as unknown) as DiscoverIndexPatternProps; expect(shallow()).toMatchSnapshot(`""`); }); @@ -80,10 +85,15 @@ describe('DiscoverIndexPattern', () => { ]); }); - test('should switch data panel to target index pattern', () => { + test('should switch data panel to target index pattern', async () => { const instance = shallow(); - - selectIndexPatternPickerOption(instance, 'test2 title'); - expect(defaultProps.setIndexPattern).toHaveBeenCalledWith('test2'); + await act(async () => { + selectIndexPatternPickerOption(instance, 'test2 title'); + }); + expect(defaultProps.setAppState).toHaveBeenCalledWith({ + index: 'the-index-pattern-id', + columns: [], + sort: [], + }); }); }); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx index 29c62d5c60775..ea3e35f607be4 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx @@ -6,35 +6,63 @@ * Side Public License, v 1. */ -import React, { useState, useEffect } from 'react'; -import { SavedObject } from 'kibana/public'; -import { IIndexPattern, IndexPatternAttributes } from 'src/plugins/data/public'; +import React, { useState, useEffect, useCallback } from 'react'; +import { IUiSettingsClient, SavedObject } from 'kibana/public'; +import { + IndexPattern, + IndexPatternAttributes, + IndexPatternsContract, +} from 'src/plugins/data/public'; import { I18nProvider } from '@kbn/i18n/react'; import { IndexPatternRef } from './types'; import { ChangeIndexPattern } from './change_indexpattern'; +import { getSwitchIndexPatternAppState } from '../../helpers/get_switch_index_pattern_app_state'; +import { SortPairArr } from '../../angular/doc_table/lib/get_sort'; +import { MODIFY_COLUMNS_ON_SWITCH } from '../../../../common'; +import { AppState } from '../../angular/discover_state'; export interface DiscoverIndexPatternProps { + /** + * Client of uiSettings + */ + config: IUiSettingsClient; /** * list of available index patterns, if length > 1, component offers a "change" link */ indexPatternList: Array>; + /** + * Index patterns service + */ + indexPatterns: IndexPatternsContract; /** * currently selected index pattern, due to angular issues it's undefined at first rendering */ - selectedIndexPattern: IIndexPattern; + selectedIndexPattern: IndexPattern; + /** + * Function to set the current state + */ + setAppState: (state: Partial) => void; + /** + * Discover App state + */ + state: AppState; /** - * triggered when user selects a new index pattern + * Read from the Fields API */ - setIndexPattern: (id: string) => void; + useNewFieldsApi?: boolean; } /** * Component allows you to select an index pattern in discovers side bar */ export function DiscoverIndexPattern({ + config, indexPatternList, selectedIndexPattern, - setIndexPattern, + indexPatterns, + state, + setAppState, + useNewFieldsApi, }: DiscoverIndexPatternProps) { const options: IndexPatternRef[] = (indexPatternList || []).map((entity) => ({ id: entity.id, @@ -42,6 +70,24 @@ export function DiscoverIndexPattern({ })); const { id: selectedId, title: selectedTitle } = selectedIndexPattern || {}; + const setIndexPattern = useCallback( + async (id: string) => { + const nextIndexPattern = await indexPatterns.get(id); + if (nextIndexPattern && selectedIndexPattern) { + const nextAppState = getSwitchIndexPatternAppState( + selectedIndexPattern, + nextIndexPattern, + state.columns || [], + (state.sort || []) as SortPairArr[], + config.get(MODIFY_COLUMNS_ON_SWITCH), + useNewFieldsApi + ); + setAppState(nextAppState); + } + }, + [selectedIndexPattern, state, config, indexPatterns, setAppState, useNewFieldsApi] + ); + const [selected, setSelected] = useState({ id: selectedId, title: selectedTitle || '', diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx index 9c33bbcbc200a..0ff70585af144 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx @@ -24,6 +24,8 @@ import { getDefaultFieldFilter } from './lib/field_filter'; import { DiscoverSidebar } from './discover_sidebar'; import { DiscoverServices } from '../../../build_services'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { configMock } from '../../../__mocks__/config'; +import { indexPatternsMock } from '../../../__mocks__/index_patterns'; const mockServices = ({ history: () => ({ @@ -56,7 +58,7 @@ jest.mock('./lib/get_index_pattern_field_list', () => ({ getIndexPatternFieldList: jest.fn((indexPattern) => indexPattern.fields), })); -function getCompProps() { +function getCompProps(): DiscoverSidebarProps { const indexPattern = getStubIndexPattern( 'logstash-*', (cfg: any) => cfg, @@ -84,20 +86,22 @@ function getCompProps() { } } return { + config: configMock, columns: ['extension'], fieldCounts, hits, indexPatternList, + indexPatterns: indexPatternsMock, onAddFilter: jest.fn(), onAddField: jest.fn(), onRemoveField: jest.fn(), selectedIndexPattern: indexPattern, services: mockServices, - setIndexPattern: jest.fn(), state: {}, trackUiMetric: jest.fn(), fieldFilter: getDefaultFieldFilter(), setFieldFilter: jest.fn(), + setAppState: jest.fn(), }; } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index db5f40d8e13cb..f0303553dfac0 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -9,7 +9,6 @@ import './discover_sidebar.scss'; import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { UiCounterMetricType } from '@kbn/analytics'; import { EuiAccordion, EuiFlexItem, @@ -25,122 +24,42 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { DiscoverField } from './discover_field'; import { DiscoverIndexPattern } from './discover_index_pattern'; import { DiscoverFieldSearch } from './discover_field_search'; -import { IndexPatternAttributes } from '../../../../../data/common'; -import { SavedObject } from '../../../../../../core/types'; import { FIELDS_LIMIT_SETTING } from '../../../../common'; import { groupFields } from './lib/group_fields'; -import { IndexPatternField, IndexPattern } from '../../../../../data/public'; +import { IndexPatternField } from '../../../../../data/public'; import { getDetails } from './lib/get_details'; import { FieldFilterState, getDefaultFieldFilter, setFieldFilterProp } from './lib/field_filter'; import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list'; -import { DiscoverServices } from '../../../build_services'; -import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { DiscoverSidebarResponsiveProps } from './discover_sidebar_responsive'; -export interface DiscoverSidebarProps { - /** - * Determines whether add/remove buttons are displayed not only when focused - */ - alwaysShowActionButtons?: boolean; - /** - * the selected columns displayed in the doc table in discover - */ - columns: string[]; - /** - * a statistics of the distribution of fields in the given hits - */ - fieldCounts: Record; +export interface DiscoverSidebarProps extends DiscoverSidebarResponsiveProps { /** * Current state of the field filter, filtering fields by name, type, ... */ fieldFilter: FieldFilterState; - /** - * hits fetched from ES, displayed in the doc table - */ - hits: ElasticSearchHit[]; - /** - * List of available index patterns - */ - indexPatternList: Array>; - /** - * Callback function when selecting a field - */ - onAddField: (fieldName: string) => void; - /** - * Callback function when adding a filter from sidebar - */ - onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; - /** - * Callback function when removing a field - * @param fieldName - */ - onRemoveField: (fieldName: string) => void; - /** - * Currently selected index pattern - */ - selectedIndexPattern?: IndexPattern; - /** - * Discover plugin services; - */ - services: DiscoverServices; /** * Change current state of fieldFilter */ setFieldFilter: (next: FieldFilterState) => void; - /** - * Callback function to select another index pattern - */ - setIndexPattern: (id: string) => void; - /** - * If on, fields are read from the fields API, not from source - */ - useNewFieldsApi?: boolean; - /** - * Metric tracking function - * @param metricType - * @param eventName - */ - trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; - /** - * Shows index pattern and a button that displays the sidebar in a flyout - */ - useFlyout?: boolean; - - /** - * an object containing properties for proper handling of unmapped fields in the UI - */ - unmappedFieldsConfig?: { - /** - * callback function to change the value of `showUnmappedFields` flag - * @param value new value to set - */ - onChangeUnmappedFields: (value: boolean) => void; - /** - * determines whether to display unmapped fields - * configurable through the switch in the UI - */ - showUnmappedFields: boolean; - /** - * determines if we should display an option to toggle showUnmappedFields value in the first place - * this value is not configurable through the UI - */ - showUnmappedFieldsDefaultValue: boolean; - }; } export function DiscoverSidebar({ alwaysShowActionButtons = false, columns, + config, fieldCounts, fieldFilter, hits, indexPatternList, + indexPatterns, onAddField, onAddFilter, onRemoveField, selectedIndexPattern, services, + setAppState, setFieldFilter, - setIndexPattern, + state, trackUiMetric, useNewFieldsApi = false, useFlyout = false, @@ -240,9 +159,13 @@ export function DiscoverSidebar({ })} > o.attributes.title)} + indexPatterns={indexPatterns} + state={state} + setAppState={setAppState} + useNewFieldsApi={useNewFieldsApi} /> ); @@ -266,9 +189,13 @@ export function DiscoverSidebar({ > o.attributes.title)} + indexPatterns={indexPatterns} + state={state} + setAppState={setAppState} + useNewFieldsApi={useNewFieldsApi} /> diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx index 7ee6cb56d99f2..02ab5abade7fb 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx @@ -15,15 +15,19 @@ import realHits from 'fixtures/real_hits.js'; import stubbedLogstashFields from 'fixtures/logstash_fields'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; -import { DiscoverSidebar, DiscoverSidebarProps } from './discover_sidebar'; import { coreMock } from '../../../../../../core/public/mocks'; import { IndexPatternAttributes } from '../../../../../data/common'; import { getStubIndexPattern } from '../../../../../data/public/test_utils'; import { SavedObject } from '../../../../../../core/types'; -import { FieldFilterState } from './lib/field_filter'; -import { DiscoverSidebarResponsive } from './discover_sidebar_responsive'; +import { + DiscoverSidebarResponsive, + DiscoverSidebarResponsiveProps, +} from './discover_sidebar_responsive'; import { DiscoverServices } from '../../../build_services'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { configMock } from '../../../__mocks__/config'; +import { indexPatternsMock } from '../../../__mocks__/index_patterns'; +import { DiscoverSidebar } from './discover_sidebar'; const mockServices = ({ history: () => ({ @@ -56,7 +60,7 @@ jest.mock('./lib/get_index_pattern_field_list', () => ({ getIndexPatternFieldList: jest.fn((indexPattern) => indexPattern.fields), })); -function getCompProps() { +function getCompProps(): DiscoverSidebarResponsiveProps { const indexPattern = getStubIndexPattern( 'logstash-*', (cfg: any) => cfg, @@ -85,25 +89,25 @@ function getCompProps() { } return { columns: ['extension'], + config: configMock, fieldCounts, hits, indexPatternList, + indexPatterns: indexPatternsMock, onAddFilter: jest.fn(), onAddField: jest.fn(), onRemoveField: jest.fn(), selectedIndexPattern: indexPattern, services: mockServices, - setIndexPattern: jest.fn(), + setAppState: jest.fn(), state: {}, trackUiMetric: jest.fn(), - fieldFilter: {} as FieldFilterState, - setFieldFilter: jest.fn(), }; } describe('discover responsive sidebar', function () { - let props: DiscoverSidebarProps; - let comp: ReactWrapper; + let props: DiscoverSidebarResponsiveProps; + let comp: ReactWrapper; beforeAll(() => { props = getCompProps(); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx index b8e8fd0679baa..b689db1296922 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx @@ -11,6 +11,7 @@ import { sortBy } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { UiCounterMetricType } from '@kbn/analytics'; +import { IUiSettingsClient } from 'kibana/public'; import { EuiTitle, EuiHideFor, @@ -25,13 +26,14 @@ import { EuiPortal, } from '@elastic/eui'; import { DiscoverIndexPattern } from './discover_index_pattern'; -import { IndexPatternAttributes } from '../../../../../data/common'; +import { IndexPatternAttributes, IndexPatternsContract } from '../../../../../data/common'; import { SavedObject } from '../../../../../../core/types'; import { IndexPatternField, IndexPattern } from '../../../../../data/public'; import { getDefaultFieldFilter } from './lib/field_filter'; import { DiscoverSidebar } from './discover_sidebar'; import { DiscoverServices } from '../../../build_services'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { AppState } from '../../angular/discover_state'; export interface DiscoverSidebarResponsiveProps { /** @@ -42,6 +44,10 @@ export interface DiscoverSidebarResponsiveProps { * the selected columns displayed in the doc table in discover */ columns: string[]; + /** + * Client of uiSettings + */ + config: IUiSettingsClient; /** * a statistics of the distribution of fields in the given hits */ @@ -54,6 +60,10 @@ export interface DiscoverSidebarResponsiveProps { * List of available index patterns */ indexPatternList: Array>; + /** + * Index patterns service + */ + indexPatterns: IndexPatternsContract; /** * Has been toggled closed */ @@ -80,9 +90,13 @@ export interface DiscoverSidebarResponsiveProps { */ services: DiscoverServices; /** - * Callback function to select another index pattern + * Function to set the current state + */ + setAppState: (state: Partial) => void; + /** + * Discover App state */ - setIndexPattern: (id: string) => void; + state: AppState; /** * Metric tracking function * @param metricType @@ -151,9 +165,13 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) )} > o.attributes.title)} + indexPatterns={props.indexPatterns} + state={props.state} + setAppState={props.setAppState} + useNewFieldsApi={props.useNewFieldsApi} /> diff --git a/src/plugins/discover/public/application/components/types.ts b/src/plugins/discover/public/application/components/types.ts index b73f7391bf22a..ee06bcab6528b 100644 --- a/src/plugins/discover/public/application/components/types.ts +++ b/src/plugins/discover/public/application/components/types.ts @@ -9,7 +9,7 @@ import { IUiSettingsClient, MountPoint, SavedObject } from 'kibana/public'; import { Chart } from '../angular/helpers/point_series'; import { IndexPattern } from '../../../../data/common/index_patterns/index_patterns'; -import { DocViewFilterFn, ElasticSearchHit } from '../doc_views/doc_views_types'; +import { ElasticSearchHit } from '../doc_views/doc_views_types'; import { AggConfigs } from '../../../../data/common/search/aggs'; import { @@ -23,6 +23,7 @@ import { import { SavedSearch } from '../../saved_searches'; import { AppState, GetStateReturn } from '../angular/discover_state'; import { RequestAdapter } from '../../../../inspector/common'; +import { DiscoverServices } from '../../build_services'; export interface DiscoverProps { /** @@ -59,38 +60,10 @@ export interface DiscoverProps { * Increased when scrolling down */ minimumVisibleRows: number; - /** - * Function to add a column to state - */ - onAddColumn: (column: string) => void; - /** - * Function to add a filter to state - */ - onAddFilter: DocViewFilterFn; - /** - * Function to change the used time interval of the date histogram - */ - onChangeInterval: (interval: string) => void; - /** - * Function to move a given column to a given index, used in legacy table - */ - onMoveColumn: (columns: string, newIdx: number) => void; - /** - * Function to remove a given column from state - */ - onRemoveColumn: (column: string) => void; - /** - * Function to replace columns in state - */ - onSetColumns: (columns: string[]) => void; /** * Function to scroll down the legacy table to the bottom */ onSkipBottomButtonClick: () => void; - /** - * Function to change sorting of the table, triggers a fetch - */ - onSort: (sort: string[][]) => void; opts: { /** * Date histogram aggregation config @@ -108,10 +81,6 @@ export interface DiscoverProps { * Use angular router for navigation */ navigateTo: () => void; - /** - * Functions to get/mutate state - */ - stateContainer: GetStateReturn; /** * Inspect, for analyzing requests and responses */ @@ -128,6 +97,10 @@ export interface DiscoverProps { * List of available index patterns */ indexPatternList: Array>; + /** + * Kibana core services used by discover + */ + services: DiscoverServices; /** * The number of documents that can be displayed in the table/grid */ @@ -140,6 +113,10 @@ export interface DiscoverProps { * Function to set the header menu */ setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; + /** + * Functions for retrieving/mutating state + */ + stateContainer: GetStateReturn; /** * Timefield of the currently used index pattern */ @@ -165,18 +142,10 @@ export interface DiscoverProps { * Instance of SearchSource, the high level search API */ searchSource: ISearchSource; - /** - * Function to change the current index pattern - */ - setIndexPattern: (id: string) => void; /** * Current app state of URL */ state: AppState; - /** - * Function to update the time filter - */ - timefilterUpdateHandler: (ranges: { from: number; to: number }) => void; /** * Currently selected time range */ @@ -185,10 +154,6 @@ export interface DiscoverProps { * Function to update the actual query */ updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; - /** - * Function to update the actual savedQuery id - */ - updateSavedQueryId: (savedQueryId?: string) => void; /** * An object containing properties for proper handling of unmapped fields in the UI */ diff --git a/src/plugins/discover/public/application/helpers/popularize_field.ts b/src/plugins/discover/public/application/helpers/popularize_field.ts index b97b6f46600ae..4ade7d1768419 100644 --- a/src/plugins/discover/public/application/helpers/popularize_field.ts +++ b/src/plugins/discover/public/application/helpers/popularize_field.ts @@ -6,12 +6,12 @@ * Side Public License, v 1. */ -import { IndexPattern, IndexPatternsService } from '../../../../data/public'; +import { IndexPattern, IndexPatternsContract } from '../../../../data/public'; async function popularizeField( indexPattern: IndexPattern, fieldName: string, - indexPatternsService: IndexPatternsService + indexPatternsService: IndexPatternsContract ) { if (!indexPattern.id) return; const field = indexPattern.fields.getByName(fieldName); From ea96eeccb45d4fd944a62ab8a6a09e0b9f6e5b52 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Mon, 8 Feb 2021 17:44:35 +0300 Subject: [PATCH 42/44] [VEGA] src/plugins/vis_type_vega/public/lib/vega.js should be removed (#89861) * [VEGA] src/plugins/vis_type_vega/public/lib/vega.js should be removed * remove leaflet dependency * fix CI * cleanup vega-scenagraph Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 2 +- src/plugins/maps_legacy/public/leaflet.js | 1 - .../vis_type_vega/public/data_model/types.ts | 35 +++++++------ .../public/data_model/vega_parser.test.js | 5 -- .../public/data_model/vega_parser.ts | 13 ++--- src/plugins/vis_type_vega/public/lib/vega.js | 12 ----- src/plugins/vis_type_vega/public/vega_fn.ts | 2 +- src/plugins/vis_type_vega/public/vega_type.ts | 3 +- .../public/vega_view/vega_base_view.js | 29 +++++------ .../vega_view/vega_map_view/constants.ts | 3 +- .../vega_map_view/layers/vega_layer.test.ts | 5 +- .../vega_map_view/layers/vega_layer.ts | 8 ++- .../vega_map_view/utils/vsi_helper.ts | 5 +- .../vega_view/vega_map_view/view.test.ts | 12 ++--- .../public/vega_view/vega_map_view/view.ts | 14 +++--- .../public/vega_view/vega_view.js | 4 +- .../public/vega_visualization.test.js | 5 -- src/plugins/vis_type_vega/server/types.ts | 3 +- .../register_vega_collector.test.ts | 3 +- src/plugins/vis_type_vega/tsconfig.json | 3 +- yarn.lock | 50 ++----------------- 21 files changed, 75 insertions(+), 142 deletions(-) delete mode 100644 src/plugins/vis_type_vega/public/lib/vega.js diff --git a/package.json b/package.json index a5c6fa6f7b3c2..b224f0c1ae0d5 100644 --- a/package.json +++ b/package.json @@ -714,7 +714,6 @@ "leaflet": "1.5.1", "leaflet-draw": "0.4.14", "leaflet-responsive-popup": "0.6.4", - "leaflet-vega": "^0.8.6", "leaflet.heat": "0.2.0", "less": "npm:@elastic/less@2.7.3-kibana", "license-checker": "^16.0.0", @@ -833,6 +832,7 @@ "val-loader": "^1.1.1", "vega": "^5.19.1", "vega-lite": "^4.17.0", + "vega-spec-injector": "^0.0.2", "vega-schema-url-parser": "^2.1.0", "vega-tooltip": "^0.25.0", "venn.js": "0.2.20", diff --git a/src/plugins/maps_legacy/public/leaflet.js b/src/plugins/maps_legacy/public/leaflet.js index 69531013abae4..fd02f83d72823 100644 --- a/src/plugins/maps_legacy/public/leaflet.js +++ b/src/plugins/maps_legacy/public/leaflet.js @@ -12,7 +12,6 @@ if (!window.hasOwnProperty('L')) { window.L.Browser.touch = false; window.L.Browser.pointer = false; - require('leaflet-vega'); require('leaflet.heat/dist/leaflet-heat.js'); require('leaflet-draw/dist/leaflet.draw.css'); require('leaflet-draw/dist/leaflet.draw.js'); diff --git a/src/plugins/vis_type_vega/public/data_model/types.ts b/src/plugins/vis_type_vega/public/data_model/types.ts index 8d6a8227203d2..042ffac583e98 100644 --- a/src/plugins/vis_type_vega/public/data_model/types.ts +++ b/src/plugins/vis_type_vega/public/data_model/types.ts @@ -10,6 +10,8 @@ import { SearchResponse, SearchParams } from 'elasticsearch'; import { Filter } from 'src/plugins/data/public'; import { DslQuery } from 'src/plugins/data/common'; +import { Assign } from '@kbn/utility-types'; +import { Spec } from 'vega'; import { EsQueryParser } from './es_query_parser'; import { EmsFileParser } from './ems_file_parser'; import { UrlParser } from './url_parser'; @@ -93,21 +95,24 @@ export interface KibanaConfig { renderer: Renderer; } -export interface VegaSpec { - [index: string]: any; - $schema: string; - data?: Data; - encoding?: Encoding; - mark?: string; - title?: string; - autosize?: AutoSize; - projections?: Projection[]; - width?: number | 'container'; - height?: number | 'container'; - padding?: number | Padding; - _hostConfig?: KibanaConfig; - config: VegaSpecConfig; -} +export type VegaSpec = Assign< + Spec, + { + [index: string]: any; + $schema: string; + data?: Data; + encoding?: Encoding; + mark?: string; + title?: string; + autosize?: AutoSize; + projections?: Projection[]; + width?: number | 'container'; + height?: number | 'container'; + padding?: number | Padding; + _hostConfig?: KibanaConfig; + config: VegaSpecConfig; + } +>; export enum CONSTANTS { TIMEFILTER = '%timefilter%', diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js index eeacec0834ea6..1948792d55a83 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js @@ -13,11 +13,6 @@ import { bypassExternalUrlCheck } from '../vega_view/vega_base_view'; jest.mock('../services'); -jest.mock('../lib/vega', () => ({ - vega: jest.requireActual('vega'), - vegaLite: jest.requireActual('vega-lite'), -})); - describe(`VegaParser.parseAsync`, () => { test(`should throw an error in case of $spec is not defined`, async () => { const vp = new VegaParser('{}'); diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts index 667350b693a54..e97418581a42f 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts @@ -13,8 +13,9 @@ import hjson from 'hjson'; import { euiPaletteColorBlind } from '@elastic/eui'; import { euiThemeVars } from '@kbn/ui-shared-deps/theme'; import { i18n } from '@kbn/i18n'; -// @ts-ignore -import { vega, vegaLite } from '../lib/vega'; + +import { logger, Warn, version as vegaVersion } from 'vega'; +import { compile, TopLevelSpec, version as vegaLiteVersion } from 'vega-lite'; import { EsQueryParser } from './es_query_parser'; import { Utils } from './utils'; import { EmsFileParser } from './ems_file_parser'; @@ -235,9 +236,9 @@ The URL is an identifier only. Kibana and your browser will never access this UR */ private _compileVegaLite() { this.vlspec = this.spec; - const logger = vega.logger(vega.Warn); // note: eslint has a false positive here - logger.warn = this._onWarning.bind(this); - this.spec = vegaLite.compile(this.vlspec, logger).spec; + const vegaLogger = logger(Warn); // note: eslint has a false positive here + vegaLogger.warn = this._onWarning.bind(this); + this.spec = compile(this.vlspec as TopLevelSpec, { logger: vegaLogger }).spec; // When using VL with the type=map and user did not provid their own projection settings, // remove the default projection that was generated by VegaLite compiler. @@ -534,7 +535,7 @@ The URL is an identifier only. Kibana and your browser will never access this UR private parseSchema(spec: VegaSpec) { const schema = schemaParser(spec.$schema); const isVegaLite = schema.library === 'vega-lite'; - const libVersion = isVegaLite ? vegaLite.version : vega.version; + const libVersion = isVegaLite ? vegaLiteVersion : vegaVersion; if (versionCompare(schema.version, libVersion) > 0) { this._onWarning( diff --git a/src/plugins/vis_type_vega/public/lib/vega.js b/src/plugins/vis_type_vega/public/lib/vega.js deleted file mode 100644 index b7c59fce6dec2..0000000000000 --- a/src/plugins/vis_type_vega/public/lib/vega.js +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as vegaLite from 'vega-lite/build-es5/vega-lite'; -import * as vega from 'vega/build-es5/vega'; - -export { vega, vegaLite }; diff --git a/src/plugins/vis_type_vega/public/vega_fn.ts b/src/plugins/vis_type_vega/public/vega_fn.ts index fb36a0097c970..76479cbcdf1ec 100644 --- a/src/plugins/vis_type_vega/public/vega_fn.ts +++ b/src/plugins/vis_type_vega/public/vega_fn.ts @@ -16,7 +16,7 @@ import { VegaInspectorAdapters } from './vega_inspector/index'; import { KibanaContext, TimeRange, Query } from '../../data/public'; import { VegaParser } from './data_model/vega_parser'; -type Input = KibanaContext | null; +type Input = KibanaContext | { type: 'null' }; type Output = Promise>; interface Arguments { diff --git a/src/plugins/vis_type_vega/public/vega_type.ts b/src/plugins/vis_type_vega/public/vega_type.ts index 54d4cf16f0cde..902f79d03e680 100644 --- a/src/plugins/vis_type_vega/public/vega_type.ts +++ b/src/plugins/vis_type_vega/public/vega_type.ts @@ -19,7 +19,6 @@ import { toExpressionAst } from './to_ast'; import { getInfoMessage } from './components/experimental_map_vis_info'; import { VegaVisEditorComponent } from './components/vega_vis_editor_lazy'; -import type { VegaSpec } from './data_model/types'; import type { VisParams } from './vega_fn'; export const createVegaTypeDefinition = (): VisTypeDefinition => { @@ -58,7 +57,7 @@ export const createVegaTypeDefinition = (): VisTypeDefinition => { try { const spec = parse(visParams.spec, { legacyRoot: false, keepWsc: true }); - return extractIndexPatternsFromSpec(spec as VegaSpec); + return extractIndexPatternsFromSpec(spec); } catch (e) { // spec is invalid } diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index 2ef687594ce06..d9b1b536a6d17 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -9,7 +9,8 @@ import $ from 'jquery'; import moment from 'moment'; import dateMath from '@elastic/datemath'; -import { vega, vegaLite } from '../lib/vega'; +import { scheme, loader, logger, Warn, version as vegaVersion, expressionFunction } from 'vega'; +import { version as vegaLiteVersion } from 'vega-lite'; import { Utils } from '../data_model/utils'; import { euiPaletteColorBlind } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -19,7 +20,7 @@ import { esFilters } from '../../../data/public'; import { getEnableExternalUrls, getData } from '../services'; import { extractIndexPatternsFromSpec } from '../lib/extract_index_pattern'; -vega.scheme('elastic', euiPaletteColorBlind()); +scheme('elastic', euiPaletteColorBlind()); // Vega's extension functions are global. When called, // we forward execution to the instance-specific handler @@ -32,8 +33,8 @@ const vegaFunctions = { }; for (const funcName of Object.keys(vegaFunctions)) { - if (!vega.expressionFunction(funcName)) { - vega.expressionFunction(funcName, function handlerFwd(...args) { + if (!expressionFunction(funcName)) { + expressionFunction(funcName, function handlerFwd(...args) { const view = this.context.dataflow; view.runAfter(() => view._kibanaView.vegaFunctionsHandler(funcName, ...args)); }); @@ -164,9 +165,9 @@ export class VegaBaseView { }; // Override URL sanitizer to prevent external data loading (if disabled) - const loader = vega.loader(); - const originalSanitize = loader.sanitize.bind(loader); - loader.sanitize = (uri, options) => { + const vegaLoader = loader(); + const originalSanitize = vegaLoader.sanitize.bind(vegaLoader); + vegaLoader.sanitize = (uri, options) => { if (uri.bypassToken === bypassToken) { // If uri has a bypass token, the uri was encoded by bypassExternalUrlCheck() above. // because user can only supply pure JSON data structure. @@ -185,14 +186,14 @@ export class VegaBaseView { } return originalSanitize(uri, options); }; - config.loader = loader; + config.loader = vegaLoader; - const logger = vega.logger(vega.Warn); + const vegaLogger = logger(Warn); - logger.warn = this.onWarn.bind(this); - logger.error = this.onError.bind(this); + vegaLogger.warn = this.onWarn.bind(this); + vegaLogger.error = this.onError.bind(this); - config.logger = logger; + config.logger = vegaLogger; return config; } @@ -430,8 +431,8 @@ export class VegaBaseView { } const debugObj = {}; window.VEGA_DEBUG = debugObj; - window.VEGA_DEBUG.VEGA_VERSION = vega.version; - window.VEGA_DEBUG.VEGA_LITE_VERSION = vegaLite.version; + window.VEGA_DEBUG.VEGA_VERSION = vegaVersion; + window.VEGA_DEBUG.VEGA_LITE_VERSION = vegaLiteVersion; window.VEGA_DEBUG.view = view; window.VEGA_DEBUG.vega_spec = spec; window.VEGA_DEBUG.vegalite_spec = vlspec; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts index f200d27e1b967..3dc245f196774 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { Style } from 'mapbox-gl'; import { TMS_IN_YML_ID } from '../../../../maps_legacy/public'; export const vegaLayerId = 'vega'; @@ -16,7 +17,7 @@ export const defaultMapConfig = { tileSize: 256, }; -export const defaultMabBoxStyle = { +export const defaultMabBoxStyle: Style = { /** * according to the MapBox documentation that value should be '8' * @see (https://docs.mapbox.com/mapbox-gl-js/style-spec/root/#version) diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.test.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.test.ts index 963c2bd03f415..da4c14c77bc98 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.test.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.test.ts @@ -7,6 +7,7 @@ */ import { initVegaLayer } from './vega_layer'; +import type { View } from 'vega'; type InitVegaLayerParams = Parameters[0]; @@ -32,9 +33,9 @@ describe('vega_map_view/tms_raster_layer', () => { addLayer: jest.fn(), } as unknown) as MapType; context = { - vegaView: { + vegaView: ({ initialize: jest.fn(), - }, + } as unknown) as View, updateVegaView: jest.fn(), }; }); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.ts index 884e948e2aea3..a3efba804b454 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.ts @@ -7,14 +7,12 @@ */ import type { Map, CustomLayerInterface } from 'mapbox-gl'; +import type { View } from 'vega'; import type { LayerParameters } from './types'; -// @ts-ignore -import { vega } from '../../lib/vega'; - export interface VegaLayerContext { - vegaView: vega.View; - updateVegaView: (map: Map, view: vega.View) => void; + vegaView: View; + updateVegaView: (map: Map, view: View) => void; } export function initVegaLayer({ diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.ts index 29c8d33cf3967..2085e250045f6 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.ts @@ -7,13 +7,12 @@ */ // @ts-expect-error -// eslint-disable-next-line import/no-extraneous-dependencies import Vsi from 'vega-spec-injector'; -import { VegaSpec } from '../../../data_model/types'; +import { Spec } from 'vega'; import { defaultProjection } from '../constants'; -export const injectMapPropsIntoSpec = (spec: VegaSpec) => { +export const injectMapPropsIntoSpec = (spec: Spec) => { const vsi = new Vsi(); vsi.overrideField(spec, 'autosize', 'none'); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts index b59e1c65ab3f8..21c18e15c242c 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts @@ -28,11 +28,8 @@ import { setMapServiceSettings, setUISettings, } from '../../services'; - -jest.mock('../../lib/vega', () => ({ - vega: jest.requireActual('vega'), - vegaLite: jest.requireActual('vega-lite'), -})); +import { initVegaLayer, initTmsRasterLayer } from './layers'; +import { Map, NavigationControl, Style } from 'mapbox-gl'; jest.mock('mapbox-gl', () => ({ Map: jest.fn().mockImplementation(() => ({ @@ -55,9 +52,6 @@ jest.mock('./layers', () => ({ initTmsRasterLayer: jest.fn(), })); -import { initVegaLayer, initTmsRasterLayer } from './layers'; -import { Map, NavigationControl } from 'mapbox-gl'; - describe('vega_map_view/view', () => { describe('VegaMapView', () => { const coreStart = coreMock.createStart(); @@ -76,7 +70,7 @@ describe('vega_map_view/view', () => { setUISettings(coreStart.uiSettings); const getTmsService = jest.fn().mockReturnValue(({ - getVectorStyleSheet: () => ({ + getVectorStyleSheet: (): Style => ({ version: 8, sources: {}, layers: [], diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts index 1cdc3af733589..4c155d6b5ea88 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import { Map, Style, NavigationControl, MapboxOptions } from 'mapbox-gl'; +import { View, parse } from 'vega'; import { initTmsRasterLayer, initVegaLayer } from './layers'; import { VegaBaseView } from '../vega_base_view'; import { getMapServiceSettings } from '../../services'; @@ -24,12 +25,9 @@ import { import { validateZoomSettings, injectMapPropsIntoSpec } from './utils'; -// @ts-expect-error -import { vega } from '../../lib/vega'; - import './vega_map_view.scss'; -async function updateVegaView(mapBoxInstance: Map, vegaView: vega.View) { +async function updateVegaView(mapBoxInstance: Map, vegaView: View) { const mapCanvas = mapBoxInstance.getCanvas(); const { lat, lng } = mapBoxInstance.getCenter(); let shouldRender = false; @@ -77,7 +75,7 @@ export class VegaMapView extends VegaBaseView { }; } - private async initMapContainer(vegaView: vega.View) { + private async initMapContainer(vegaView: View) { let style: Style = defaultMabBoxStyle; let customAttribution: MapboxOptions['customAttribution'] = []; const zoomSettings = { @@ -139,7 +137,7 @@ export class VegaMapView extends VegaBaseView { } } - private initLayers(mapBoxInstance: Map, vegaView: vega.View) { + private initLayers(mapBoxInstance: Map, vegaView: View) { const shouldShowUserConfiguredLayer = this.mapStyle === userConfiguredLayerId; if (shouldShowUserConfiguredLayer) { @@ -168,8 +166,8 @@ export class VegaMapView extends VegaBaseView { } protected async _initViewCustomizations() { - const vegaView = new vega.View( - vega.parse(injectMapPropsIntoSpec(this._parser.spec)), + const vegaView = new View( + parse(injectMapPropsIntoSpec(this._parser.spec)), this._vegaViewConfig ); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_view.js index 5d5f3ed3d3733..5b1e49a73343b 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_view.js @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { vega } from '../lib/vega'; +import { View, parse } from 'vega'; import { VegaBaseView } from './vega_base_view'; export class VegaView extends VegaBaseView { @@ -14,7 +14,7 @@ export class VegaView extends VegaBaseView { // In some cases, Vega may be initialized twice... TBD if (!this._$container) return; - const view = new vega.View(vega.parse(this._parser.spec), this._vegaViewConfig); + const view = new View(parse(this._parser.spec), this._vegaViewConfig); if (this._parser.useResize) this.updateVegaSize(view); view.initialize(this._$container.get(0), this._$controls.get(0)); diff --git a/src/plugins/vis_type_vega/public/vega_visualization.test.js b/src/plugins/vis_type_vega/public/vega_visualization.test.js index a55d5c4423f0e..776f8898b3e3a 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.test.js +++ b/src/plugins/vis_type_vega/public/vega_visualization.test.js @@ -26,11 +26,6 @@ jest.mock('./default_spec', () => ({ getDefaultSpec: () => jest.requireActual('./test_utils/default.spec.json'), })); -jest.mock('./lib/vega', () => ({ - vega: jest.requireActual('vega'), - vegaLite: jest.requireActual('vega-lite'), -})); - // FLAKY: https://github.com/elastic/kibana/issues/71713 describe('VegaVisualizations', () => { let domNode; diff --git a/src/plugins/vis_type_vega/server/types.ts b/src/plugins/vis_type_vega/server/types.ts index f1e97416d7665..affd93dedb8ca 100644 --- a/src/plugins/vis_type_vega/server/types.ts +++ b/src/plugins/vis_type_vega/server/types.ts @@ -7,10 +7,11 @@ */ import { Observable } from 'rxjs'; +import { SharedGlobalConfig } from 'kibana/server'; import { HomeServerPluginSetup } from '../../home/server'; import { UsageCollectionSetup } from '../../usage_collection/server'; -export type ConfigObservable = Observable<{ kibana: { index: string } }>; +export type ConfigObservable = Observable; export interface VegaSavedObjectAttributes { title: string; diff --git a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts index b3abc46070159..9db1b7657f444 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts @@ -12,11 +12,12 @@ import { createUsageCollectionSetupMock } from 'src/plugins/usage_collection/ser import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; import { HomeServerPluginSetup } from '../../../home/server'; import { registerVegaUsageCollector } from './register_vega_collector'; +import { ConfigObservable } from '../types'; describe('registerVegaUsageCollector', () => { const mockIndex = 'mock_index'; const mockDeps = { home: ({} as unknown) as HomeServerPluginSetup }; - const mockConfig = of({ kibana: { index: mockIndex } }); + const mockConfig = of({ kibana: { index: mockIndex } }) as ConfigObservable; it('makes a usage collector and registers it`', () => { const mockCollectorSet = createUsageCollectionSetupMock(); diff --git a/src/plugins/vis_type_vega/tsconfig.json b/src/plugins/vis_type_vega/tsconfig.json index c013056ba4566..d03ee6eae790e 100644 --- a/src/plugins/vis_type_vega/tsconfig.json +++ b/src/plugins/vis_type_vega/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "strictNullChecks": false }, "include": [ "server/**/*", diff --git a/yarn.lock b/yarn.lock index fa7ebacb1cd70..6df258e9715b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19427,13 +19427,6 @@ leaflet-responsive-popup@0.6.4: resolved "https://registry.yarnpkg.com/leaflet-responsive-popup/-/leaflet-responsive-popup-0.6.4.tgz#b93d9368ef9f96d6dc911cf5b96d90e08601c6b3" integrity sha512-2D8G9aQA6NHkulDBPN9kqbUCkCpWQQ6dF0xFL11AuEIWIbsL4UC/ZPP5m8GYM0dpU6YTlmyyCh1Tz+cls5Q4dg== -leaflet-vega@^0.8.6: - version "0.8.6" - resolved "https://registry.yarnpkg.com/leaflet-vega/-/leaflet-vega-0.8.6.tgz#dd4090a6123cb983c2b732d53ec9e4daa53736b2" - integrity sha1-3UCQphI8uYPCtzLVPsnk2qU3NrI= - dependencies: - vega-spec-injector "^0.0.2" - leaflet.heat@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/leaflet.heat/-/leaflet.heat-0.2.0.tgz#109d8cf586f0adee41f05aff031e27a77fecc229" @@ -29574,7 +29567,7 @@ vega-event-selector@^2.0.6, vega-event-selector@~2.0.6: resolved "https://registry.yarnpkg.com/vega-event-selector/-/vega-event-selector-2.0.6.tgz#6beb00e066b78371dde1a0f40cb5e0bbaecfd8bc" integrity sha512-UwCu50Sqd8kNZ1X/XgiAY+QAyQUmGFAwyDu7y0T5fs6/TPQnDo/Bo346NgSgINBEhEKOAMY1Nd/rPOk4UEm/ew== -vega-expression@^4.0.0, vega-expression@^4.0.1, vega-expression@~4.0.1: +vega-expression@^4.0.1, vega-expression@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/vega-expression/-/vega-expression-4.0.1.tgz#c03e4fc68a00acac49557faa4e4ed6ac8a59c5fd" integrity sha512-ZrDj0hP8NmrCpdLFf7Rd/xMUHGoSYsAOTaYp7uXZ2dkEH5x0uPy5laECMc8TiQvL8W+8IrN2HAWCMRthTSRe2Q== @@ -29608,24 +29601,7 @@ vega-format@^1.0.4, vega-format@~1.0.4: vega-time "^2.0.3" vega-util "^1.15.2" -vega-functions@^5.10.0: - version "5.10.0" - resolved "https://registry.yarnpkg.com/vega-functions/-/vega-functions-5.10.0.tgz#3d384111f13b3b0dd38a4fca656c5ae54b66e158" - integrity sha512-1l28OxUwOj8FEvRU62Oz2hiTuDECrvx1DPU1qLebBKhlgaKbcCk3XyHrn1kUzhMKpXq+SFv5VPxchZP47ASSvQ== - dependencies: - d3-array "^2.7.1" - d3-color "^2.0.0" - d3-geo "^2.0.1" - vega-dataflow "^5.7.3" - vega-expression "^4.0.1" - vega-scale "^7.1.1" - vega-scenegraph "^4.9.2" - vega-selections "^5.1.5" - vega-statistics "^1.7.9" - vega-time "^2.0.4" - vega-util "^1.16.0" - -vega-functions@^5.12.0, vega-functions@~5.12.0: +vega-functions@^5.10.0, vega-functions@^5.12.0, vega-functions@~5.12.0: version "5.12.0" resolved "https://registry.yarnpkg.com/vega-functions/-/vega-functions-5.12.0.tgz#44bf08a7b20673dc8cf51d6781c8ea1399501668" integrity sha512-3hljmGs+gR7TbO/yYuvAP9P5laKISf1GKk4yRHLNdM61fWgKm8pI3f6LY2Hvq9cHQFTiJ3/5/Bx2p1SX5R4quQ== @@ -29752,19 +29728,7 @@ vega-scale@^7.0.3, vega-scale@^7.1.1, vega-scale@~7.1.1: vega-time "^2.0.4" vega-util "^1.15.2" -vega-scenegraph@^4.9.2: - version "4.9.2" - resolved "https://registry.yarnpkg.com/vega-scenegraph/-/vega-scenegraph-4.9.2.tgz#83b1dbc34a9ab5595c74d547d6d95849d74451ed" - integrity sha512-epm1CxcB8AucXQlSDeFnmzy0FCj+HV2k9R6ch2lfLRln5lPLEfgJWgFcFhVf5jyheY0FSeHH52Q5zQn1vYI1Ow== - dependencies: - d3-path "^2.0.0" - d3-shape "^2.0.0" - vega-canvas "^1.2.5" - vega-loader "^4.3.3" - vega-scale "^7.1.1" - vega-util "^1.15.2" - -vega-scenegraph@^4.9.3, vega-scenegraph@~4.9.3: +vega-scenegraph@^4.9.2, vega-scenegraph@^4.9.3, vega-scenegraph@~4.9.3: version "4.9.3" resolved "https://registry.yarnpkg.com/vega-scenegraph/-/vega-scenegraph-4.9.3.tgz#c4720550ea7ff5c8d9d0690f47fe2640547cfc6b" integrity sha512-lBvqLbXqrqRCTGJmSgzZC/tLR/o+TXfakbdhDzNdpgTavTaQ65S/67Gpj5hPpi77DvsfZUIY9lCEeO37aJhy0Q== @@ -29781,14 +29745,6 @@ vega-schema-url-parser@^2.1.0: resolved "https://registry.yarnpkg.com/vega-schema-url-parser/-/vega-schema-url-parser-2.1.0.tgz#847f9cf9f1624f36f8a51abc1adb41ebc6673cb4" integrity sha512-JHT1PfOyVzOohj89uNunLPirs05Nf59isPT5gnwIkJph96rRgTIBJE7l7yLqndd7fLjr3P8JXHGAryRp74sCaQ== -vega-selections@^5.1.5: - version "5.1.5" - resolved "https://registry.yarnpkg.com/vega-selections/-/vega-selections-5.1.5.tgz#c7662edf26c1cfb18623573b30590c9774348d1c" - integrity sha512-oRSsfkqYqA5xfEJqDpgnSDd+w0k6p6SGYisMD6rGXMxuPl0x0Uy6RvDr4nbEtB+dpWdoWEvgrsZVS6axyDNWvQ== - dependencies: - vega-expression "^4.0.0" - vega-util "^1.15.2" - vega-selections@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/vega-selections/-/vega-selections-5.3.0.tgz#810f2e7b7642fa836cf98b2e5dcc151093b1f6a7" From 5176aa6bc7b6b74dfc9fbe6b84d212e044fd43cd Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Mon, 8 Feb 2021 07:51:45 -0700 Subject: [PATCH 43/44] Update geo alerting docs to just cover geo containment (#90480) --- docs/user/alerting/geo-alert-types.asciidoc | 67 ++---------------- .../images/alert-types-tracking-select.png | Bin 37690 -> 30066 bytes 2 files changed, 7 insertions(+), 60 deletions(-) diff --git a/docs/user/alerting/geo-alert-types.asciidoc b/docs/user/alerting/geo-alert-types.asciidoc index f79885e3bc716..d9073ecca1145 100644 --- a/docs/user/alerting/geo-alert-types.asciidoc +++ b/docs/user/alerting/geo-alert-types.asciidoc @@ -1,19 +1,16 @@ [role="xpack"] -[[geo-alert-types]] -== Geo alert types +[[geo-alerting]] +== Geo alerting -Two additional stack alerts are available: -<> and <>. +Alerting now includes one additional stack alert: <>. As with other stack alerts, you need `all` access to the *Stack Alerts* feature -to be able to create and edit either of the geo alerts. +to be able to create and edit a geo alert. See <> for more information on configuring roles that provide access to this feature. [float] -=== Geo alert requirements - -To create either a *Tracking threshold* or a *Tracking containment* alert, the -following requirements must be present: +=== Geo alerting requirements +To create a *Tracking containment* alert, the following requirements must be present: - *Tracks index or index pattern*: An index containing a `geo_point` field, `date` field, and some form of entity identifier. An entity identifier is a `keyword` or `number` @@ -33,62 +30,12 @@ than the current time minus the amount of the interval. If data older than [float] === Creating a geo alert -Both *threshold* and *containment* alerts can be created by clicking the *Create* -button in the <>. +Click the *Create* button in the <>. Complete the <>. -Select <> to generate an alert when an entity crosses a boundary, and you desire the -ability to highlight lines of crossing on a custom map. -Select -<> if an entity should send out constant alerts -while contained within a boundary (this feature is optional) or if the alert is generally -just more focused around activity when an entity exists within a shape. [role="screenshot"] image::images/alert-types-tracking-select.png[Choosing a tracking alert type] -[NOTE] -================================================== -With recent advances in the alerting framework, most of the features -available in Tracking threshold alerts can be replicated with just -a little more work in Tracking containment alerts. The capabilities of Tracking -threshold alerts may be deprecated or folded into Tracking containment alerts -in the future. -================================================== - -[float] -[[alert-type-tracking-threshold]] -=== Tracking threshold -The Tracking threshold alert type runs an {es} query over indices, comparing the latest -entity locations with their previous locations. In the event that an entity has crossed a -boundary from the selected boundary index, an alert may be generated. - -[float] -==== Defining the conditions -Tracking threshold has a *Delayed evaluation offset* and 4 clauses that define the -condition to detect, as well as 2 Kuery bars used to provide additional filtering -context for each of the indices. - -[role="screenshot"] -image::images/alert-types-tracking-threshold-conditions.png[Five clauses define the condition to detect] - - -Delayed evaluation offset:: If a data source lags or is intermittent, you may supply -an optional value to evaluate alert conditions following a fixed delay. For instance, if data -is consistently indexed 5-10 minutes following its original timestamp, a *Delayed evaluation -offset* of `10 minutes` would ensure that alertable instances are still captured. -Index (entity):: This clause requires an *index or index pattern*, a *time field* that will be used for the *time window*, and a *`geo_point` field* for tracking. -By:: This clause specifies the field to use in the previously provided -*index or index pattern* for tracking Entities. An entity is a `keyword` -or `number` field that consistently identifies the entity to be tracked. -When entity:: This clause specifies which crossing option to track. The values -*Entered*, *Exited*, and *Crossed* can be selected to indicate which crossing conditions -should trigger an alert. *Entered* alerts on entry into a boundary, *Exited* alerts on exit -from a boundary, and *Crossed* alerts on all boundary crossings whether they be entrances -or exits. -Index (Boundary):: This clause requires an *index or index pattern*, a *`geo_shape` field* -identifying boundaries, and an optional *Human-readable boundary name* for better alerting -messages. - [float] [[alert-type-tracking-containment]] === Tracking containment diff --git a/docs/user/alerting/images/alert-types-tracking-select.png b/docs/user/alerting/images/alert-types-tracking-select.png index 445a5202ffd0c2c9b809d95e7d9c4d6d8a8723ca..44fcf1a2600b8e287594ef13b0bffd274994b88c 100644 GIT binary patch literal 30066 zcmdRW1yq#ryXIJcBCT{NAV@a|Lx>EZq=105NO$KDD$*t0Al=;!(jXw+3|#|5ckR#r z|L*SDy}S3^yLZo?vmQJ!@O|I>`hDK#dEW7VFDHrhi1ZNz0>OIs7N!V+-2DiF+&RFw z3w~2$MxPA6J+ywSW(R>_xBvZrCyEK13<7xuc?T2y;Hau#)-*916@&wqM^7eJmv|6^WaxwRWjzuhj(+auyb{yDhSF zpZI+!e2A(;G|m1tfA)#k2fXK*RDcHfwm@+IIRvst`VAKXamEtGfIu32p52E)w8d~i z1B;(y+<|uL5|KSvtDHtJpK?mZ9U)# zJnn0Pa)iPz7j757T`<=M!y?^L zGjy$IWIZ`hg?zF)N{KD>AQ>+@I@*;*Yp628Y@zh$$_kOVc*pL1W4+Dc_-{4TKvG?! z%b^eawK)7>WpmnL9vz!}3HvIHRlA|VG0ExUnF9*U)zv-p6N_ts2mU!=REjg7MTuZE z@?D&tThXO`y?3GYii-S}`DA{MtS^QoSG{s8J$ABHOcK1WZRi~^bjwjp7&=XEm5y_0 z)CZcctj^0`tQlrKdi02=q5=(jgUEinyJGk5Y0Ol??3p*tRv5To)C&FauIlPzDMIkIO8fK7z8D;eNhT&H zX_#=+OX|Bk zZvKLYr_f8!$U@6qr@8-eo4(OHx5d0var97TU#hv7goL3`AKi4_1re+EXQr;ul+p1q zC)8;0oIx)KOILqFbue<&Vz|OUQIS)l*74e>s>q{%x^s8lfGY*%hTb)>ozvCu0Nn0= ziSt>K#AmA9sTs6Ie9q^=1MkOeHSpg?x23+=R0d@2h6A<}<6}BYm5EB|kS`*#wXDHe znVAG($;sGG3(&}@sAy*Gm-O^qC`w`ckMG~Jn{OcqmjJl_f}l| ze4ucz8{KK!e5MR*EKk{DI0Fw)2u?Vt(xkFhLC&Ji9T*tgU@csuQW$UEQI{E2Ru=yL zrHq{1O>m*khx^*ud3nDG(Xlrs>KyW&rlvse@tvlps&>Hf(0~bnc*fXj zks3U_f7fyoYC2xP;9_v#0lSYU6cTtCTmM>n5;{|ii z)I1~3ef;q8@!Ox$(6kM z#tmy|NQ{e9%an=&=K}4_M{q*Xq|MGYXw{$Eb_vgMBat-^AYYo_)_yB7iJ((0_DK

    T1+R=<4bOKr@3A)ZMu-l$DkJ-_}b6 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

    Z=b$?CE%Gp~U{yd9HNKQ^@G22Il7oCaCoLa&z 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 Date: Mon, 8 Feb 2021 08:59:45 -0600 Subject: [PATCH 44/44] TypeScript project references for infra plugin (#90118) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Felix Stürmer --- .../http_api/log_alerts/chart_preview_data.ts | 16 ++++- .../infra/common/http_api/shared/errors.ts | 41 +++++++++---- .../infra/common/inventory_models/types.ts | 2 +- .../use_metric_threshold_alert_prefill.ts | 2 +- .../public/components/document_title.tsx | 8 +-- .../public/components/eui/toolbar/toolbar.tsx | 18 ++++-- .../public/components/fixed_datepicker.tsx | 14 +++-- .../infra/public/components/toolbar_panel.ts | 17 ++++-- .../components/waffle/custom_field_panel.tsx | 4 +- .../inventory_view/components/waffle/node.tsx | 4 +- .../waffle/waffle_group_by_controls.tsx | 4 +- .../infra/public/utils/use_tracked_promise.ts | 8 +-- x-pack/plugins/infra/tsconfig.json | 36 +++++++++++ x-pack/test/tsconfig.json | 60 +++++++++---------- x-pack/tsconfig.json | 2 + x-pack/tsconfig.refs.json | 1 + 16 files changed, 163 insertions(+), 74 deletions(-) create mode 100644 x-pack/plugins/infra/tsconfig.json diff --git a/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts b/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts index 76533a476561b..e6baca305508e 100644 --- a/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts +++ b/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts @@ -41,7 +41,21 @@ export type GetLogAlertsChartPreviewDataSuccessResponsePayload = rt.TypeOf< typeof getLogAlertsChartPreviewDataSuccessResponsePayloadRT >; -export const getLogAlertsChartPreviewDataAlertParamsSubsetRT = rt.intersection([ +// This should not have an explicit `any` return type, but it's here because its +// inferred type includes `Comparator` which is a string enum exported from +// common/alerting/logs/log_threshold/types.ts. +// +// There's a bug that's fixed in TypeScript 4.2.0 that will allow us to remove +// the `:any` from this, so remove it when that update happens. +// +// If it's removed before then you get: +// +// x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts:44:14 - error TS4023: +// Exported variable 'getLogAlertsChartPreviewDataAlertParamsSubsetRT' has or is using name 'Comparator' +// from external module "/Users/smith/Code/kibana/x-pack/plugins/infra/common/alerting/logs/log_threshold/types" +// but cannot be named. +// +export const getLogAlertsChartPreviewDataAlertParamsSubsetRT: any = rt.intersection([ rt.type({ criteria: countCriteriaRT, timeUnit: timeUnitRT, diff --git a/x-pack/plugins/infra/common/http_api/shared/errors.ts b/x-pack/plugins/infra/common/http_api/shared/errors.ts index 5e439c31bbdc9..2b5461d71500e 100644 --- a/x-pack/plugins/infra/common/http_api/shared/errors.ts +++ b/x-pack/plugins/infra/common/http_api/shared/errors.ts @@ -7,18 +7,35 @@ import * as rt from 'io-ts'; -const createErrorRuntimeType = ( - statusCode: number, - errorCode: string, - attributes?: Attributes -) => +export const badRequestErrorRT = rt.intersection([ rt.type({ - statusCode: rt.literal(statusCode), - error: rt.literal(errorCode), + statusCode: rt.literal(400), + error: rt.literal('Bad Request'), message: rt.string, - ...(!!attributes ? { attributes } : {}), - }); + }), + rt.partial({ + attributes: rt.unknown, + }), +]); -export const badRequestErrorRT = createErrorRuntimeType(400, 'Bad Request'); -export const forbiddenErrorRT = createErrorRuntimeType(403, 'Forbidden'); -export const conflictErrorRT = createErrorRuntimeType(409, 'Conflict'); +export const forbiddenErrorRT = rt.intersection([ + rt.type({ + statusCode: rt.literal(403), + error: rt.literal('Forbidden'), + message: rt.string, + }), + rt.partial({ + attributes: rt.unknown, + }), +]); + +export const conflictErrorRT = rt.intersection([ + rt.type({ + statusCode: rt.literal(409), + error: rt.literal('Conflict'), + message: rt.string, + }), + rt.partial({ + attributes: rt.unknown, + }), +]); diff --git a/x-pack/plugins/infra/common/inventory_models/types.ts b/x-pack/plugins/infra/common/inventory_models/types.ts index 2d3b6a7c45d07..764f41966261c 100644 --- a/x-pack/plugins/infra/common/inventory_models/types.ts +++ b/x-pack/plugins/infra/common/inventory_models/types.ts @@ -286,7 +286,7 @@ export const ESTopHitsAggRT = rt.type({ top_hits: rt.object, }); -interface SnapshotTermsWithAggregation { +export interface SnapshotTermsWithAggregation { terms: { field: string }; aggregations: MetricsUIAggregation; } diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts index 3664be3b4903a..068c33ea2c31f 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts @@ -9,7 +9,7 @@ import { isEqual } from 'lodash'; import { useState } from 'react'; import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer'; -interface MetricThresholdPrefillOptions { +export interface MetricThresholdPrefillOptions { groupBy: string | string[] | undefined; filterQuery: string | undefined; metrics: MetricsExplorerMetric[]; diff --git a/x-pack/plugins/infra/public/components/document_title.tsx b/x-pack/plugins/infra/public/components/document_title.tsx index 9c3c89294f403..20e482d9df5b5 100644 --- a/x-pack/plugins/infra/public/components/document_title.tsx +++ b/x-pack/plugins/infra/public/components/document_title.tsx @@ -48,19 +48,19 @@ const wrapWithSharedState = () => { return null; } - private getTitle(title: TitleProp) { + public getTitle(title: TitleProp) { return typeof title === 'function' ? title(titles[this.state.index - 1]) : title; } - private pushTitle(title: string) { + public pushTitle(title: string) { titles[this.state.index] = title; } - private removeTitle() { + public removeTitle() { titles.pop(); } - private updateDocumentTitle() { + public updateDocumentTitle() { const title = (titles[titles.length - 1] || '') + TITLE_SUFFIX; if (title !== document.title) { document.title = title; diff --git a/x-pack/plugins/infra/public/components/eui/toolbar/toolbar.tsx b/x-pack/plugins/infra/public/components/eui/toolbar/toolbar.tsx index a2dd383695983..f1a793d11166c 100644 --- a/x-pack/plugins/infra/public/components/eui/toolbar/toolbar.tsx +++ b/x-pack/plugins/infra/public/components/eui/toolbar/toolbar.tsx @@ -6,13 +6,19 @@ */ import { EuiPanel } from '@elastic/eui'; +import { FunctionComponent } from 'react'; +import { StyledComponent } from 'styled-components'; +import { euiStyled, EuiTheme } from '../../../../../../../src/plugins/kibana_react/common'; -import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; - -export const Toolbar = euiStyled(EuiPanel).attrs(() => ({ - grow: false, - paddingSize: 'none', -}))` +// The return type of this component needs to be specified because the inferred +// return type depends on types that are not exported from EUI. You get a TS4023 +// error if the return type is not specified. +export const Toolbar: StyledComponent = euiStyled(EuiPanel).attrs( + () => ({ + grow: false, + paddingSize: 'none', + }) +)` border-top: none; border-right: none; border-left: none; diff --git a/x-pack/plugins/infra/public/components/fixed_datepicker.tsx b/x-pack/plugins/infra/public/components/fixed_datepicker.tsx index 62093dbfe53ec..dfaf0a490225a 100644 --- a/x-pack/plugins/infra/public/components/fixed_datepicker.tsx +++ b/x-pack/plugins/infra/public/components/fixed_datepicker.tsx @@ -5,12 +5,18 @@ * 2.0. */ -import React from 'react'; - import { EuiDatePicker, EuiDatePickerProps } from '@elastic/eui'; -import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; +import React, { FunctionComponent } from 'react'; +import { StyledComponent } from 'styled-components'; +import { euiStyled, EuiTheme } from '../../../../../src/plugins/kibana_react/common'; -export const FixedDatePicker = euiStyled( +// The return type of this component needs to be specified because the inferred +// return type depends on types that are not exported from EUI. You get a TS4023 +// error if the return type is not specified. +export const FixedDatePicker: StyledComponent< + FunctionComponent, + EuiTheme +> = euiStyled( ({ className, inputClassName, diff --git a/x-pack/plugins/infra/public/components/toolbar_panel.ts b/x-pack/plugins/infra/public/components/toolbar_panel.ts index 22352b97da0ea..d94e7faa0eabf 100644 --- a/x-pack/plugins/infra/public/components/toolbar_panel.ts +++ b/x-pack/plugins/infra/public/components/toolbar_panel.ts @@ -5,13 +5,20 @@ * 2.0. */ +import { FunctionComponent } from 'react'; import { EuiPanel } from '@elastic/eui'; -import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; +import { StyledComponent } from 'styled-components'; +import { EuiTheme, euiStyled } from '../../../../../src/plugins/kibana_react/common'; -export const ToolbarPanel = euiStyled(EuiPanel).attrs(() => ({ - grow: false, - paddingSize: 'none', -}))` +// The return type of this component needs to be specified because the inferred +// return type depends on types that are not exported from EUI. You get a TS4023 +// error if the return type is not specified. +export const ToolbarPanel: StyledComponent = euiStyled(EuiPanel).attrs( + () => ({ + grow: false, + paddingSize: 'none', + }) +)` border-top: none; border-right: none; border-left: none; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx index 8932388398b6a..acc6ae7af2727 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx @@ -27,7 +27,7 @@ const initialState = { type State = Readonly; -export const CustomFieldPanel = class extends React.PureComponent { +export class CustomFieldPanel extends React.PureComponent { public static displayName = 'CustomFieldPanel'; public readonly state: State = initialState; public render() { @@ -86,4 +86,4 @@ export const CustomFieldPanel = class extends React.PureComponent private handleFieldSelection = (selectedOptions: SelectedOption[]) => { this.setState({ selectedOptions }); }; -}; +} diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx index c76ff798b1286..d6934c6846b79 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx @@ -44,7 +44,7 @@ interface Props { currentTime: number; } -export const Node = class extends React.PureComponent { +export class Node extends React.PureComponent { public readonly state: State = initialState; public render() { const { nodeType, node, options, squareSize, bounds, formatter, currentTime } = this.props; @@ -164,7 +164,7 @@ export const Node = class extends React.PureComponent { this.setState({ isPopoverOpen: false }); } }; -}; +} const NodeContainer = euiStyled.div` position: relative; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx index 5c57ef11380e5..9f350610b1366 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx @@ -39,7 +39,7 @@ const initialState = { type State = Readonly; -export const WaffleGroupByControls = class extends React.PureComponent { +export class WaffleGroupByControls extends React.PureComponent { public static displayName = 'WaffleGroupByControls'; public readonly state: State = initialState; @@ -192,7 +192,7 @@ export const WaffleGroupByControls = class extends React.PureComponent( return [promiseState, execute] as [typeof promiseState, typeof execute]; }; -interface UninitializedPromiseState { +export interface UninitializedPromiseState { state: 'uninitialized'; } -interface PendingPromiseState { +export interface PendingPromiseState { state: 'pending'; promise: Promise; } -interface ResolvedPromiseState { +export interface ResolvedPromiseState { state: 'resolved'; promise: Promise; value: ResolvedValue; } -interface RejectedPromiseState { +export interface RejectedPromiseState { state: 'rejected'; promise: Promise; value: RejectedValue; diff --git a/x-pack/plugins/infra/tsconfig.json b/x-pack/plugins/infra/tsconfig.json new file mode 100644 index 0000000000000..a8a0e2c7119a9 --- /dev/null +++ b/x-pack/plugins/infra/tsconfig.json @@ -0,0 +1,36 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "../../typings/**/*", + "common/**/*", + "public/**/*", + "scripts/**/*", + "server/**/*", + "types/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../../../src/plugins/vis_type_timeseries/tsconfig.json" }, + { "path": "../data_enhanced/tsconfig.json" }, + { "path": "../alerts/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + { "path": "../license_management/tsconfig.json" }, + { "path": "../ml/tsconfig.json" }, + { "path": "../observability/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" }, + { "path": "../triggers_actions_ui/tsconfig.json" } + ] +} diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 10943b3a2929f..0a7a30f373e07 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -9,73 +9,73 @@ "exclude": ["../typings/jest.d.ts"], "references": [ { "path": "../../src/core/tsconfig.json" }, - { "path": "../../src/plugins/telemetry_management_section/tsconfig.json" }, - { "path": "../../src/plugins/management/tsconfig.json" }, { "path": "../../src/plugins/bfetch/tsconfig.json" }, { "path": "../../src/plugins/charts/tsconfig.json" }, { "path": "../../src/plugins/console/tsconfig.json" }, { "path": "../../src/plugins/dashboard/tsconfig.json" }, - { "path": "../../src/plugins/discover/tsconfig.json" }, { "path": "../../src/plugins/data/tsconfig.json" }, + { "path": "../../src/plugins/discover/tsconfig.json" }, { "path": "../../src/plugins/embeddable/tsconfig.json" }, { "path": "../../src/plugins/es_ui_shared/tsconfig.json" }, { "path": "../../src/plugins/expressions/tsconfig.json" }, { "path": "../../src/plugins/home/tsconfig.json" }, + { "path": "../../src/plugins/index_pattern_management/tsconfig.json" }, { "path": "../../src/plugins/kibana_overview/tsconfig.json" }, { "path": "../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../src/plugins/legacy_export/tsconfig.json" }, + { "path": "../../src/plugins/management/tsconfig.json" }, { "path": "../../src/plugins/navigation/tsconfig.json" }, { "path": "../../src/plugins/newsfeed/tsconfig.json" }, - { "path": "../../src/plugins/saved_objects/tsconfig.json" }, { "path": "../../src/plugins/saved_objects_management/tsconfig.json" }, { "path": "../../src/plugins/saved_objects_tagging_oss/tsconfig.json" }, + { "path": "../../src/plugins/saved_objects/tsconfig.json" }, { "path": "../../src/plugins/share/tsconfig.json" }, { "path": "../../src/plugins/telemetry_collection_manager/tsconfig.json" }, + { "path": "../../src/plugins/telemetry_management_section/tsconfig.json" }, { "path": "../../src/plugins/telemetry/tsconfig.json" }, - { "path": "../../src/plugins/usage_collection/tsconfig.json" }, { "path": "../../src/plugins/ui_actions/tsconfig.json" }, { "path": "../../src/plugins/url_forwarding/tsconfig.json" }, - { "path": "../../src/plugins/index_pattern_management/tsconfig.json" }, - + { "path": "../../src/plugins/usage_collection/tsconfig.json" }, { "path": "../plugins/actions/tsconfig.json" }, { "path": "../plugins/alerts/tsconfig.json" }, + { "path": "../plugins/beats_management/tsconfig.json" }, + { "path": "../plugins/cloud/tsconfig.json" }, { "path": "../plugins/code/tsconfig.json" }, { "path": "../plugins/console_extensions/tsconfig.json" }, - { "path": "../plugins/data_enhanced/tsconfig.json" }, { "path": "../plugins/dashboard_mode/tsconfig.json" }, - { "path": "../plugins/enterprise_search/tsconfig.json" }, - { "path": "../plugins/global_search/tsconfig.json" }, - { "path": "../plugins/global_search_providers/tsconfig.json" }, - { "path": "../plugins/features/tsconfig.json" }, + { "path": "../plugins/data_enhanced/tsconfig.json" }, { "path": "../plugins/embeddable_enhanced/tsconfig.json" }, + { "path": "../plugins/encrypted_saved_objects/tsconfig.json" }, + { "path": "../plugins/enterprise_search/tsconfig.json" }, { "path": "../plugins/event_log/tsconfig.json" }, - { "path": "../plugins/licensing/tsconfig.json" }, + { "path": "../plugins/features/tsconfig.json" }, + { "path": "../plugins/global_search_bar/tsconfig.json" }, + { "path": "../plugins/global_search_providers/tsconfig.json" }, + { "path": "../plugins/global_search/tsconfig.json" }, + { "path": "../plugins/grokdebugger/tsconfig.json" }, + { "path": "../plugins/index_management/tsconfig.json" }, + { "path": "../plugins/infra/tsconfig.json" }, + { "path": "../plugins/ingest_pipelines/tsconfig.json" }, { "path": "../plugins/lens/tsconfig.json" }, + { "path": "../plugins/license_management/tsconfig.json" }, + { "path": "../plugins/licensing/tsconfig.json" }, { "path": "../plugins/ml/tsconfig.json" }, + { "path": "../plugins/observability/tsconfig.json" }, + { "path": "../plugins/painless_lab/tsconfig.json" }, + { "path": "../plugins/runtime_fields/tsconfig.json" }, + { "path": "../plugins/saved_objects_tagging/tsconfig.json" }, + { "path": "../plugins/security/tsconfig.json" }, + { "path": "../plugins/snapshot_restore/tsconfig.json" }, + { "path": "../plugins/spaces/tsconfig.json" }, + { "path": "../plugins/stack_alerts/tsconfig.json" }, { "path": "../plugins/task_manager/tsconfig.json" }, { "path": "../plugins/telemetry_collection_xpack/tsconfig.json" }, { "path": "../plugins/transform/tsconfig.json" }, { "path": "../plugins/triggers_actions_ui/tsconfig.json" }, { "path": "../plugins/ui_actions_enhanced/tsconfig.json" }, - { "path": "../plugins/spaces/tsconfig.json" }, - { "path": "../plugins/security/tsconfig.json" }, - { "path": "../plugins/encrypted_saved_objects/tsconfig.json" }, - { "path": "../plugins/stack_alerts/tsconfig.json" }, - { "path": "../plugins/beats_management/tsconfig.json" }, - { "path": "../plugins/cloud/tsconfig.json" }, - { "path": "../plugins/saved_objects_tagging/tsconfig.json" }, - { "path": "../plugins/global_search_bar/tsconfig.json" }, - { "path": "../plugins/observability/tsconfig.json" }, - { "path": "../plugins/ingest_pipelines/tsconfig.json" }, - { "path": "../plugins/license_management/tsconfig.json" }, - { "path": "../plugins/snapshot_restore/tsconfig.json" }, - { "path": "../plugins/grokdebugger/tsconfig.json" }, - { "path": "../plugins/painless_lab/tsconfig.json" }, { "path": "../plugins/upgrade_assistant/tsconfig.json" }, - { "path": "../plugins/watcher/tsconfig.json" }, - { "path": "../plugins/runtime_fields/tsconfig.json" }, - { "path": "../plugins/index_management/tsconfig.json" } + { "path": "../plugins/watcher/tsconfig.json" } ] } diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 6fabd16752dfa..5d51c2923abd0 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -22,6 +22,7 @@ "plugins/embeddable_enhanced/**/*", "plugins/event_log/**/*", "plugins/enterprise_search/**/*", + "plugins/infra/**/*", "plugins/licensing/**/*", "plugins/lens/**/*", "plugins/maps/**/*", @@ -118,6 +119,7 @@ { "path": "./plugins/global_search/tsconfig.json" }, { "path": "./plugins/graph/tsconfig.json" }, { "path": "./plugins/grokdebugger/tsconfig.json" }, + { "path": "./plugins/infra/tsconfig.json" }, { "path": "./plugins/ingest_pipelines/tsconfig.json" }, { "path": "./plugins/lens/tsconfig.json" }, { "path": "./plugins/license_management/tsconfig.json" }, diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json index e35cfe4e024a2..ae88ab6486e64 100644 --- a/x-pack/tsconfig.refs.json +++ b/x-pack/tsconfig.refs.json @@ -23,6 +23,7 @@ { "path": "./plugins/global_search/tsconfig.json" }, { "path": "./plugins/graph/tsconfig.json" }, { "path": "./plugins/grokdebugger/tsconfig.json" }, + { "path": "./plugins/infra/tsconfig.json" }, { "path": "./plugins/ingest_pipelines/tsconfig.json" }, { "path": "./plugins/lens/tsconfig.json" }, { "path": "./plugins/license_management/tsconfig.json" },