+ + + {routes.map((route, i) => ( diff --git a/x-pack/plugins/apm/public/components/app/Settings/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/index.tsx index 36c36e3957e962..61f68a74be9b70 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/index.tsx @@ -16,8 +16,6 @@ import { import { i18n } from '@kbn/i18n'; import React, { ReactNode, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { HeaderMenuPortal } from '../../../../../observability/public'; -import { ActionMenu } from '../../../application/action_menu'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { getAPMHref } from '../../shared/Links/apm/APMLink'; import { HomeLink } from '../../shared/Links/apm/HomeLink'; @@ -27,7 +25,7 @@ interface SettingsProps extends RouteComponentProps<{}> { } export function Settings({ children, location }: SettingsProps) { - const { appMountParameters, core } = useApmPluginContext(); + const { core } = useApmPluginContext(); const { basePath } = core.http; const canAccessML = !!core.application.capabilities.ml?.canAccessML; const { search, pathname } = location; @@ -44,11 +42,6 @@ export function Settings({ children, location }: SettingsProps) { return ( <> - - - diff --git a/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx b/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx index 20a589f3126c45..2ba2ae4b5acb6f 100644 --- a/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx @@ -8,9 +8,6 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { ReactNode } from 'react'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; -import { HeaderMenuPortal } from '../../../../../observability/public'; -import { ActionMenu } from '../../../application/action_menu'; -import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { EnvironmentFilter } from '../EnvironmentFilter'; const HeaderFlexGroup = euiStyled(EuiFlexGroup)` @@ -19,13 +16,8 @@ const HeaderFlexGroup = euiStyled(EuiFlexGroup)` `; export function ApmHeader({ children }: { children: ReactNode }) { - const { setHeaderActionMenu } = useApmPluginContext().appMountParameters; - return ( - - - {children} diff --git a/x-pack/plugins/observability/public/components/shared/index.tsx b/x-pack/plugins/observability/public/components/shared/index.tsx index 9f738a6b9143a3..bdeb4a1ea99904 100644 --- a/x-pack/plugins/observability/public/components/shared/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/index.tsx @@ -8,8 +8,9 @@ import React, { lazy, Suspense } from 'react'; import { CoreVitalProps, HeaderMenuPortalProps } from './types'; +const CoreVitalsLazy = lazy(() => import('./core_web_vitals/index')); + export function getCoreVitalsComponent(props: CoreVitalProps) { - const CoreVitalsLazy = lazy(() => import('./core_web_vitals/index')); return ( @@ -17,8 +18,9 @@ export function getCoreVitalsComponent(props: CoreVitalProps) { ); } +const HeaderMenuPortalLazy = lazy(() => import('./header_menu_portal')); + export function HeaderMenuPortal(props: HeaderMenuPortalProps) { - const HeaderMenuPortalLazy = lazy(() => import('./header_menu_portal')); return ( From df8a8cb2afd4b4aaf85c33570db25f4f0ded6822 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 22 Feb 2021 19:18:22 +0100 Subject: [PATCH 35/42] [Search Sessions] Fix completed session icon (#92206) --- .../ui/search_session_indicator/search_session_indicator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx index 24ffc1359acae4..c27a42d8d3d601 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx @@ -163,7 +163,7 @@ const searchSessionIndicatorViewStateToProps: { [SearchSessionState.Completed]: { button: { color: 'subdued', - iconType: 'clock', + iconType: 'check', 'aria-label': i18n.translate('xpack.data.searchSessionIndicator.resultsLoadedIconAriaLabel', { defaultMessage: 'Search session complete', }), From c0de6680f0bfa3f1c2de1e1155b5a8c94b8f9e18 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 22 Feb 2021 13:51:13 -0500 Subject: [PATCH 36/42] [ML] Data Frame Analytics: adds api integration tests for job creation (#92101) * add create endpoint integration tests * update test suite name --- .../ml/data_frame_analytics/create_job.ts | 143 ++++++++++++++++++ .../apis/ml/data_frame_analytics/index.ts | 1 + 2 files changed, 144 insertions(+) create mode 100644 x-pack/test/api_integration/apis/ml/data_frame_analytics/create_job.ts diff --git a/x-pack/test/api_integration/apis/ml/data_frame_analytics/create_job.ts b/x-pack/test/api_integration/apis/ml/data_frame_analytics/create_job.ts new file mode 100644 index 00000000000000..4a4f5c1e555b45 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/data_frame_analytics/create_job.ts @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { DataFrameAnalyticsConfig } from '../../../../../plugins/ml/public/application/data_frame_analytics/common'; +import { DeepPartial } from '../../../../../plugins/ml/common/types/common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const jobId = `bm_${Date.now()}`; + const generateDestinationIndex = (analyticsId: string) => `user-${analyticsId}`; + const commonJobConfig = { + source: { + index: ['ft_bank_marketing'], + query: { + match_all: {}, + }, + }, + analyzed_fields: { + includes: [], + excludes: [], + }, + model_memory_limit: '60mb', + allow_lazy_start: false, // default value + max_num_threads: 1, // default value + }; + + const jobTypes = ['classification', 'regression', 'outlier_detection']; + const jobAnalyses: any = { + classification: { + dependent_variable: 'y', + training_percent: 20, + }, + regression: { + dependent_variable: 'y', + training_percent: 20, + }, + outlier_detection: { + compute_feature_influence: true, + standardization_enabled: true, + }, + }; + + const testJobConfigs: Array<{ + jobId: string; + jobType: string; + config: DeepPartial; + }> = ['Test classification job', 'Test regression job', 'Test outlier detection job'].map( + (description, idx) => { + const analyticsId = `${jobId}_${idx}`; + const jobType = jobTypes[idx]; + return { + jobId: analyticsId, + jobType, + config: { + description, + dest: { + index: generateDestinationIndex(analyticsId), + results_field: 'ml', + }, + analysis: { [jobType]: jobAnalyses[jobType] }, + ...commonJobConfig, + }, + }; + } + ); + + describe('PUT data_frame/analytics/{analyticsId}', () => { + before(async () => { + await esArchiver.loadIfNeeded('ml/bm_classification'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + describe('CreateDataFrameAnalytics', () => { + testJobConfigs.forEach((testConfig) => { + it(`should create ${testConfig.jobType} job with given config`, async () => { + const analyticsId = `${testConfig.jobId}`; + const requestBody = testConfig.config; + + const { body } = await supertest + .put(`/api/ml/data_frame/analytics/${analyticsId}`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(200); + + expect(body).not.to.be(undefined); + + expect(body.description).to.eql(requestBody.description); + expect(body.allow_lazy_start).to.eql(requestBody.allow_lazy_start); + expect(body.model_memory_limit).to.eql(requestBody.model_memory_limit); + expect(body.max_num_threads).to.eql(requestBody.max_num_threads); + + expect(Object.keys(body.analysis)).to.eql(Object.keys(requestBody.analysis!)); + }); + }); + + it('should not allow analytics job creation for unauthorized user', async () => { + const analyticsId = `${testJobConfigs[0].jobId}`; + const requestBody = testJobConfigs[0].config; + + const { body } = await supertest + .put(`/api/ml/data_frame/analytics/${analyticsId}`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(403); + + expect(body.error).to.eql('Forbidden'); + expect(body.message).to.eql('Forbidden'); + }); + + it('should not allow analytics job creation for the user with only view permission', async () => { + const analyticsId = `${testJobConfigs[0].jobId}`; + const requestBody = testJobConfigs[0].config; + + const { body } = await supertest + .put(`/api/ml/data_frame/analytics/${analyticsId}`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(403); + + expect(body.error).to.eql('Forbidden'); + expect(body.message).to.eql('Forbidden'); + }); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts b/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts index a576a4d825da0d..b15e6d892b9e31 100644 --- a/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts +++ b/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts @@ -12,6 +12,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./update')); + loadTestFile(require.resolve('./create_job')); loadTestFile(require.resolve('./get_spaces')); loadTestFile(require.resolve('./update_spaces')); loadTestFile(require.resolve('./delete_spaces')); From d9cdb99c077b8330698753b837875325ca0b0ba8 Mon Sep 17 00:00:00 2001 From: Daniil Date: Mon, 22 Feb 2021 22:03:01 +0300 Subject: [PATCH 37/42] [Data Table] Fix visualization layout in split mode (#91799) * Fix table vis layout in split mode * Stick pagination at the bottom * Fix functional tests * Fix jest test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/components/table_visualization.scss | 4 ++++ .../public/components/table_visualization.test.tsx | 1 - .../public/components/table_visualization.tsx | 14 ++++++-------- test/functional/apps/visualize/_data_table.ts | 2 ++ 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/plugins/vis_type_table/public/components/table_visualization.scss b/src/plugins/vis_type_table/public/components/table_visualization.scss index 7bc51ed5c3d932..28dbf17b18739a 100644 --- a/src/plugins/vis_type_table/public/components/table_visualization.scss +++ b/src/plugins/vis_type_table/public/components/table_visualization.scss @@ -18,6 +18,10 @@ padding: $euiSizeS; margin-bottom: $euiSizeL; + display: flex; + flex-direction: column; + flex: 1 0 0; + > h3 { text-align: center; } 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 index 3d169531f57575..44c315cdbd9e4b 100644 --- a/src/plugins/vis_type_table/public/components/table_visualization.test.tsx +++ b/src/plugins/vis_type_table/public/components/table_visualization.test.tsx @@ -46,7 +46,6 @@ describe('TableVisualizationComponent', () => { ); expect(useUiState).toHaveBeenLastCalledWith(handlers.uiState); expect(comp.find('.tbvChart__splitColumns').exists()).toBeFalsy(); - expect(comp.find('.tbvChart__split').exists()).toBeTruthy(); }); it('should render split table', () => { diff --git a/src/plugins/vis_type_table/public/components/table_visualization.tsx b/src/plugins/vis_type_table/public/components/table_visualization.tsx index ad47fbc2ae4faa..c5a4f42cbb65ed 100644 --- a/src/plugins/vis_type_table/public/components/table_visualization.tsx +++ b/src/plugins/vis_type_table/public/components/table_visualization.tsx @@ -48,14 +48,12 @@ const TableVisualizationComponent = ({
{table ? ( -
- -
+ ) : ( Date: Mon, 22 Feb 2021 20:08:17 +0100 Subject: [PATCH 38/42] [Security Solution][Detections] - Fix loading indicators in the rules management table (#91925) **Base PR:** https://github.com/elastic/kibana/pull/91342 **Fixes:** https://github.com/elastic/kibana/issues/91336 ## Summary This PR fixes loading indicators used in the rules management table. - [Added] Blocking indicator. We show a spinner and "freeze" (fade out) the table when any of these changes: filters, sorting, pagination, manual click on Refresh button. - [Adjusted] Non-blocking indicator. We show a non-blocking "ribbon" (progress bar) only when auto-refresh is in progress. - Initial loading indicator. We show it only on the first table load. Code and tests are slightly adjusted. Things to note are marked below in additional GH comments. Co-authored-by: Yara Tercero --- .../detection_rules/custom_query_rule.spec.ts | 15 ++- .../event_correlation_rule.spec.ts | 15 +-- .../indicator_match_rule.spec.ts | 10 +- .../machine_learning_rule.spec.ts | 10 +- .../detection_rules/override.spec.ts | 10 +- .../detection_rules/prebuilt_rules.spec.ts | 32 ++--- .../detection_rules/sorting.spec.ts | 62 +++++++--- .../detection_rules/threshold_rule.spec.ts | 10 +- ...table.spec.ts => exceptions_table.spec.ts} | 4 +- .../cypress/screens/alerts_detection_rules.ts | 20 ++-- .../cypress/tasks/alerts_detection_rules.ts | 71 +++++++----- .../rules/rules_table/rules_table_facade.ts | 8 ++ .../rules_table/rules_table_reducer.test.ts | 1 + .../rules/rules_table/rules_table_reducer.ts | 14 ++- .../rules/rules_table/use_rules_table.ts | 1 + .../detection_engine/rules/all/index.test.tsx | 20 +++- .../rules/all/rules_tables.tsx | 109 ++++++++++++------ 17 files changed, 257 insertions(+), 155 deletions(-) rename x-pack/plugins/security_solution/cypress/integration/exceptions/{alerts_detection_exceptions_table.spec.ts => exceptions_table.spec.ts} (97%) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts index bedbccadc797f8..ecfa96d59170fa 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts @@ -80,7 +80,7 @@ import { waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; import { - changeToThreeHundredRowsPerPage, + changeRowsPerPageTo300, deleteFirstRule, deleteSelectedRules, editFirstRule, @@ -88,8 +88,8 @@ import { goToCreateNewRule, goToRuleDetails, selectNumberOfRules, - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, - waitForRulesToBeLoaded, + waitForRulesTableToBeLoaded, + waitForRulesTableToBeRefreshed, } from '../../tasks/alerts_detection_rules'; import { createCustomRuleActivated } from '../../tasks/api_calls/rules'; import { createTimeline } from '../../tasks/api_calls/timelines'; @@ -136,7 +136,7 @@ describe('Custom detection rules creation', () => { waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + waitForRulesTableToBeLoaded(); goToCreateNewRule(); fillDefineCustomRuleWithImportedQueryAndContinue(this.rule); fillAboutRuleAndContinue(this.rule); @@ -158,8 +158,7 @@ describe('Custom detection rules creation', () => { cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); @@ -245,7 +244,7 @@ describe('Custom detection rules deletion and edition', () => { cy.get(SHOWING_RULES_TEXT).should('have.text', `Showing ${initialNumberOfRules} rules`); deleteFirstRule(); - waitForRulesToBeLoaded(); + waitForRulesTableToBeRefreshed(); cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should( @@ -275,7 +274,7 @@ describe('Custom detection rules deletion and edition', () => { selectNumberOfRules(numberOfRulesToBeDeleted); deleteSelectedRules(); - waitForRulesToBeLoaded(); + waitForRulesTableToBeRefreshed(); cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should( diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts index fac1056310523b..93c200309fff68 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts @@ -56,12 +56,11 @@ import { waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; import { - changeToThreeHundredRowsPerPage, + changeRowsPerPageTo300, filterByCustomRules, goToCreateNewRule, goToRuleDetails, - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, - waitForRulesToBeLoaded, + waitForRulesTableToBeLoaded, } from '../../tasks/alerts_detection_rules'; import { createTimeline } from '../../tasks/api_calls/timelines'; import { cleanKibana } from '../../tasks/common'; @@ -104,7 +103,7 @@ describe('Detection rules, EQL', () => { waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + waitForRulesTableToBeLoaded(); goToCreateNewRule(); selectEqlRuleType(); fillDefineEqlRuleAndContinue(this.rule); @@ -114,8 +113,7 @@ describe('Detection rules, EQL', () => { cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); @@ -200,7 +198,7 @@ describe('Detection rules, sequence EQL', () => { waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + waitForRulesTableToBeLoaded(); goToCreateNewRule(); selectEqlRuleType(); fillDefineEqlRuleAndContinue(this.rule); @@ -210,8 +208,7 @@ describe('Detection rules, sequence EQL', () => { cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index db29f44ceb98c8..966ce3098d6a7a 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -59,12 +59,11 @@ import { waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; import { - changeToThreeHundredRowsPerPage, + changeRowsPerPageTo300, filterByCustomRules, goToCreateNewRule, goToRuleDetails, - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, - waitForRulesToBeLoaded, + waitForRulesTableToBeLoaded, } from '../../tasks/alerts_detection_rules'; import { cleanKibana } from '../../tasks/common'; import { @@ -375,7 +374,7 @@ describe('indicator match', () => { waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + waitForRulesTableToBeLoaded(); goToCreateNewRule(); selectIndicatorMatchType(); }); @@ -388,8 +387,7 @@ describe('indicator match', () => { cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts index f4e5aaf513190a..e420b970ad85f6 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts @@ -46,12 +46,11 @@ import { waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; import { - changeToThreeHundredRowsPerPage, + changeRowsPerPageTo300, filterByCustomRules, goToCreateNewRule, goToRuleDetails, - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, - waitForRulesToBeLoaded, + waitForRulesTableToBeLoaded, } from '../../tasks/alerts_detection_rules'; import { cleanKibana } from '../../tasks/common'; import { @@ -81,7 +80,7 @@ describe('Detection rules, machine learning', () => { waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + waitForRulesTableToBeLoaded(); goToCreateNewRule(); selectMachineLearningRuleType(); fillDefineMachineLearningRuleAndContinue(machineLearningRule); @@ -91,8 +90,7 @@ describe('Detection rules, machine learning', () => { cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts index 8bd30e54c77a56..82402019fa1e2b 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts @@ -68,12 +68,11 @@ import { waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; import { - changeToThreeHundredRowsPerPage, + changeRowsPerPageTo300, filterByCustomRules, goToCreateNewRule, goToRuleDetails, - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, - waitForRulesToBeLoaded, + waitForRulesTableToBeLoaded, } from '../../tasks/alerts_detection_rules'; import { createTimeline } from '../../tasks/api_calls/timelines'; import { cleanKibana } from '../../tasks/common'; @@ -113,7 +112,7 @@ describe('Detection rules, override', () => { waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + waitForRulesTableToBeLoaded(); goToCreateNewRule(); fillDefineCustomRuleWithImportedQueryAndContinue(this.rule); fillAboutRuleWithOverrideAndContinue(this.rule); @@ -122,8 +121,7 @@ describe('Detection rules, override', () => { cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); const expectedNumberOfRules = 1; cy.get(RULES_TABLE).then(($table) => { diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts index 14954f48111396..d290773d425e24 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts @@ -20,16 +20,15 @@ import { waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; import { - changeToThreeHundredRowsPerPage, + changeRowsPerPageTo300, deleteFirstRule, deleteSelectedRules, loadPrebuiltDetectionRules, - paginate, + goToNextPage, reloadDeletedRules, selectNumberOfRules, - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, + waitForRulesTableToBeLoaded, waitForPrebuiltDetectionRulesToBeLoaded, - waitForRulesToBeLoaded, } from '../../tasks/alerts_detection_rules'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; @@ -51,20 +50,18 @@ describe('Alerts rules, prebuilt rules', () => { waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + waitForRulesTableToBeLoaded(); loadPrebuiltDetectionRules(); waitForPrebuiltDetectionRulesToBeLoaded(); cy.get(ELASTIC_RULES_BTN).should('have.text', expectedElasticRulesBtnText); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); cy.get(SHOWING_RULES_TEXT).should('have.text', `Showing ${expectedNumberOfRules} rules`); cy.get(RULES_TABLE).then(($table1) => { const firstScreenRules = $table1.find(RULES_ROW).length; - paginate(); - waitForRulesToBeLoaded(); + goToNextPage(); cy.get(RULES_TABLE).then(($table2) => { const secondScreenRules = $table2.find(RULES_ROW).length; const totalNumberOfRules = firstScreenRules + secondScreenRules; @@ -85,14 +82,13 @@ describe('Deleting prebuilt rules', () => { waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + waitForRulesTableToBeLoaded(); loadPrebuiltDetectionRules(); waitForPrebuiltDetectionRulesToBeLoaded(); cy.get(ELASTIC_RULES_BTN).should('have.text', expectedElasticRulesBtnText); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); }); it('Does not allow to delete one rule when more than one is selected', () => { @@ -110,8 +106,7 @@ describe('Deleting prebuilt rules', () => { deleteFirstRule(); cy.reload(); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); cy.get(ELASTIC_RULES_BTN).should( 'have.text', @@ -125,8 +120,7 @@ describe('Deleting prebuilt rules', () => { cy.get(RELOAD_PREBUILT_RULES_BTN).should('not.exist'); cy.reload(); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); cy.get(ELASTIC_RULES_BTN).should( 'have.text', @@ -142,8 +136,7 @@ describe('Deleting prebuilt rules', () => { selectNumberOfRules(numberOfRulesToBeSelected); deleteSelectedRules(); cy.reload(); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); cy.get(RELOAD_PREBUILT_RULES_BTN).should('exist'); cy.get(RELOAD_PREBUILT_RULES_BTN).should( @@ -160,8 +153,7 @@ describe('Deleting prebuilt rules', () => { cy.get(RELOAD_PREBUILT_RULES_BTN).should('not.exist'); cy.reload(); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); cy.get(ELASTIC_RULES_BTN).should( 'have.text', diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts index 26d87f9ce9e17b..0cf3caa09814c2 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts @@ -12,6 +12,8 @@ import { SECOND_RULE, RULE_AUTO_REFRESH_IDLE_MODAL, FOURTH_RULE, + RULES_TABLE, + pageSelector, } from '../../screens/alerts_detection_rules'; import { @@ -21,12 +23,14 @@ import { } from '../../tasks/alerts'; import { activateRule, + changeRowsPerPageTo, checkAllRulesIdleModal, checkAutoRefresh, dismissAllRulesIdleModal, + goToPage, resetAllRulesIdleModalTimeout, sortByActivatedRules, - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, + waitForRulesTableToBeLoaded, waitForRuleToBeActivated, } from '../../tasks/alerts_detection_rules'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; @@ -49,12 +53,9 @@ describe('Alerts detection rules', () => { createCustomRule(newThresholdRule, '4'); }); - after(() => { - cy.clock().invoke('restore'); - }); - it('Sorts by activated rules', () => { goToManageAlertsDetectionRules(); + waitForRulesTableToBeLoaded(); cy.get(RULE_NAME) .eq(SECOND_RULE) @@ -88,15 +89,50 @@ describe('Alerts detection rules', () => { }); }); - // FIXME: UI hangs on loading - it.skip('Auto refreshes rules', () => { + it('Pagination updates page number and results', () => { + createCustomRule({ ...newRule, name: 'Test a rule' }, '5'); + createCustomRule({ ...newRule, name: 'Not same as first rule' }, '6'); + + goToManageAlertsDetectionRules(); + waitForRulesTableToBeLoaded(); + + changeRowsPerPageTo(5); + + const FIRST_PAGE_SELECTOR = pageSelector(1); + const SECOND_PAGE_SELECTOR = pageSelector(2); + + cy.get(RULES_TABLE) + .find(FIRST_PAGE_SELECTOR) + .should('have.class', 'euiPaginationButton-isActive'); + + cy.get(RULES_TABLE) + .find(RULE_NAME) + .first() + .invoke('text') + .then((ruleNameFirstPage) => { + goToPage(2); + cy.get(RULES_TABLE) + .find(RULE_NAME) + .first() + .invoke('text') + .should((ruleNameSecondPage) => { + expect(ruleNameFirstPage).not.to.eq(ruleNameSecondPage); + }); + }); + + cy.get(RULES_TABLE) + .find(FIRST_PAGE_SELECTOR) + .should('not.have.class', 'euiPaginationButton-isActive'); + cy.get(RULES_TABLE) + .find(SECOND_PAGE_SELECTOR) + .should('have.class', 'euiPaginationButton-isActive'); + }); + + it('Auto refreshes rules', () => { cy.clock(Date.now()); - loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); - waitForAlertsPanelToBeLoaded(); - waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + waitForRulesTableToBeLoaded(); // mock 1 minute passing to make sure refresh // is conducted @@ -105,7 +141,7 @@ describe('Alerts detection rules', () => { // mock 45 minutes passing to check that idle modal shows // and refreshing is paused checkAllRulesIdleModal('be.visible'); - checkAutoRefresh(DEFAULT_RULE_REFRESH_INTERVAL_VALUE, 'not.be.visible'); + checkAutoRefresh(DEFAULT_RULE_REFRESH_INTERVAL_VALUE, 'not.exist'); // clicking on modal to continue, should resume refreshing dismissAllRulesIdleModal(); @@ -115,7 +151,5 @@ describe('Alerts detection rules', () => { // show after 45 min resetAllRulesIdleModalTimeout(); cy.get(RULE_AUTO_REFRESH_IDLE_MODAL).should('not.exist'); - - cy.clock().invoke('restore'); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts index 3c188345111c85..572422a4936df4 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts @@ -58,12 +58,11 @@ import { waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; import { - changeToThreeHundredRowsPerPage, + changeRowsPerPageTo300, filterByCustomRules, goToCreateNewRule, goToRuleDetails, - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, - waitForRulesToBeLoaded, + waitForRulesTableToBeLoaded, } from '../../tasks/alerts_detection_rules'; import { createTimeline } from '../../tasks/api_calls/timelines'; import { cleanKibana } from '../../tasks/common'; @@ -102,7 +101,7 @@ describe.skip('Threshold Rules', () => { waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + waitForRulesTableToBeLoaded(); goToCreateNewRule(); selectThresholdRuleType(); fillDefineThresholdRuleAndContinue(rule); @@ -112,8 +111,7 @@ describe.skip('Threshold Rules', () => { cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeRowsPerPageTo300(); const expectedNumberOfRules = 1; cy.get(RULES_TABLE).then(($table) => { diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/alerts_detection_exceptions_table.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_table.spec.ts similarity index 97% rename from x-pack/plugins/security_solution/cypress/integration/exceptions/alerts_detection_exceptions_table.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_table.spec.ts index aa469a0cb25311..fdc8a268ca3684 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/alerts_detection_exceptions_table.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_table.spec.ts @@ -12,7 +12,7 @@ import { RULE_STATUS } from '../../screens/create_new_rule'; import { goToManageAlertsDetectionRules, waitForAlertsIndexToBeCreated } from '../../tasks/alerts'; import { createCustomRule } from '../../tasks/api_calls/rules'; -import { goToRuleDetails, waitForRulesToBeLoaded } from '../../tasks/alerts_detection_rules'; +import { goToRuleDetails, waitForRulesTableToBeLoaded } from '../../tasks/alerts_detection_rules'; import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { @@ -61,7 +61,7 @@ describe('Exceptions Table', () => { createExceptionList(exceptionList).as('exceptionListResponse'); goBackToAllRulesTable(); - waitForRulesToBeLoaded(); + waitForRulesTableToBeLoaded(); }); after(() => { diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts index 1a22d14e396ed0..68baad7d3d259a 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts @@ -31,14 +31,12 @@ export const FOURTH_RULE = 3; export const LOAD_PREBUILT_RULES_BTN = '[data-test-subj="load-prebuilt-rules"]'; -export const LOADING_INITIAL_PREBUILT_RULES_TABLE = +export const RULES_TABLE_INITIAL_LOADING_INDICATOR = '[data-test-subj="initialLoadingPanelAllRulesTable"]'; -export const ASYNC_LOADING_PROGRESS = '[data-test-subj="loadingRulesInfoProgress"]'; +export const RULES_TABLE_REFRESH_INDICATOR = '[data-test-subj="loading-spinner"]'; -export const NEXT_BTN = '[data-test-subj="pagination-button-next"]'; - -export const PAGINATION_POPOVER_BTN = '[data-test-subj="tablePaginationPopoverButton"]'; +export const RULES_TABLE_AUTOREFRESH_INDICATOR = '[data-test-subj="loadingRulesInfoProgress"]'; export const RISK_SCORE = '[data-test-subj="riskScore"]'; @@ -66,8 +64,16 @@ export const SHOWING_RULES_TEXT = '[data-test-subj="showingRules"]'; export const SORT_RULES_BTN = '[data-test-subj="tableHeaderSortButton"]'; -export const THREE_HUNDRED_ROWS = '[data-test-subj="tablePagination-300-rows"]'; - export const RULE_AUTO_REFRESH_IDLE_MODAL = '[data-test-subj="allRulesIdleModal"]'; export const RULE_AUTO_REFRESH_IDLE_MODAL_CONTINUE = '[data-test-subj="allRulesIdleModal"] button'; + +export const PAGINATION_POPOVER_BTN = '[data-test-subj="tablePaginationPopoverButton"]'; + +export const rowsPerPageSelector = (count: number) => + `[data-test-subj="tablePagination-${count}-rows"]`; + +export const pageSelector = (pageNumber: number) => + `[data-test-subj="pagination-button-${pageNumber - 1}"]`; + +export const NEXT_BTN = '[data-test-subj="pagination-button-next"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts index 799124190b18d4..3553889449e6d9 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts @@ -13,7 +13,9 @@ import { DELETE_RULE_ACTION_BTN, DELETE_RULE_BULK_BTN, LOAD_PREBUILT_RULES_BTN, - LOADING_INITIAL_PREBUILT_RULES_TABLE, + RULES_TABLE_INITIAL_LOADING_INDICATOR, + RULES_TABLE_REFRESH_INDICATOR, + RULES_TABLE_AUTOREFRESH_INDICATOR, PAGINATION_POPOVER_BTN, RELOAD_PREBUILT_RULES_BTN, RULE_CHECKBOX, @@ -22,13 +24,13 @@ import { RULE_SWITCH_LOADER, RULES_TABLE, SORT_RULES_BTN, - THREE_HUNDRED_ROWS, EXPORT_ACTION_BTN, EDIT_RULE_ACTION_BTN, NEXT_BTN, - ASYNC_LOADING_PROGRESS, RULE_AUTO_REFRESH_IDLE_MODAL, RULE_AUTO_REFRESH_IDLE_MODAL_CONTINUE, + rowsPerPageSelector, + pageSelector, } from '../screens/alerts_detection_rules'; import { ALL_ACTIONS, DELETE_RULE } from '../screens/rule_details'; @@ -36,11 +38,6 @@ export const activateRule = (rulePosition: number) => { cy.get(RULE_SWITCH).eq(rulePosition).click({ force: true }); }; -export const changeToThreeHundredRowsPerPage = () => { - cy.get(PAGINATION_POPOVER_BTN).click({ force: true }); - cy.get(THREE_HUNDRED_ROWS).click(); -}; - export const editFirstRule = () => { cy.get(COLLAPSED_ACTION_BTN).should('be.visible'); cy.get(COLLAPSED_ACTION_BTN).first().click({ force: true }); @@ -71,8 +68,7 @@ export const exportFirstRule = () => { export const filterByCustomRules = () => { cy.get(CUSTOM_RULES_BTN).click({ force: true }); - cy.get(ASYNC_LOADING_PROGRESS).should('exist'); - cy.get(ASYNC_LOADING_PROGRESS).should('not.exist'); + waitForRulesTableToBeRefreshed(); }; export const goToCreateNewRule = () => { @@ -87,10 +83,6 @@ export const loadPrebuiltDetectionRules = () => { cy.get(LOAD_PREBUILT_RULES_BTN).should('exist').click({ force: true }); }; -export const paginate = () => { - cy.get(NEXT_BTN).click(); -}; - export const reloadDeletedRules = () => { cy.get(RELOAD_PREBUILT_RULES_BTN).click({ force: true }); }; @@ -103,14 +95,24 @@ export const selectNumberOfRules = (numberOfRules: number) => { export const sortByActivatedRules = () => { cy.get(SORT_RULES_BTN).contains('Activated').click({ force: true }); - waitForRulesToBeLoaded(); + waitForRulesTableToBeRefreshed(); cy.get(SORT_RULES_BTN).contains('Activated').click({ force: true }); - waitForRulesToBeLoaded(); + waitForRulesTableToBeRefreshed(); }; -export const waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded = () => { - cy.get(LOADING_INITIAL_PREBUILT_RULES_TABLE).should('exist'); - cy.get(LOADING_INITIAL_PREBUILT_RULES_TABLE).should('not.exist'); +export const waitForRulesTableToBeLoaded = () => { + cy.get(RULES_TABLE_INITIAL_LOADING_INDICATOR).should('exist'); + cy.get(RULES_TABLE_INITIAL_LOADING_INDICATOR).should('not.exist'); +}; + +export const waitForRulesTableToBeRefreshed = () => { + cy.get(RULES_TABLE_REFRESH_INDICATOR).should('exist'); + cy.get(RULES_TABLE_REFRESH_INDICATOR).should('not.exist'); +}; + +export const waitForRulesTableToBeAutoRefreshed = () => { + cy.get(RULES_TABLE_AUTOREFRESH_INDICATOR).should('exist'); + cy.get(RULES_TABLE_AUTOREFRESH_INDICATOR).should('not.exist'); }; export const waitForPrebuiltDetectionRulesToBeLoaded = () => { @@ -123,15 +125,10 @@ export const waitForRuleToBeActivated = () => { cy.get(RULE_SWITCH_LOADER).should('not.exist'); }; -export const waitForRulesToBeLoaded = () => { - cy.get(ASYNC_LOADING_PROGRESS).should('exist'); - cy.get(ASYNC_LOADING_PROGRESS).should('not.exist'); -}; - export const checkAutoRefresh = (ms: number, condition: string) => { - cy.get(ASYNC_LOADING_PROGRESS).should('not.exist'); + cy.get(RULES_TABLE_AUTOREFRESH_INDICATOR).should('not.exist'); cy.tick(ms); - cy.get(ASYNC_LOADING_PROGRESS).should(condition); + cy.get(RULES_TABLE_AUTOREFRESH_INDICATOR).should(condition); }; export const dismissAllRulesIdleModal = () => { @@ -152,3 +149,25 @@ export const resetAllRulesIdleModalTimeout = () => { cy.window().trigger('mousemove', { force: true }); cy.tick(700000); }; + +export const changeRowsPerPageTo = (rowsCount: number) => { + cy.get(PAGINATION_POPOVER_BTN).click({ force: true }); + cy.get(rowsPerPageSelector(rowsCount)).click(); + waitForRulesTableToBeRefreshed(); +}; + +export const changeRowsPerPageTo300 = () => { + changeRowsPerPageTo(300); +}; + +export const goToPage = (pageNumber: number) => { + cy.get(RULES_TABLE_REFRESH_INDICATOR).should('not.exist'); + cy.get(pageSelector(pageNumber)).last().click({ force: true }); + waitForRulesTableToBeRefreshed(); +}; + +export const goToNextPage = () => { + cy.get(RULES_TABLE_REFRESH_INDICATOR).should('not.exist'); + cy.get(NEXT_BTN).click({ force: true }); + waitForRulesTableToBeRefreshed(); +}; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_facade.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_facade.ts index 77c327c9f7939b..e9fec425d467e1 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_facade.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_facade.ts @@ -18,6 +18,7 @@ export interface RulesTableFacade { setShowIdleModal(show: boolean): void; setLastRefreshDate(): void; setAutoRefreshOn(on: boolean): void; + setIsRefreshing(isRefreshing: boolean): void; } export const createRulesTableFacade = (dispatch: Dispatch): RulesTableFacade => { @@ -80,5 +81,12 @@ export const createRulesTableFacade = (dispatch: Dispatch): Ru on, }); }, + + setIsRefreshing: (isRefreshing: boolean) => { + dispatch({ + type: 'setIsRefreshing', + isRefreshing, + }); + }, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_reducer.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_reducer.test.ts index 1a45c60dba58a4..c90bfe2b0267f4 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_reducer.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_reducer.test.ts @@ -30,6 +30,7 @@ const initialState: RulesTableState = { exportRuleIds: [], lastUpdated: 0, isRefreshOn: false, + isRefreshing: false, showIdleModal: false, }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_reducer.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_reducer.ts index edcf4f6395d895..92f21f6b508aaa 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_reducer.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_reducer.ts @@ -28,6 +28,7 @@ export interface RulesTableState { exportRuleIds: string[]; lastUpdated: number; isRefreshOn: boolean; + isRefreshing: boolean; showIdleModal: boolean; } @@ -44,6 +45,7 @@ export type RulesTableAction = | { type: 'exportRuleIds'; ids: string[] } | { type: 'setLastRefreshDate' } | { type: 'setAutoRefreshOn'; on: boolean } + | { type: 'setIsRefreshing'; isRefreshing: boolean } | { type: 'setShowIdleModal'; show: boolean } | { type: 'failure' }; @@ -53,11 +55,7 @@ export const createRulesTableReducer = ( const rulesTableReducer = (state: RulesTableState, action: RulesTableAction): RulesTableState => { switch (action.type) { case 'setRules': { - if ( - tableRef != null && - tableRef.current != null && - tableRef.current.changeSelection != null - ) { + if (tableRef?.current?.changeSelection != null) { // for future devs: eui basic table is not giving us a prop to set the value, so // we are using the ref in setTimeout to reset on the next loop so that we // do not get a warning telling us we are trying to update during a render @@ -142,6 +140,12 @@ export const createRulesTableReducer = ( isRefreshOn: action.on, }; } + case 'setIsRefreshing': { + return { + ...state, + isRefreshing: action.isRefreshing, + }; + } case 'setShowIdleModal': { return { ...state, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_rules_table.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_rules_table.ts index f31b2894301ba6..e36474a2fdddd0 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_rules_table.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_rules_table.ts @@ -38,6 +38,7 @@ const initialStateDefaults: RulesTableState = { exportRuleIds: [], lastUpdated: 0, isRefreshOn: true, + isRefreshing: false, showIdleModal: false, }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx index 7f9061b6abc83b..d552afdd9f13f5 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx @@ -11,10 +11,9 @@ import { waitFor } from '@testing-library/react'; import '../../../../../common/mock/match_media'; import '../../../../../common/mock/formatted_relative'; -import { AllRules } from './index'; -import { useKibana, useUiSetting$ } from '../../../../../common/lib/kibana'; -import { useRulesTable, useRulesStatuses } from '../../../../containers/detection_engine/rules'; import { TestProviders } from '../../../../../common/mock'; + +import { useKibana, useUiSetting$ } from '../../../../../common/lib/kibana'; import { createUseUiSetting$Mock } from '../../../../../common/lib/kibana/kibana_react.mock'; import { DEFAULT_RULE_REFRESH_INTERVAL_ON, @@ -23,6 +22,14 @@ import { DEFAULT_RULES_TABLE_REFRESH_SETTING, } from '../../../../../../common/constants'; +import { + useRulesTable, + useRulesStatuses, + RulesTableState, +} from '../../../../containers/detection_engine/rules'; + +import { AllRules } from './index'; + jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -64,10 +71,11 @@ describe('AllRules', () => { }); mockUseRulesTable.mockImplementation(({ initialStateOverride }) => { - const initialState = { + const initialState: RulesTableState = { rules: [ { actions: [], + author: [], created_at: '2020-02-14T19:49:28.178Z', created_by: 'elastic', description: 'jibber jabber', @@ -86,8 +94,10 @@ describe('AllRules', () => { query: 'host.name:*', references: [], risk_score: 73, + risk_score_mapping: [], rule_id: '571afc56-5ed9-465d-a2a9-045f099f6e7e', severity: 'high', + severity_mapping: [], tags: ['Elastic', 'Endpoint'], threat: [], throttle: null, @@ -117,6 +127,7 @@ describe('AllRules', () => { exportRuleIds: [], lastUpdated: 0, isRefreshOn: true, + isRefreshing: false, showIdleModal: false, }; @@ -132,6 +143,7 @@ describe('AllRules', () => { setShowIdleModal: jest.fn(), setLastRefreshDate: jest.fn(), setAutoRefreshOn: jest.fn(), + setIsRefreshing: jest.fn(), }; }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx index a40833d8d14acb..411e817c4407f5 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx @@ -5,6 +5,8 @@ * 2.0. */ +/* eslint-disable complexity */ + import { EuiBasicTable, EuiLoadingContent, @@ -130,6 +132,7 @@ export const RulesTables = React.memo( lastUpdated, showIdleModal, isRefreshOn, + isRefreshing, } = rulesTable.state; const { @@ -139,11 +142,10 @@ export const RulesTables = React.memo( setShowIdleModal, setLastRefreshDate, setAutoRefreshOn, + setIsRefreshing, reFetchRules, } = rulesTable; - const isLoadingRules = loadingRulesAction === 'load'; - const { loading: isLoadingRulesStatuses, rulesStatuses } = useRulesStatuses(rules); const [, dispatchToaster] = useStateToaster(); const mlCapabilities = useMlCapabilities(); @@ -151,6 +153,19 @@ export const RulesTables = React.memo( // TODO: Refactor license check + hasMlAdminPermissions to common check const hasMlPermissions = hasMlLicense(mlCapabilities) && hasMlAdminPermissions(mlCapabilities); + const isLoadingRules = loadingRulesAction === 'load'; + const isLoadingAnActionOnRule = useMemo(() => { + if ( + loadingRuleIds.length > 0 && + (loadingRulesAction === 'disable' || loadingRulesAction === 'enable') + ) { + return false; + } else if (loadingRuleIds.length > 0) { + return true; + } + return false; + }, [loadingRuleIds, loadingRulesAction]); + const sorting = useMemo( (): SortingType => ({ sort: { @@ -225,8 +240,9 @@ export const RulesTables = React.memo( }, { page: page.index + 1, perPage: page.size } ); + setLastRefreshDate(); }, - [updateOptions] + [updateOptions, setLastRefreshDate] ); const rulesColumns = useMemo(() => { @@ -292,25 +308,41 @@ export const RulesTables = React.memo( [loadingRuleIds, dispatch] ); - const isLoadingAnActionOnRule = useMemo(() => { - if ( - loadingRuleIds.length > 0 && - (loadingRulesAction === 'disable' || loadingRulesAction === 'enable') - ) { - return false; - } else if (loadingRuleIds.length > 0) { - return true; - } - return false; - }, [loadingRuleIds, loadingRulesAction]); + const refreshTable = useCallback( + async (mode: 'auto' | 'manual' = 'manual'): Promise => { + if (isLoadingAnActionOnRule) { + return; + } + + const isAutoRefresh = mode === 'auto'; + if (isAutoRefresh) { + setIsRefreshing(true); + } - const handleRefreshData = useCallback(async (): Promise => { - if (!isLoadingAnActionOnRule) { await reFetchRules(); await refetchPrePackagedRulesStatus(); setLastRefreshDate(); - } - }, [reFetchRules, isLoadingAnActionOnRule, setLastRefreshDate, refetchPrePackagedRulesStatus]); + + if (isAutoRefresh) { + setIsRefreshing(false); + } + }, + [ + isLoadingAnActionOnRule, + setIsRefreshing, + reFetchRules, + refetchPrePackagedRulesStatus, + setLastRefreshDate, + ] + ); + + const handleAutoRefresh = useCallback(async (): Promise => { + await refreshTable('auto'); + }, [refreshTable]); + + const handleManualRefresh = useCallback(async (): Promise => { + await refreshTable(); + }, [refreshTable]); const handleResetIdleTimer = useCallback((): void => { if (isRefreshOn) { @@ -326,29 +358,29 @@ export const RulesTables = React.memo( useEffect(() => { const interval = setInterval(() => { if (isRefreshOn) { - handleRefreshData(); + handleAutoRefresh(); } }, defaultAutoRefreshSetting.value); return () => { clearInterval(interval); }; - }, [isRefreshOn, handleRefreshData, defaultAutoRefreshSetting.value]); + }, [isRefreshOn, handleAutoRefresh, defaultAutoRefreshSetting.value]); const handleIdleModalContinue = useCallback((): void => { setShowIdleModal(false); - handleRefreshData(); + handleAutoRefresh(); setAutoRefreshOn(true); - }, [setShowIdleModal, setAutoRefreshOn, handleRefreshData]); + }, [setShowIdleModal, setAutoRefreshOn, handleAutoRefresh]); const handleAutoRefreshSwitch = useCallback( (refreshOn: boolean) => { if (refreshOn) { - handleRefreshData(); + handleAutoRefresh(); } setAutoRefreshOn(refreshOn); }, - [setAutoRefreshOn, handleRefreshData] + [setAutoRefreshOn, handleAutoRefresh] ); const shouldShowRulesTable = useMemo( @@ -401,14 +433,16 @@ export const RulesTables = React.memo( data-test-subj="allRulesPanel" > <> - {(isLoadingRules || isLoadingRulesStatuses) && ( - - )} + {!initLoading && + (loading || isLoadingRules || isLoadingAnActionOnRule) && + isRefreshing && ( + + )} ( )} - {isLoadingAnActionOnRule && !initLoading && ( - - )} + {!initLoading && + (loading || isLoadingRules || isLoadingAnActionOnRule) && + !isRefreshing && ( + + )} + {shouldShowPrepackagedRulesPrompt && ( ( paginationTotal={pagination.total ?? 0} numberSelectedItems={selectedRuleIds.length} onGetBatchItemsPopoverContent={getBatchItemsPopoverContent} - onRefresh={handleRefreshData} + onRefresh={handleManualRefresh} isAutoRefreshOn={isRefreshOn} onRefreshSwitch={handleAutoRefreshSwitch} showBulkActions From 4614202297b1740b8b9c74c68366291a2e6baddf Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Mon, 22 Feb 2021 14:24:10 -0500 Subject: [PATCH 39/42] [Discover] Show correct data for top level object columns (#91954) * [Discover] Show correct data for top level object columns * Fix bug with missing fields * Fix bug in data grid * Fix remaining bug in datagrid * Change use of API to work with any type Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/__mocks__/index_pattern.ts | 12 +- .../angular/doc_table/components/table_row.ts | 38 +- .../application/angular/helpers/index.ts | 2 +- .../angular/helpers/row_formatter.test.ts | 91 ++++- .../angular/helpers/row_formatter.ts | 28 ++ .../application/components/discover.tsx | 1 + .../discover_grid/discover_grid.tsx | 10 +- .../get_render_cell_value.test.tsx | 327 +++++++++++++++++- .../discover_grid/get_render_cell_value.tsx | 71 +++- test/functional/apps/discover/_field_data.ts | 22 ++ .../discover/_field_data_with_fields_api.ts | 23 ++ test/functional/page_objects/discover_page.ts | 8 +- 12 files changed, 589 insertions(+), 44 deletions(-) diff --git a/src/plugins/discover/public/__mocks__/index_pattern.ts b/src/plugins/discover/public/__mocks__/index_pattern.ts index 8101590f029e10..e74046e9dc1ecc 100644 --- a/src/plugins/discover/public/__mocks__/index_pattern.ts +++ b/src/plugins/discover/public/__mocks__/index_pattern.ts @@ -52,6 +52,13 @@ const fields = [ scripted: true, filterable: false, }, + { + name: 'object.value', + type: 'number', + scripted: false, + filterable: true, + aggregatable: true, + }, ] as IIndexPatternFieldList; fields.getByName = (name: string) => { @@ -64,13 +71,14 @@ const indexPattern = ({ metaFields: ['_index', '_score'], formatField: jest.fn(), flattenHit: undefined, - formatHit: jest.fn((hit) => hit._source), + formatHit: jest.fn((hit) => (hit.fields ? hit.fields : hit._source)), fields, getComputedFields: () => ({ docvalueFields: [], scriptFields: {}, storedFields: ['*'] }), getSourceFiltering: () => ({}), - getFieldByName: () => ({}), + getFieldByName: jest.fn(() => ({})), timeFieldName: '', docvalueFields: [], + getFormatterForField: () => ({ convert: () => 'formatted' }), } as unknown) as IndexPattern; indexPattern.flattenHit = indexPatterns.flattenHitWrapper(indexPattern, indexPattern.metaFields); diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts index b527b202ad87d1..12ec9445f4afcd 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts @@ -16,7 +16,7 @@ import cellTemplateHtml from '../components/table_row/cell.html'; import truncateByHeightTemplateHtml from '../components/table_row/truncate_by_height.html'; import { getServices } from '../../../../kibana_services'; import { getContextUrl } from '../../../helpers/get_context_url'; -import { formatRow } from '../../helpers'; +import { formatRow, formatTopLevelObject } from '../../helpers'; const TAGS_WITH_WS = />\s+ { + return key.indexOf(`${column}.`) === 0; + }) + ); + newHtmls.push( + cellTemplate({ + timefield: false, + sourcefield: true, + formatted: formatTopLevelObject(row, innerColumns, indexPattern), + filterable: false, + column, + }) + ); + } else { + newHtmls.push( + cellTemplate({ + timefield: false, + sourcefield: column === '_source', + formatted: _displayField(row, column, true), + filterable: isFilterable, + column, + }) + ); + } }); } diff --git a/src/plugins/discover/public/application/angular/helpers/index.ts b/src/plugins/discover/public/application/angular/helpers/index.ts index 3d4893268fdee8..1dd194436cdfb0 100644 --- a/src/plugins/discover/public/application/angular/helpers/index.ts +++ b/src/plugins/discover/public/application/angular/helpers/index.ts @@ -7,5 +7,5 @@ */ export { buildPointSeriesData } from './point_series'; -export { formatRow } from './row_formatter'; +export { formatRow, formatTopLevelObject } from './row_formatter'; export { handleSourceColumnState } from './state_helpers'; diff --git a/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts b/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts index 259ad2c2d3d1bd..abbc5294605918 100644 --- a/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts +++ b/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { formatRow } from './row_formatter'; +import { formatRow, formatTopLevelObject } from './row_formatter'; import { stubbedSavedObjectIndexPattern } from '../../../__mocks__/stubbed_saved_object_index_pattern'; import { IndexPattern } from '../../../../../data/common/index_patterns/index_patterns'; import { fieldFormatsMock } from '../../../../../data/common/field_formats/mocks'; @@ -43,16 +43,97 @@ describe('Row formatter', () => { foo: 'bar', hello: '<h1>World</h1>', }; - const formatHitMock = jest.fn().mockReturnValueOnce(formatHitReturnValue); + const formatHitMock = jest.fn().mockReturnValue(formatHitReturnValue); beforeEach(() => { - // @ts-ignore + // @ts-expect-error indexPattern.formatHit = formatHitMock; }); it('formats document properly', () => { - expect(formatRow(hit, indexPattern).trim()).toBe( - '
also:
with \\"quotes\\" or 'single qoutes'
number:
42
foo:
bar
hello:
<h1>World</h1>
' + expect(formatRow(hit, indexPattern).trim()).toMatchInlineSnapshot( + `"
also:
with \\\\"quotes\\\\" or 'single qoutes'
number:
42
foo:
bar
hello:
<h1>World</h1>
"` + ); + }); + + it('formats document with highlighted fields first', () => { + expect( + formatRow({ ...hit, highlight: { number: '42' } }, indexPattern).trim() + ).toMatchInlineSnapshot( + `"
number:
42
also:
with \\\\"quotes\\\\" or 'single qoutes'
foo:
bar
hello:
<h1>World</h1>
"` + ); + }); + + it('formats top level objects using formatter', () => { + indexPattern.getFieldByName = jest.fn().mockReturnValue({ + name: 'subfield', + }); + indexPattern.getFormatterForField = jest.fn().mockReturnValue({ + convert: () => 'formatted', + }); + expect( + formatTopLevelObject( + { + fields: { + 'object.value': [5, 10], + }, + }, + { + 'object.value': [5, 10], + }, + indexPattern + ).trim() + ).toMatchInlineSnapshot( + `"
object.value:
formatted, formatted
"` + ); + }); + + it('formats top level objects with subfields and highlights', () => { + indexPattern.getFieldByName = jest.fn().mockReturnValue({ + name: 'subfield', + }); + indexPattern.getFormatterForField = jest.fn().mockReturnValue({ + convert: () => 'formatted', + }); + expect( + formatTopLevelObject( + { + fields: { + 'object.value': [5, 10], + 'object.keys': ['a', 'b'], + }, + highlight: { + 'object.keys': 'a', + }, + }, + { + 'object.value': [5, 10], + 'object.keys': ['a', 'b'], + }, + indexPattern + ).trim() + ).toMatchInlineSnapshot( + `"
object.keys:
formatted, formatted
object.value:
formatted, formatted
"` + ); + }); + + it('formats top level objects, converting unknown fields to string', () => { + indexPattern.getFieldByName = jest.fn(); + indexPattern.getFormatterForField = jest.fn(); + expect( + formatTopLevelObject( + { + fields: { + 'object.value': [5, 10], + }, + }, + { + 'object.value': [5, 10], + }, + indexPattern + ).trim() + ).toMatchInlineSnapshot( + `"
object.value:
5, 10
"` ); }); }); diff --git a/src/plugins/discover/public/application/angular/helpers/row_formatter.ts b/src/plugins/discover/public/application/angular/helpers/row_formatter.ts index 1291bce23c5f30..e17e840e404846 100644 --- a/src/plugins/discover/public/application/angular/helpers/row_formatter.ts +++ b/src/plugins/discover/public/application/angular/helpers/row_formatter.ts @@ -35,3 +35,31 @@ export const formatRow = (hit: Record, indexPattern: IndexPattern) }); return doTemplate({ defPairs: [...highlightPairs, ...sourcePairs] }); }; + +export const formatTopLevelObject = ( + row: Record, + fields: Record, + indexPattern: IndexPattern +) => { + const highlights = row.highlight ?? {}; + const highlightPairs: Array<[string, unknown]> = []; + const sourcePairs: Array<[string, unknown]> = []; + Object.entries(fields).forEach(([key, values]) => { + const field = indexPattern.getFieldByName(key); + const formatter = field + ? indexPattern.getFormatterForField(field) + : { convert: (v: string, ...rest: unknown[]) => String(v) }; + const formatted = values + .map((val: unknown) => + formatter.convert(val, 'html', { + field, + hit: row, + indexPattern, + }) + ) + .join(', '); + const pairs = highlights[key] ? highlightPairs : sourcePairs; + pairs.push([key, formatted]); + }); + return doTemplate({ defPairs: [...highlightPairs, ...sourcePairs] }); +}; diff --git a/src/plugins/discover/public/application/components/discover.tsx b/src/plugins/discover/public/application/components/discover.tsx index e62dccbadcbd08..d0c839aac5a6c6 100644 --- a/src/plugins/discover/public/application/components/discover.tsx +++ b/src/plugins/discover/public/application/components/discover.tsx @@ -410,6 +410,7 @@ export function Discover({ onSetColumns={onSetColumns} onSort={onSort} onResize={onResize} + useNewFieldsApi={useNewFieldsApi} />
)} diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx index 4f92a49abd2921..a0dcc2c2af4669 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx @@ -120,6 +120,10 @@ export interface DiscoverGridProps { * Current sort setting */ sort: SortPairArr[]; + /** + * How the data is fetched + */ + useNewFieldsApi: boolean; } export const EuiDataGridMemoized = React.memo((props: EuiDataGridProps) => { @@ -146,6 +150,7 @@ export const DiscoverGrid = ({ settings, showTimeCol, sort, + useNewFieldsApi, }: DiscoverGridProps) => { const displayedColumns = getDisplayedColumns(columns, indexPattern); const defaultColumns = displayedColumns.includes('_source'); @@ -197,9 +202,10 @@ export const DiscoverGrid = ({ getRenderCellValueFn( indexPattern, rows, - rows ? rows.map((hit) => indexPattern.flattenHit(hit)) : [] + rows ? rows.map((hit) => indexPattern.flattenHit(hit)) : [], + useNewFieldsApi ), - [rows, indexPattern] + [rows, indexPattern, useNewFieldsApi] ); /** diff --git a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx index 786d7bc74bf6b9..a1447a9a836727 100644 --- a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx @@ -10,13 +10,45 @@ import React from 'react'; import { shallow } from 'enzyme'; import { getRenderCellValueFn } from './get_render_cell_value'; import { indexPatternMock } from '../../../__mocks__/index_pattern'; -const rows = [ + +const rowsSource = [ + { + _id: '1', + _index: 'test', + _type: 'test', + _score: 1, + _source: { bytes: 100, extension: '.gz' }, + highlight: { + extension: '@kibana-highlighted-field.gz@/kibana-highlighted-field', + }, + }, +]; + +const rowsFields = [ { _id: '1', _index: 'test', _type: 'test', _score: 1, - _source: { bytes: 100 }, + _source: undefined, + fields: { bytes: [100], extension: ['.gz'] }, + highlight: { + extension: '@kibana-highlighted-field.gz@/kibana-highlighted-field', + }, + }, +]; + +const rowsFieldsWithTopLevelObject = [ + { + _id: '1', + _index: 'test', + _type: 'test', + _score: 1, + _source: undefined, + fields: { 'object.value': [100], extension: ['.gz'] }, + highlight: { + extension: '@kibana-highlighted-field.gz@/kibana-highlighted-field', + }, }, ]; @@ -24,8 +56,9 @@ describe('Discover grid cell rendering', function () { it('renders bytes column correctly', () => { const DiscoverGridCellValue = getRenderCellValueFn( indexPatternMock, - rows, - rows.map((row) => indexPatternMock.flattenHit(row)) + rowsSource, + rowsSource.map((row) => indexPatternMock.flattenHit(row)), + false ); const component = shallow( 100"`); }); + it('renders _source column correctly', () => { const DiscoverGridCellValue = getRenderCellValueFn( indexPatternMock, - rows, - rows.map((row) => indexPatternMock.flattenHit(row)) + rowsSource, + rowsSource.map((row) => indexPatternMock.flattenHit(row)), + false ); const component = shallow( ); - expect(component.html()).toMatchInlineSnapshot( - `"
bytes
100
"` - ); + expect(component).toMatchInlineSnapshot(` + + + extension + + + + bytes + + + + `); }); it('renders _source column correctly when isDetails is set to true', () => { const DiscoverGridCellValue = getRenderCellValueFn( indexPatternMock, - rows, - rows.map((row) => indexPatternMock.flattenHit(row)) + rowsSource, + rowsSource.map((row) => indexPatternMock.flattenHit(row)), + false ); const component = shallow( " + `); + }); + + it('renders fields-based column correctly', () => { + const DiscoverGridCellValue = getRenderCellValueFn( + indexPatternMock, + rowsFields, + rowsFields.map((row) => indexPatternMock.flattenHit(row)), + true + ); + const component = shallow( + + ); + expect(component).toMatchInlineSnapshot(` + + + extension + + + + bytes + + + + `); + }); + + it('renders fields-based column correctly when isDetails is set to true', () => { + const DiscoverGridCellValue = getRenderCellValueFn( + indexPatternMock, + rowsFields, + rowsFields.map((row) => indexPatternMock.flattenHit(row)), + true + ); + const component = shallow( + + ); + expect(component.html()).toMatchInlineSnapshot(` + "{ + "_id": "1", + "_index": "test", + "_type": "test", + "_score": 1, + "fields": { + "bytes": [ + 100 + ], + "extension": [ + ".gz" + ] + }, + "highlight": { + "extension": "@kibana-highlighted-field.gz@/kibana-highlighted-field" } }" `); }); + it('collect object fields and renders them like _source', () => { + const DiscoverGridCellValue = getRenderCellValueFn( + indexPatternMock, + rowsFieldsWithTopLevelObject, + rowsFieldsWithTopLevelObject.map((row) => indexPatternMock.flattenHit(row)), + true + ); + const component = shallow( + + ); + expect(component).toMatchInlineSnapshot(` + + + object.value + + + + `); + }); + + it('collect object fields and renders them like _source with fallback for unmapped', () => { + (indexPatternMock.getFieldByName as jest.Mock).mockReturnValueOnce(undefined); + const DiscoverGridCellValue = getRenderCellValueFn( + indexPatternMock, + rowsFieldsWithTopLevelObject, + rowsFieldsWithTopLevelObject.map((row) => indexPatternMock.flattenHit(row)), + true + ); + const component = shallow( + + ); + expect(component).toMatchInlineSnapshot(` + + + object.value + + + + `); + }); + + it('collect object fields and renders them as json in details', () => { + const DiscoverGridCellValue = getRenderCellValueFn( + indexPatternMock, + rowsFieldsWithTopLevelObject, + rowsFieldsWithTopLevelObject.map((row) => indexPatternMock.flattenHit(row)), + true + ); + const component = shallow( + + ); + expect(component).toMatchInlineSnapshot(` + + { + "object.value": [ + 100 + ] + } + + `); + }); + + it('does not collect subfields when the the column is unmapped but part of fields response', () => { + (indexPatternMock.getFieldByName as jest.Mock).mockReturnValueOnce(undefined); + const DiscoverGridCellValue = getRenderCellValueFn( + indexPatternMock, + rowsFieldsWithTopLevelObject, + rowsFieldsWithTopLevelObject.map((row) => indexPatternMock.flattenHit(row)), + true + ); + const component = shallow( + + ); + expect(component).toMatchInlineSnapshot(` + + `); + }); + it('renders correctly when invalid row is given', () => { const DiscoverGridCellValue = getRenderCellValueFn( indexPatternMock, - rows, - rows.map((row) => indexPatternMock.flattenHit(row)) + rowsSource, + rowsSource.map((row) => indexPatternMock.flattenHit(row)), + false ); const component = shallow( -"`); }); + it('renders correctly when invalid column is given', () => { const DiscoverGridCellValue = getRenderCellValueFn( indexPatternMock, - rows, - rows.map((row) => indexPatternMock.flattenHit(row)) + rowsSource, + rowsSource.map((row) => indexPatternMock.flattenHit(row)), + false ); const component = shallow( > + rowsFlattened: Array>, + useNewFieldsApi: boolean ) => ({ rowIndex, columnId, isDetails, setCellProps }: EuiDataGridCellValueElementProps) => { const row = rows ? (rows[rowIndex] as Record) : undefined; const rowFlattened = rowsFlattened @@ -51,6 +52,60 @@ export const getRenderCellValueFn = ( return -; } + if ( + useNewFieldsApi && + !field && + row && + row.fields && + !(row.fields as Record)[columnId] + ) { + const innerColumns = Object.fromEntries( + Object.entries(row.fields as Record).filter(([key]) => { + return key.indexOf(`${columnId}.`) === 0; + }) + ); + if (isDetails) { + // nicely formatted JSON for the expanded view + return {JSON.stringify(innerColumns, null, 2)}; + } + + // Put the most important fields first + const highlights: Record = (row.highlight as Record) ?? {}; + const highlightPairs: Array<[string, string]> = []; + const sourcePairs: Array<[string, string]> = []; + Object.entries(innerColumns).forEach(([key, values]) => { + const subField = indexPattern.getFieldByName(key); + const formatter = subField + ? indexPattern.getFormatterForField(subField) + : { convert: (v: string, ...rest: unknown[]) => String(v) }; + const formatted = (values as unknown[]) + .map((val: unknown) => + formatter.convert(val, 'html', { + field: subField, + hit: row, + indexPattern, + }) + ) + .join(', '); + const pairs = highlights[key] ? highlightPairs : sourcePairs; + pairs.push([key, formatted]); + }); + + return ( + + {[...highlightPairs, ...sourcePairs].map(([key, value]) => ( + + {key} + + + ))} + + ); + } + if (field && field.type === '_source') { if (isDetails) { // nicely formatted JSON for the expanded view @@ -58,13 +113,23 @@ export const getRenderCellValueFn = ( } const formatted = indexPattern.formatHit(row); + // Put the most important fields first + const highlights: Record = (row.highlight as Record) ?? {}; + const highlightPairs: Array<[string, string]> = []; + const sourcePairs: Array<[string, string]> = []; + + Object.entries(formatted).forEach(([key, val]) => { + const pairs = highlights[key] ? highlightPairs : sourcePairs; + pairs.push([key, val as string]); + }); + return ( - {Object.keys(formatted).map((key) => ( + {[...highlightPairs, ...sourcePairs].map(([key, value]) => ( {key} diff --git a/test/functional/apps/discover/_field_data.ts b/test/functional/apps/discover/_field_data.ts index 3a84158609a18e..3583a8b12c4156 100644 --- a/test/functional/apps/discover/_field_data.ts +++ b/test/functional/apps/discover/_field_data.ts @@ -16,6 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const toasts = getService('toasts'); const queryBar = getService('queryBar'); + const browser = getService('browser'); const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']); describe('discover tab', function describeIndexTests() { @@ -89,6 +90,27 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(message).to.contain(expectedError); await toasts.dismissToast(); }); + + it('shows top-level object keys', async function () { + await queryBar.setQuery('election'); + await queryBar.submitQuery(); + const currentUrl = await browser.getCurrentUrl(); + const [, hash] = currentUrl.split('#/'); + await PageObjects.common.navigateToUrl( + 'discover', + hash.replace('columns:!(_source)', 'columns:!(relatedContent)'), + { useActualUrl: true } + ); + await retry.try(async function tryingForTime() { + expect(await PageObjects.discover.getDocHeader()).to.be('Time relatedContent'); + }); + + const field = await PageObjects.discover.getDocTableField(1, 1); + expect(field).to.include.string('"og:description":'); + + const marks = await PageObjects.discover.getMarks(); + expect(marks.length).to.be(0); + }); }); }); } diff --git a/test/functional/apps/discover/_field_data_with_fields_api.ts b/test/functional/apps/discover/_field_data_with_fields_api.ts index 73864377476b21..168f718c386021 100644 --- a/test/functional/apps/discover/_field_data_with_fields_api.ts +++ b/test/functional/apps/discover/_field_data_with_fields_api.ts @@ -16,6 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const toasts = getService('toasts'); const queryBar = getService('queryBar'); + const browser = getService('browser'); const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']); describe('discover tab with new fields API', function describeIndexTests() { @@ -89,6 +90,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(message).to.contain(expectedError); await toasts.dismissToast(); }); + + it('shows top-level object keys', async function () { + await queryBar.setQuery('election'); + await queryBar.submitQuery(); + const currentUrl = await browser.getCurrentUrl(); + const [, hash] = currentUrl.split('#/'); + await PageObjects.common.navigateToUrl( + 'discover', + hash.replace('columns:!()', 'columns:!(relatedContent)'), + { useActualUrl: true } + ); + await retry.try(async function tryingForTime() { + expect(await PageObjects.discover.getDocHeader()).to.be('Time relatedContent'); + }); + + const field = await PageObjects.discover.getDocTableField(1, 1); + expect(field).to.include.string('relatedContent.url:'); + + const marks = await PageObjects.discover.getMarks(); + expect(marks.length).to.be(172); + expect(marks.indexOf('election')).to.be(0); + }); }); }); } diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 63667afa8289a9..b7b4535641c900 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -194,11 +194,11 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider return await row.getVisibleText(); } - public async getDocTableField(index: number) { - const field = await find.byCssSelector( - `tr.kbnDocTable__row:nth-child(${index}) > [data-test-subj='docTableField']` + public async getDocTableField(index: number, cellIndex = 0) { + const fields = await find.allByCssSelector( + `tr.kbnDocTable__row:nth-child(${index}) [data-test-subj='docTableField']` ); - return await field.getVisibleText(); + return await fields[cellIndex].getVisibleText(); } public async skipToEndOfDocTable() { From 6cca06715d82fd40a04fc7962f2120838b7990f5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Feb 2021 14:38:04 -0500 Subject: [PATCH 40/42] Update dependency @elastic/elasticsearch to ^8.0.0-canary.3 (#91706) Co-authored-by: Renovate Bot --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 8c8a866e9f214d..7302dc3c02f614 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "@babel/core": "^7.12.10", "@babel/runtime": "^7.12.5", "@elastic/datemath": "link:packages/elastic-datemath", - "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.2", + "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.3", "@elastic/ems-client": "7.12.0", "@elastic/eui": "31.7.0", "@elastic/filesaver": "1.1.2", diff --git a/yarn.lock b/yarn.lock index e2c6ba8d320e60..ae0764590462c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2167,10 +2167,10 @@ version "0.0.0" uid "" -"@elastic/elasticsearch@npm:@elastic/elasticsearch-canary@^8.0.0-canary.2": - version "8.0.0-canary.2" - resolved "https://registry.yarnpkg.com/@elastic/elasticsearch-canary/-/elasticsearch-canary-8.0.0-canary.2.tgz#476e22bc90fc4f422f7195f693fdcddb7f8e1897" - integrity sha512-xYdVJ1MCAprVxd0rqmkBVof7I0N+e6VBCcr0UOwEYjvpQJTvu6PPQROBAAmtAAgvIKs4a8HmpArGgu5QJUnNjw== +"@elastic/elasticsearch@npm:@elastic/elasticsearch-canary@^8.0.0-canary.3": + version "8.0.0-canary.3" + resolved "https://registry.yarnpkg.com/@elastic/elasticsearch-canary/-/elasticsearch-canary-8.0.0-canary.3.tgz#b06b95b1370417ac700f30277814fbe7ad532760" + integrity sha512-D8kiFxip0IATzXS+5MAA3+4jnTPiJrpkW+FVNc9e3eq8iCIW/BIv9kPqEH55N/RlJxFwGTz5W4jmmFqBeamNFA== dependencies: debug "^4.1.1" hpagent "^0.1.1" From 581a8862eac26455526dcc7c0069a9d4fdc7de31 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Mon, 22 Feb 2021 11:51:45 -0800 Subject: [PATCH 41/42] Moves Babel/Storybook/Cypress packages to devDependencies (#91204) Signed-off-by: Tyler Smalley --- package.json | 10 +++++----- yarn.lock | 56 ++++++++++++++++++++++++---------------------------- 2 files changed, 31 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index 7302dc3c02f614..c68a8ae34eb814 100644 --- a/package.json +++ b/package.json @@ -93,8 +93,6 @@ "yarn": "^1.21.1" }, "dependencies": { - "@babel/core": "^7.12.10", - "@babel/runtime": "^7.12.5", "@elastic/datemath": "link:packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.3", "@elastic/ems-client": "7.12.0", @@ -103,7 +101,7 @@ "@elastic/good": "^9.0.1-kibana3", "@elastic/node-crypto": "1.2.1", "@elastic/numeral": "^2.5.0", - "@elastic/react-search-ui": "^1.5.0", + "@elastic/react-search-ui": "^1.5.1", "@elastic/request-crypto": "1.1.4", "@elastic/safer-lodash-set": "link:packages/elastic-safer-lodash-set", "@elastic/search-ui-app-search-connector": "^1.5.0", @@ -137,7 +135,6 @@ "@loaders.gl/core": "^2.3.1", "@loaders.gl/json": "^2.3.1", "@slack/webhook": "^5.0.4", - "@storybook/addons": "^6.0.16", "@turf/along": "6.0.1", "@turf/area": "6.0.1", "@turf/bbox": "6.0.1", @@ -330,7 +327,9 @@ }, "devDependencies": { "@babel/cli": "^7.12.10", + "@babel/core": "^7.12.10", "@babel/parser": "^7.12.11", + "@babel/runtime": "^7.12.5", "@babel/plugin-proposal-class-properties": "^7.12.1", "@babel/plugin-proposal-export-namespace-from": "^7.12.1", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", @@ -393,6 +392,7 @@ "@storybook/addon-essentials": "^6.0.26", "@storybook/addon-knobs": "^6.0.26", "@storybook/addon-storyshots": "^6.0.26", + "@storybook/addons": "^6.0.16", "@storybook/components": "^6.0.26", "@storybook/core": "^6.0.26", "@storybook/core-events": "^6.0.26", @@ -609,11 +609,11 @@ "cpy": "^8.1.1", "cronstrue": "^1.51.0", "css-loader": "^3.4.2", - "cypress": "^6.2.1", "cypress-cucumber-preprocessor": "^2.5.2", "cypress-multi-reporters": "^1.4.0", "cypress-pipe": "^2.0.0", "cypress-promise": "^1.1.0", + "cypress": "^6.2.1", "d3": "3.5.17", "d3-cloud": "1.2.5", "d3-scale": "1.0.7", diff --git a/yarn.lock b/yarn.lock index ae0764590462c2..ee18a37a0e5e16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2296,25 +2296,22 @@ resolved "https://registry.yarnpkg.com/@elastic/numeral/-/numeral-2.5.0.tgz#8da714827fc278f17546601fdfe55f5c920e2bc5" integrity sha512-NVTuy9Wzblp6nOH86CXjWXTajHgJGn5Tk2l59/Z5cWFU14KlE+8/zqPTgZdxYABzBJFE3L7S07kJDMN8sDvTmA== -"@elastic/react-search-ui-views@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@elastic/react-search-ui-views/-/react-search-ui-views-1.5.0.tgz#33988ae71588ad3e64f68c6e278d8262f5d59320" - integrity sha512-Ur5Cya+B1em79ZNbPg+KYORuoHDM72LO5lqJeTNrW8WwRTEZi/vL21dOy47VYcSGVnCkttFD2BuyDOTMYFuExQ== +"@elastic/react-search-ui-views@1.5.1": + version "1.5.1" + resolved "https://registry.yarnpkg.com/@elastic/react-search-ui-views/-/react-search-ui-views-1.5.1.tgz#766cd6b6049f7aa8ab711a6a3a4a060ee5fdd0ce" + integrity sha512-x4X2xc/69996IEId3VVBTwPICnx/sschnfQ6YmuU3+myRa+VUPkkAWIK/cBcyBW8TNsLtZHWZrjQYi24+H7YWA== dependencies: - "@babel/runtime" "^7.5.4" - autoprefixer "^9.6.1" downshift "^3.2.10" rc-pagination "^1.20.1" react-select "^2.4.4" -"@elastic/react-search-ui@^1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@elastic/react-search-ui/-/react-search-ui-1.5.0.tgz#d89304a2d6ad6377fe2a7f9202906f05e9bbc159" - integrity sha512-fcfdD9v/87koM1dCsiAhJQz1Fb8Qz4NHEgRqdxZzSsqDaasTeSTRXX6UgbAiDidTa87mvGpT0SxAz8utAATpTQ== +"@elastic/react-search-ui@^1.5.1": + version "1.5.1" + resolved "https://registry.yarnpkg.com/@elastic/react-search-ui/-/react-search-ui-1.5.1.tgz#2c261226d2eda3834b4779fbeea5693958169ff2" + integrity sha512-SI7uOF+jI+Z2D+2otym+4eLBYnocmxa+NA6VPSBrADZXyn8oUEzA4MBtJtxHLtcj64Tj8Riv0tw3t9q3b8iF+w== dependencies: - "@babel/runtime" "^7.5.4" - "@elastic/react-search-ui-views" "1.5.0" - "@elastic/search-ui" "1.5.0" + "@elastic/react-search-ui-views" "1.5.1" + "@elastic/search-ui" "1.5.1" "@elastic/request-crypto@1.1.4": version "1.1.4" @@ -2337,12 +2334,11 @@ "@babel/runtime" "^7.5.4" "@elastic/app-search-javascript" "^7.3.0" -"@elastic/search-ui@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@elastic/search-ui/-/search-ui-1.5.0.tgz#32ea25f3a4fca10d0c56d535658415b276593f05" - integrity sha512-UJzh3UcaAWKLjDIeJlVd0Okg+InLp8bijk+yOvCe4wtbVpTu5NCvAsfxo6mVTNnxS1ik9cRpMOqDT5sw6qyKoQ== +"@elastic/search-ui@1.5.1": + version "1.5.1" + resolved "https://registry.yarnpkg.com/@elastic/search-ui/-/search-ui-1.5.1.tgz#14c66a66f5e937ef5e24d6266620b49d986fb3ed" + integrity sha512-ssfvX1q76X1UwqYASWtBni4PZ+3SYk1PvHmOjpVf9BYai1OqZLGVaj8Sw+cE1ia56zl5In7viCfciC+CP31ovA== dependencies: - "@babel/runtime" "^7.5.4" date-fns "^1.30.1" deep-equal "^1.0.1" history "^4.9.0" @@ -8408,27 +8404,27 @@ autobind-decorator@^1.3.4: resolved "https://registry.yarnpkg.com/autobind-decorator/-/autobind-decorator-1.4.3.tgz#4c96ffa77b10622ede24f110f5dbbf56691417d1" integrity sha1-TJb/p3sQYi7eJPEQ9du/VmkUF9E= -autoprefixer@^9.6.1, autoprefixer@^9.8.6: - version "9.8.6" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f" - integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg== +autoprefixer@^9.7.2, autoprefixer@^9.7.4: + version "9.8.5" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.5.tgz#2c225de229ddafe1d1424c02791d0c3e10ccccaa" + integrity sha512-C2p5KkumJlsTHoNv9w31NrBRgXhf6eCMteJuHZi2xhkgC+5Vm40MEtCKPhc0qdgAOhox0YPy1SQHTAky05UoKg== dependencies: browserslist "^4.12.0" - caniuse-lite "^1.0.30001109" - colorette "^1.2.1" + caniuse-lite "^1.0.30001097" + colorette "^1.2.0" normalize-range "^0.1.2" num2fraction "^1.2.2" postcss "^7.0.32" postcss-value-parser "^4.1.0" -autoprefixer@^9.7.2, autoprefixer@^9.7.4: - version "9.8.5" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.5.tgz#2c225de229ddafe1d1424c02791d0c3e10ccccaa" - integrity sha512-C2p5KkumJlsTHoNv9w31NrBRgXhf6eCMteJuHZi2xhkgC+5Vm40MEtCKPhc0qdgAOhox0YPy1SQHTAky05UoKg== +autoprefixer@^9.8.6: + version "9.8.6" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f" + integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg== dependencies: browserslist "^4.12.0" - caniuse-lite "^1.0.30001097" - colorette "^1.2.0" + caniuse-lite "^1.0.30001109" + colorette "^1.2.1" normalize-range "^0.1.2" num2fraction "^1.2.2" postcss "^7.0.32" From a2be28019c8f51e6c7d8982f79ab250f3056727c Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Mon, 22 Feb 2021 13:14:24 -0700 Subject: [PATCH 42/42] Converts logging README format to be compatible with new docs system (#91958) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- dev_docs/kibana_platform_plugin_intro.mdx | 34 +++++------ src/core/README.md | 2 +- .../server/logging/{README.md => README.mdx} | 61 +++++++++++-------- 3 files changed, 55 insertions(+), 42 deletions(-) rename src/core/server/logging/{README.md => README.mdx} (94%) diff --git a/dev_docs/kibana_platform_plugin_intro.mdx b/dev_docs/kibana_platform_plugin_intro.mdx index ce8b8b8b547561..bf009a3c5251d5 100644 --- a/dev_docs/kibana_platform_plugin_intro.mdx +++ b/dev_docs/kibana_platform_plugin_intro.mdx @@ -7,25 +7,25 @@ date: 2021-01-06 tags: ['kibana','onboarding', 'dev', 'architecture'] --- -From an end user perspective, Kibana is a tool for interacting with Elasticsearch, providing an easy way +From an end user perspective, Kibana is a tool for interacting with Elasticsearch, providing an easy way to visualize and analyze data. From a developer perspective, Kibana is a platform that provides a set of tools to build not only the UI you see in Kibana today, but -a wide variety of applications that can be used to explore, visualize, and act upon data in Elasticsearch. The platform provides developers the ability -to build applications, or inject extra functionality into +a wide variety of applications that can be used to explore, visualize, and act upon data in Elasticsearch. The platform provides developers the ability +to build applications, or inject extra functionality into already existing applications. Did you know that almost everything you see in the Kibana UI is built inside a plugin? If you removed all plugins from Kibana, you'd be left with an empty navigation menu, and a set of developer tools. The Kibana platform is a blank canvas, just waiting for a developer to come along and create something! ![Kibana personas](assets/kibana_platform_plugin_end_user.png) - + ## Platform services Plugins have access to three kinds of public services: - Platform services provided by `core` () - Platform services provided by plugins () - - Shared services provided by plugins, that are only relevant for only a few, specific plugins (e.g. "presentation utils"). + - Shared services provided by plugins, that are only relevant for only a few, specific plugins (e.g. "presentation utils"). The first two items are what make up "Platform services". @@ -37,9 +37,9 @@ clear, and we haven't done a great job of sticking to it. For example, notificat Today it looks something like this. ![Core vs platform plugins vs plugins](assets/platform_plugins_core.png) - + -When the Kibana platform and plugin infrastructure was built, we thought of two types of code: core services, and other plugin services. We planned to keep the most stable and fundamental +When the Kibana platform and plugin infrastructure was built, we thought of two types of code: core services, and other plugin services. We planned to keep the most stable and fundamental code needed to build plugins inside core. In reality, we ended up with many platform-like services living outside of core, with no (short term) intention of moving them. We highly encourage plugin developers to use @@ -54,7 +54,7 @@ In reality, our plugin model ended up being used like micro-services. Plugins ar they desire, without the need to build a plugin. Another side effect of having many small plugins is that common code often ends up extracted into another plugin. Use case specific utilities are exported, - that are not meant to be used in a general manner. This makes our definition of "platform code" a bit trickier to define. We'd like to say "The platform is made up of + that are not meant to be used in a general manner. This makes our definition of "platform code" a bit trickier to define. We'd like to say "The platform is made up of every publically exposed service", but in today's world, that wouldn't be a very accurate picture. We recognize the need to better clarify the relationship between core functionality, platform-like plugin functionality, and functionality exposed by other plugins. @@ -69,19 +69,19 @@ We will continue to focus on adding clarity around these types of services and w ### Core services Sometimes referred to just as Core, Core services provide the most basic and fundamental tools neccessary for building a plugin, like creating saved objects, -routing, application registration, and notifications. The Core platform is not a plugin itself, although +routing, application registration, notifications and . The Core platform is not a plugin itself, although there are some plugins that provide platform functionality. We call these . ### Platform plugins -Plugins that provide fundamental services and functionality to extend and customize Kibana, for example, the +Plugins that provide fundamental services and functionality to extend and customize Kibana, for example, the plugin. There is no official way to tell if a plugin is a platform plugin or not. Platform plugins are _usually_ plugins that are managed by the Platform Group, but we are starting to see some exceptions. ## Plugins -Plugins are code that is written to extend and customize Kibana. Plugin's don't have to be part of the Kibana repo, though the Kibana -repo does contain many plugins! Plugins add customizations by +Plugins are code that is written to extend and customize Kibana. Plugin's don't have to be part of the Kibana repo, though the Kibana +repo does contain many plugins! Plugins add customizations by using provided by . Sometimes people confuse the term "plugin" and "application". While often there is a 1:1 relationship between a plugin and an application, it is not always the case. A plugin may register many applications, or none. @@ -97,7 +97,7 @@ adding it to core's application , +A plugin's public API consists of everything exported from a plugin's , as well as from the top level `index.ts` files that exist in the three "scope" folders: - common/index.ts @@ -113,7 +113,7 @@ Core, and plugins, expose different features at different parts of their lifecyc specifically-named functions on the service definition. Kibana has three lifecycles: setup, start, and stop. Each plugin’s setup function is called sequentially while Kibana is setting up - on the server or when it is being loaded in the browser. The start functions are called sequentially after setup has been completed for all plugins. + on the server or when it is being loaded in the browser. The start functions are called sequentially after setup has been completed for all plugins. The stop functions are called sequentially while Kibana is gracefully shutting down the server or when the browser tab or window is being closed. The table below explains how each lifecycle relates to the state of Kibana. @@ -121,10 +121,10 @@ The table below explains how each lifecycle relates to the state of Kibana. | lifecycle | purpose | server | browser | | ---------- | ------ | ------- | ----- | | setup | perform "registration" work to setup environment for runtime |configure REST API endpoint, register saved object types, etc. | configure application routes in SPA, register custom UI elements in extension points, etc. | -| start | bootstrap runtime logic | respond to an incoming request, request Elasticsearch server, etc. | start polling Kibana server, update DOM tree in response to user interactions, etc.| +| start | bootstrap runtime logic | respond to an incoming request, request Elasticsearch server, etc. | start polling Kibana server, update DOM tree in response to user interactions, etc.| | stop | cleanup runtime | dispose of active handles before the server shutdown. | store session data in the LocalStorage when the user navigates away from Kibana, etc. | -Different service interfaces can and will be passed to setup, start, and stop because certain functionality makes sense in the context of a running plugin while other types +Different service interfaces can and will be passed to setup, start, and stop because certain functionality makes sense in the context of a running plugin while other types of functionality may have restrictions or may only make sense in the context of a plugin that is stopping. ## Extension points @@ -141,4 +141,4 @@ plugins to customize the Kibana experience. Examples of extension points are: ## Follow up material -Learn how to build your own plugin by following \ No newline at end of file +Learn how to build your own plugin by following diff --git a/src/core/README.md b/src/core/README.md index c73c6aa56bfd0d..799cdb5c799ae0 100644 --- a/src/core/README.md +++ b/src/core/README.md @@ -29,7 +29,7 @@ rules tailored to our needs (e.g. `byteSize`, `duration` etc.). That means that by the "legacy" Kibana may be rejected by the `core` now. ### Logging -`core` has its own [logging system](./server/logging/README.md) and will output log records directly (e.g. to file or terminal) when configured. When no +`core` has its own [logging system](./server/logging/README.mdx) and will output log records directly (e.g. to file or terminal) when configured. When no specific configuration is provided, logs are forwarded to the "legacy" Kibana so that they look the same as the rest of the log records throughout Kibana. diff --git a/src/core/server/logging/README.md b/src/core/server/logging/README.mdx similarity index 94% rename from src/core/server/logging/README.md rename to src/core/server/logging/README.mdx index 0704431c282c98..6fd730f47dc07e 100644 --- a/src/core/server/logging/README.md +++ b/src/core/server/logging/README.mdx @@ -1,3 +1,13 @@ +--- +id: kibCoreLogging +slug: /kibana-dev-docs/services/logging +title: Logging system +image: https://source.unsplash.com/400x175/?Logging +summary: Core logging contains the system and service for Kibana logs. +date: 2020-12-02 +tags: ['kibana','dev', 'contributor', 'api docs'] +--- + # Logging - [Loggers, Appenders and Layouts](#loggers-appenders-and-layouts) - [Logger hierarchy](#logger-hierarchy) @@ -16,7 +26,7 @@ - [Log record format changes](#log-record-format-changes) The way logging works in Kibana is inspired by `log4j 2` logging framework used by [Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/settings.html#logging). -The main idea is to have consistent logging behaviour (configuration, log format etc.) across the entire Elastic Stack +The main idea is to have consistent logging behaviour (configuration, log format etc.) across the entire Elastic Stack where possible. ## Loggers, Appenders and Layouts @@ -34,16 +44,17 @@ __Layouts__ define how log messages are formatted and what type of information t ## Logger hierarchy -Every logger has its unique context name that follows hierarchical naming rule. The logger is considered to be an +Every logger has its unique context name that follows hierarchical naming rule. The logger is considered to be an ancestor of another logger if its name followed by a `.` is a prefix of the descendant logger name. For example logger with `a.b` context name is an ancestor of logger with `a.b.c` context name. All top-level loggers are descendants of special -logger with `root` context name that resides at the top of the logger hierarchy. This logger always exists and +logger with `root` context name that resides at the top of the logger hierarchy. This logger always exists and fully configured. Developer can configure _log level_ and _appenders_ that should be used within particular context name. If logger configuration -specifies only _log level_ then _appenders_ configuration will be inherited from the ancestor logger. +specifies only _log level_ then _appenders_ configuration will be inherited from the ancestor logger. -__Note:__ in the current implementation log messages are only forwarded to appenders configured for a particular logger +__Note:__ +In the current implementation log messages are only forwarded to appenders configured for a particular logger context name or to appenders of the closest ancestor if current logger doesn't have any appenders configured. That means that we __don't support__ so called _appender additivity_ when log messages are forwarded to _every_ distinct appender within ancestor chain including `root`. @@ -51,8 +62,10 @@ ancestor chain including `root`. ## Log level Currently we support the following log levels: _all_, _fatal_, _error_, _warn_, _info_, _debug_, _trace_, _off_. + Levels are ordered, so _all_ > _fatal_ > _error_ > _warn_ > _info_ > _debug_ > _trace_ > _off_. -A log record is being logged by the logger if its level is higher than or equal to the level of its logger. Otherwise, + +A log record is being logged by the logger if its level is higher than or equal to the level of its logger. Otherwise, the log record is ignored. The _all_ and _off_ levels can be used only in configuration and are just handy shortcuts that allow developer to log every @@ -61,15 +74,15 @@ log record or disable logging entirely for the specific context name. ## Layouts Every appender should know exactly how to format log messages before they are written to the console or file on the disk. -This behaviour is controlled by the layouts and configured through `appender.layout` configuration property for every +This behaviour is controlled by the layouts and configured through `appender.layout` configuration property for every custom appender (see examples in [Configuration](#configuration)). Currently we don't define any default layout for the custom appenders, so one should always make the choice explicitly. -There are two types of layout supported at the moment: `pattern` and `json`. +There are two types of layout supported at the moment: `pattern` and `json`. ### Pattern layout With `pattern` layout it's possible to define a string pattern with special placeholders `%conversion_pattern` (see the table below) that -will be replaced with data from the actual log message. By default the following pattern is used: +will be replaced with data from the actual log message. By default the following pattern is used: `[%date][%level][%logger]%meta %message`. Also `highlight` option can be enabled for `pattern` layout so that some parts of the log message are highlighted with different colors that may be quite handy if log messages are forwarded to the terminal with color support. @@ -111,7 +124,7 @@ Example of `%meta` output: ##### date Outputs the date of the logging event. The date conversion specifier may be followed by a set of braces containing a name of predefined date format and canonical timezone name. -Timezone name is expected to be one from [TZ database name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). +Timezone name is expected to be one from [TZ database name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). Timezone defaults to the host timezone when not explicitly specified. Example of `%date` output: @@ -130,7 +143,7 @@ Example of `%date` output: Outputs the process ID. ### JSON layout -With `json` layout log messages will be formatted as JSON strings that include timestamp, log level, context name, message +With `json` layout log messages will be formatted as JSON strings that include timestamp, log level, context name, message text and any other metadata that may be associated with the log message itself. ## Appenders @@ -160,7 +173,7 @@ logging: type: size-limit size: 50mb strategy: - //... + //... layout: type: pattern ``` @@ -188,7 +201,7 @@ logging: interval: 10s modulate: true strategy: - //... + //... layout: type: pattern ``` @@ -202,10 +215,10 @@ How often a rollover should occur. The default value is `24h` - `modulate` - + Whether the interval should be adjusted to cause the next rollover to occur on the interval boundary. - -For example, when true, if the interval is `4h` and the current hour is 3 am then the first rollover will occur at 4 am + +For example, when true, if the interval is `4h` and the current hour is 3 am then the first rollover will occur at 4 am and then next ones will occur at 8 am, noon, 4pm, etc. The default value is `true`. @@ -332,8 +345,8 @@ Here is what we get with the config above: | metrics.ops | console | debug | -The `root` logger has a dedicated configuration node since this context name is special and should always exist. By -default `root` is configured with `info` level and `default` appender that is also always available. This is the +The `root` logger has a dedicated configuration node since this context name is special and should always exist. By +default `root` is configured with `info` level and `default` appender that is also always available. This is the configuration that all custom loggers will use unless they're re-configured explicitly. For example to see _all_ log messages that fall back on the `root` logger configuration, just add one line to the configuration: @@ -392,8 +405,8 @@ The message contains some high-level information, and the corresponding log meta ## Usage -Usage is very straightforward, one should just get a logger for a specific context name and use it to log messages with -different log level. +Usage is very straightforward, one should just get a logger for a specific context name and use it to log messages with +different log level. ```typescript const logger = kibana.logger.get('server'); @@ -436,7 +449,7 @@ All log messages handled by `root` context are forwarded to the legacy logging s root appenders, make sure that it contains `default` appender to provide backward compatibility. **Note**: If you define an appender for a context name, the log messages aren't handled by the `root` context anymore and not forwarded to the legacy logging service. - + #### logging.dest By default logs in *stdout*. With new Kibana logging you can use pre-existing `console` appender or define a custom one. @@ -446,7 +459,7 @@ logging: - name: plugins.myPlugin appenders: [console] ``` -Logs in a *file* if given file path. You should define a custom appender with `type: file` +Logs in a *file* if given file path. You should define a custom appender with `type: file` ```yaml logging: @@ -459,13 +472,13 @@ logging: loggers: - name: plugins.myPlugin appenders: [file] -``` +``` #### logging.json Defines the format of log output. Logs in JSON if `true`. With new logging config you can adjust the output format with [layouts](#layouts). #### logging.quiet -Suppresses all logging output other than error messages. With new logging, config can be achieved +Suppresses all logging output other than error messages. With new logging, config can be achieved with adjusting minimum required [logging level](#log-level). ```yaml loggers: