{readOnlyMode && ( <> diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/account_settings_sidebar.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/account_settings_sidebar.tsx index 490f3ff0ae4a5..8ab654aaebf60 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/account_settings_sidebar.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/account_settings_sidebar.tsx @@ -12,6 +12,10 @@ import { ViewContentHeader } from '../../shared/view_content_header'; export const AccountSettingsSidebar = () => { return ( - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx index 3f6863175e29b..ac497f5ae3a28 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx @@ -42,7 +42,7 @@ export const PrivateSourcesSidebar = () => { return ( <> - + {id && } ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.test.tsx index d37af01287c46..9301ebb85582f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.test.tsx @@ -31,10 +31,20 @@ describe('ContentSection', () => { it('displays title and description', () => { const wrapper = shallow(); + const header = wrapper.find(ViewContentHeader); - expect(wrapper.find(ViewContentHeader)).toHaveLength(1); - expect(wrapper.find(ViewContentHeader).prop('title')).toEqual('foo'); - expect(wrapper.find(ViewContentHeader).prop('description')).toEqual('bar'); + expect(header.prop('title')).toEqual('foo'); + expect(header.prop('description')).toEqual('bar'); + expect(header.prop('headingLevel')).toEqual(3); + }); + + it('sets heading level for personal dashboard', () => { + const wrapper = shallow( + + ); + const header = wrapper.find(ViewContentHeader); + + expect(header.prop('headingLevel')).toEqual(2); }); it('displays header content', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.tsx index f0b86e0cc925b..79cb82817e2bc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.tsx @@ -14,6 +14,7 @@ import { ViewContentHeader } from '../view_content_header'; interface ContentSectionProps { children: React.ReactNode; + isOrganization?: boolean; className?: string; title?: React.ReactNode; description?: React.ReactNode; @@ -25,6 +26,7 @@ interface ContentSectionProps { export const ContentSection: React.FC = ({ children, + isOrganization = true, className = '', title, description, @@ -35,7 +37,13 @@ export const ContentSection: React.FC = ({
{title && ( <> - + {headerChildren} )} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/license_badge.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/license_badge.test.tsx index 6deb37d850076..97abb3929f985 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/license_badge.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/license_badge.test.tsx @@ -17,7 +17,6 @@ describe('LicenseBadge', () => { it('renders', () => { const wrapper = shallow(); - expect(wrapper.find(EuiBadge)).toHaveLength(1); - expect(wrapper.find('span').text()).toEqual('Platinum Feature'); + expect(wrapper.find(EuiBadge).prop('children')).toEqual('Platinum feature'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/license_badge.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/license_badge.tsx index 37211d8cad43e..3d286c8da005b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/license_badge.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/license_badge.tsx @@ -9,12 +9,6 @@ import React from 'react'; import { EuiBadge } from '@elastic/eui'; -import './license_badge.scss'; +import { PLATINUM_FEATURE } from '../../../constants'; -const licenseColor = '#00A7B1'; - -export const LicenseBadge: React.FC = () => ( - - Platinum Feature - -); +export const LicenseBadge: React.FC = () => {PLATINUM_FEATURE}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx index 34d6c2401b300..2d5ffe183632a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx @@ -21,5 +21,10 @@ interface SourceIconProps { } export const SourceIcon: React.FC = ({ name, serviceType, className, size }) => ( - + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.test.tsx index f54f7ccdf24bd..63ca5e8153d41 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.test.tsx @@ -11,9 +11,8 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiTable } from '@elastic/eui'; +import { EuiTable, EuiTableHeaderCell } from '@elastic/eui'; -import { TableHeader } from '../../../../shared/table_header/table_header'; import { SourceRow } from '../source_row'; import { SourcesTable } from './'; @@ -25,14 +24,15 @@ describe('SourcesTable', () => { const wrapper = shallow(); expect(wrapper.find(EuiTable)).toHaveLength(1); + expect(wrapper.find(EuiTableHeaderCell)).toHaveLength(3); expect(wrapper.find(SourceRow)).toHaveLength(2); }); it('renders "Searchable" header item when toggle fn present', () => { const wrapper = shallow( - + ); - expect(wrapper.find(TableHeader).prop('headerItems')).toContain('Searchable'); + expect(wrapper.find(EuiTableHeaderCell)).toHaveLength(5); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.tsx index 9bc3d6ec2f1f4..8081dbff5b3fe 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.tsx @@ -7,10 +7,12 @@ import React from 'react'; -import { EuiTable, EuiTableBody } from '@elastic/eui'; +import { EuiTable, EuiTableBody, EuiTableHeader, EuiTableHeaderCell } from '@elastic/eui'; -import { TableHeader } from '../../../../shared/table_header/table_header'; +import { ACTIONS_HEADER } from '../../../../shared/constants'; +import { SOURCE, DOCUMENTS_HEADER, SEARCHABLE_HEADER } from '../../../constants'; import { ContentSourceDetails } from '../../../types'; +import { STATUS_HEADER } from '../../../views/content_sources/constants'; import { SourceRow, ISourceRow } from '../source_row'; interface SourcesTableProps extends ISourceRow { @@ -23,12 +25,15 @@ export const SourcesTable: React.FC = ({ isOrganization, onSearchableToggle, }) => { - const headerItems = ['Source', 'Status', 'Documents']; - if (onSearchableToggle) headerItems.push('Searchable'); - return ( - + + {SOURCE} + {STATUS_HEADER} + {DOCUMENTS_HEADER} + {onSearchableToggle && {SEARCHABLE_HEADER}} + {isOrganization && {ACTIONS_HEADER}} + {sources.map((source) => ( { it('renders with title and alignItems', () => { const wrapper = shallow(); - expect(wrapper.find('h3').text()).toEqual('Header'); + expect(wrapper.find('h2').text()).toEqual('Header'); expect(wrapper.find(EuiFlexGroup).prop('alignItems')).toEqual('flexStart'); }); @@ -39,19 +39,29 @@ describe('ViewContentHeader', () => { expect(wrapper.find('.action')).toHaveLength(1); }); - it('renders small heading', () => { + it('renders h1 heading', () => { const wrapper = shallow( - } /> + } + /> ); - expect(wrapper.find('h4')).toHaveLength(1); + expect(wrapper.find('h1')).toHaveLength(1); }); - it('renders large heading', () => { + it('renders h3 heading', () => { const wrapper = shallow( - } /> + } + /> ); - expect(wrapper.find('h2')).toHaveLength(1); + expect(wrapper.find('h3')).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.tsx index fa3a1d3ccb2e4..d8361a115d883 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.tsx @@ -13,6 +13,7 @@ import { FlexGroupAlignItems } from '@elastic/eui/src/components/flex/flex_group interface ViewContentHeaderProps { title: React.ReactNode; description?: React.ReactNode; + headingLevel?: 1 | 2 | 3; action?: React.ReactNode; alignItems?: FlexGroupAlignItems; titleSize?: 's' | 'm' | 'l'; @@ -21,24 +22,24 @@ interface ViewContentHeaderProps { export const ViewContentHeader: React.FC = ({ title, titleSize = 'm', + headingLevel = 2, description, action, alignItems = 'center', }) => { let titleElement; - switch (titleSize) { - case 's': - titleElement =

{title}

; + switch (headingLevel) { + case 1: + titleElement =

{title}

; break; - case 'l': + case 2: titleElement =

{title}

; break; default: titleElement =

{title}

; break; } - return ( <> diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts index 0e56ee8f67241..d4fa2059f62fb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts @@ -805,3 +805,24 @@ export const STATUS_POPOVER_TOOLTIP = i18n.translate( defaultMessage: 'Click to view info', } ); + +export const DOCUMENTS_HEADER = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.documentsHeader', + { + defaultMessage: 'Documents', + } +); + +export const SEARCHABLE_HEADER = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.searchableHeader', + { + defaultMessage: 'Searchable', + } +); + +export const PLATINUM_FEATURE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.platinumFeature', + { + defaultMessage: 'Platinum feature', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx index a4910f3a68ea2..64f297ae4bdbb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx @@ -9,7 +9,14 @@ import React from 'react'; import { startCase } from 'lodash'; -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTextColor } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiText, + EuiTitle, + EuiTextColor, +} from '@elastic/eui'; import { SourceIcon } from '../../../../components/shared/source_icon'; @@ -42,11 +49,11 @@ export const AddSourceHeader: React.FC = ({ /> - -

+ +

{name} -

- +

+ {categories.map((category) => startCase(category)).join(', ')} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.test.tsx index 2ebc021925abf..b3ce53a0321dc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.test.tsx @@ -25,7 +25,7 @@ describe('ConfigurationIntro', () => { const wrapper = shallow(); expect(wrapper.find('[data-test-subj="ConfigureStepButton"]')).toHaveLength(1); - expect(wrapper.find(EuiText)).toHaveLength(5); - expect(wrapper.find(EuiTitle)).toHaveLength(1); + expect(wrapper.find(EuiText)).toHaveLength(3); + expect(wrapper.find(EuiTitle)).toHaveLength(3); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx index 23bd34cfeb944..d17e8b84efb2b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx @@ -96,9 +96,9 @@ export const ConfigurationIntro: React.FC = ({ >
- -

{CONFIG_INTRO_STEP1_HEADING}

-
+ +

{CONFIG_INTRO_STEP1_HEADING}

+
@@ -125,9 +125,9 @@ export const ConfigurationIntro: React.FC = ({ >
- +

{CONFIG_INTRO_STEP2_HEADING}

-
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx index 6da09acf45cbe..b5b22afec39b1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx @@ -122,7 +122,7 @@ export const ConfiguredSourcesList: React.FC = ({ return ( <> -

{CONFIGURED_SOURCES_TITLE}

+

{CONFIGURED_SOURCES_TITLE}

{CONFIGURED_SOURCES_EMPTY_BODY}

diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx index e3b34050593fa..d8b5a9eedbaa7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx @@ -67,8 +67,6 @@ export const ConnectInstance: React.FC = ({ objTypes, name, serviceType, - sourceDescription, - connectStepDescription, needsPermissions, onFormCreated, header, @@ -162,9 +160,9 @@ export const ConnectInstance: React.FC = ({ <> -

+

{CONNECT_DOC_PERMISSIONS_TITLE} -

+
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts index 712be15e7c046..2bf185ee048bd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts @@ -307,6 +307,13 @@ export const SAVE_CUSTOM_DOC_PERMISSIONS_TITLE = i18n.translate( } ); +export const INCLUDED_FEATURES_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.includedFeaturesTitle', + { + defaultMessage: 'Included features', + } +); + export const SOURCE_FEATURES_SEARCHABLE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.contentSource.sourceFeatures.searchable.text', { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx index 0f170be8ba076..02856320aa535 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx @@ -25,6 +25,7 @@ import { AppLogic } from '../../../../app_logic'; import { Features, FeatureIds } from '../../../../types'; import { + INCLUDED_FEATURES_TITLE, SOURCE_FEATURES_SEARCHABLE, SOURCE_FEATURES_REMOTE_FEATURE, SOURCE_FEATURES_PRIVATE_FEATURE, @@ -179,9 +180,7 @@ export const SourceFeatures: React.FC = ({ features, objTy return ( <> -

- Included features -

+

{INCLUDED_FEATURES_TITLE}

diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.scss index e5b680c5edec3..90f0613df57d9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.scss +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.scss @@ -29,7 +29,7 @@ align-items: center; padding: 0 .25rem; background: #E9EDF2; - color: #647487; + color: #3F4B58; font-size: 10px; font-weight: 600; text-transform: uppercase; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx index e9b8574032916..f44d15c27f002 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx @@ -51,6 +51,7 @@ import { } from '../../../routes'; import { SOURCES_NO_CONTENT_TITLE, + SOURCE_OVERVIEW_TITLE, CONTENT_SUMMARY_TITLE, CONTENT_TYPE_HEADER, ITEMS_HEADER, @@ -133,7 +134,7 @@ export const Overview: React.FC = () => { return ( <> -

{CONTENT_SUMMARY_TITLE}

+

{CONTENT_SUMMARY_TITLE}

{!summary && } @@ -218,7 +219,7 @@ export const Overview: React.FC = () => { return ( <> -

{RECENT_ACTIVITY_TITLE}

+

{RECENT_ACTIVITY_TITLE}

{activities.length === 0 ? emptyState : activitiesTable} @@ -228,9 +229,9 @@ export const Overview: React.FC = () => { const groupsSummary = ( <> - -

{GROUP_ACCESS_TITLE}

-
+ +
{GROUP_ACCESS_TITLE}
+
{groups.map((group, index) => ( @@ -254,9 +255,9 @@ export const Overview: React.FC = () => { const detailsSummary = ( <> - -

{CONFIGURATION_TITLE}

-
+ +

{CONFIGURATION_TITLE}

+
@@ -427,7 +428,7 @@ export const Overview: React.FC = () => { -

{title}

+ {title}
{children}
@@ -438,7 +439,7 @@ export const Overview: React.FC = () => { -

{DOCUMENT_PERMISSIONS_TITLE}

+ {DOCUMENT_PERMISSIONS_TITLE}

{DOC_PERMISSIONS_DESCRIPTION}

@@ -454,7 +455,7 @@ export const Overview: React.FC = () => { return ( - + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx index 1c3c44887946a..d98b4f6b1e67d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx @@ -41,7 +41,7 @@ export const SourceInfoCard: React.FC = ({ -
{sourceName}
+

{sourceName}

diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts index d57afc6699c1a..7e4918929f1ce 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts @@ -477,3 +477,10 @@ export const PERSONAL_DASHBOARD_SOURCE_ERROR = (error: string) => 'Could not connect the source, reach out to your admin for help. Error message: {error}', values: { error }, }); + +export const SOURCE_OVERVIEW_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceOverviewTitle', + { + defaultMessage: 'Source overview', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx index 693c1e8bd5e40..57574ce14df67 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx @@ -97,6 +97,7 @@ export const PrivateSources: React.FC = () => { const privateSourcesSection = ( { const sharedSourcesSection = ( = ({ id, name, updatedAt, contentSources - + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx index 45175e489f94a..3c44261cc911f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx @@ -18,6 +18,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { ACTIONS_HEADER } from '../../../../shared/constants'; import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; import { GroupsLogic } from '../groups_logic'; @@ -71,7 +72,7 @@ export const GroupsTable: React.FC<{}> = () => { {GROUP_TABLE_HEADER} {SOURCES_TABLE_HEADER} - + {ACTIONS_HEADER} {groups.map((group, index) => ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx index 338eda0214ea2..652fb89f41c7c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx @@ -70,7 +70,7 @@ export const OnboardingCard: React.FC = ({ {title}} + title={

{title}

} body={description} actions={complete ? completeButton : incompleteButton} /> diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx index 44d09ed73ed1f..9f525235af6f2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx @@ -145,12 +145,12 @@ export const OrgNameOnboarding: React.FC = () => {
-

+

-

+
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.tsx index 68f2a2289c1f2..d2f8232168eb1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.tsx @@ -16,6 +16,7 @@ import { EuiFlexItem, EuiSwitch, EuiText, + EuiTitle, EuiTable, EuiTableBody, EuiTableHeader, @@ -129,9 +130,9 @@ export const PrivateSourcesTable: React.FC = ({ /> - -

{isRemote ? REMOTE_SOURCES_TOGGLE_TEXT : STANDARD_SOURCES_TOGGLE_TEXT}

-
+ +

{isRemote ? REMOTE_SOURCES_TOGGLE_TEXT : STANDARD_SOURCES_TOGGLE_TEXT}

+
{isRemote ? REMOTE_SOURCES_TABLE_DESCRIPTION : STANDARD_SOURCES_TABLE_DESCRIPTION} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx index 6cf1831dc07e7..a971df8f89914 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx @@ -16,7 +16,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSwitch, - EuiText, + EuiTitle, EuiSpacer, EuiPanel, EuiConfirmModal, @@ -109,9 +109,9 @@ export const Security: React.FC = () => { />
- -

{PRIVATE_SOURCES_TOGGLE_DESCRIPTION}

-
+ +

{PRIVATE_SOURCES_TOGGLE_DESCRIPTION}

+
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.tsx index c3d01a3410d70..98662585ce330 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.tsx @@ -48,6 +48,7 @@ export const Customize: React.FC = () => { isInvalid={false} required value={orgNameInputValue} + aria-label={CUSTOMIZE_NAME_LABEL} data-test-subj="OrgNameInput" onChange={(e) => onOrgNameInputChange(e.target.value)} /> diff --git a/x-pack/plugins/file_upload/public/components/import_complete_view.tsx b/x-pack/plugins/file_upload/public/components/import_complete_view.tsx index a3bc2ed082b1a..70e1ac25a7ecc 100644 --- a/x-pack/plugins/file_upload/public/components/import_complete_view.tsx +++ b/x-pack/plugins/file_upload/public/components/import_complete_view.tsx @@ -75,7 +75,6 @@ export class ImportCompleteView extends Component { {}} options={{ readOnly: true, lineNumbers: 'off', diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx index 476a5d8e029d3..dd24f1091843a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx @@ -31,6 +31,11 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ packageInfo?: PackageInfo; integrationInfo?: RegistryPolicyTemplate; 'data-test-subj'?: string; + tabs?: Array<{ + title: string; + isSelected: boolean; + onClick: React.ReactEventHandler; + }>; }> = memo( ({ from, @@ -41,6 +46,7 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ integrationInfo, children, 'data-test-subj': dataTestSubj, + tabs = [], }) => { const pageTitle = useMemo(() => { if ( @@ -184,6 +190,7 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ rightColumn={rightColumn} rightColumnGrow={false} data-test-subj={dataTestSubj} + tabs={tabs.map(({ title, ...rest }) => ({ name: title, ...rest }))} > {children} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index 65496afc1a101..3dc88c7565e73 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -332,30 +332,58 @@ export const EditPackagePolicyForm = memo<{ } }; + const extensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-edit'); + const extensionTabsView = useUIExtension( + packagePolicy.package?.name ?? '', + 'package-policy-edit-tabs' + ); + const tabsViews = extensionTabsView?.tabs; + const [selectedTab, setSelectedTab] = useState(0); + const layoutProps = { from, cancelUrl, agentPolicy, packageInfo, + tabs: tabsViews?.length + ? [ + { + title: i18n.translate('xpack.fleet.editPackagePolicy.settingsTabName', { + defaultMessage: 'Settings', + }), + isSelected: selectedTab === 0, + onClick: () => { + setSelectedTab(0); + }, + }, + ...tabsViews.map(({ title }, index) => ({ + title, + isSelected: selectedTab === index + 1, + onClick: () => { + setSelectedTab(index + 1); + }, + })), + ] + : [], }; - const extensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-edit'); - const configurePackage = useMemo( () => agentPolicy && packageInfo ? ( <> - + {selectedTab === 0 && ( + + )} {/* Only show the out-of-box configuration step if a UI extension is NOT registered */} - {!extensionView && ( + {!extensionView && selectedTab === 0 && ( - + {selectedTab > 0 && tabsViews ? ( + React.createElement(tabsViews[selectedTab - 1].Component, { + policy: originalPackagePolicy, + newPolicy: packagePolicy, + onChange: handleExtensionViewOnChange, + }) + ) : ( + + )} )} @@ -389,6 +425,8 @@ export const EditPackagePolicyForm = memo<{ originalPackagePolicy, extensionView, handleExtensionViewOnChange, + selectedTab, + tabsViews, ] ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx index 1edf1bf697251..d2342dafca2eb 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx @@ -295,7 +295,7 @@ const AgentDetailsPageContent: React.FunctionComponent<{ agent: Agent; agentPolicy?: AgentPolicy; }> = ({ agent, agentPolicy }) => { - useBreadcrumbs('agent_list', { + useBreadcrumbs('agent_details', { agentHost: typeof agent.local_metadata.host === 'object' && typeof agent.local_metadata.host.hostname === 'string' diff --git a/x-pack/plugins/fleet/public/types/ui_extensions.ts b/x-pack/plugins/fleet/public/types/ui_extensions.ts index bc692fe1caa7d..40e92fe86555d 100644 --- a/x-pack/plugins/fleet/public/types/ui_extensions.ts +++ b/x-pack/plugins/fleet/public/types/ui_extensions.ts @@ -52,6 +52,16 @@ export interface PackagePolicyEditExtension { Component: LazyExoticComponent; } +/** Extension point registration contract for Integration Policy Edit tabs views */ +export interface PackagePolicyEditTabsExtension { + package: string; + view: 'package-policy-edit-tabs'; + tabs: Array<{ + title: EuiStepProps['title']; + Component: LazyExoticComponent; + }>; +} + /** * UI Component Extension is used on the pages displaying the ability to Create an * Integration Policy @@ -120,6 +130,7 @@ export interface AgentEnrollmentFlyoutFinalStepExtension { /** Fleet UI Extension Point */ export type UIExtensionPoint = | PackagePolicyEditExtension + | PackagePolicyEditTabsExtension | PackageCustomExtension | PackagePolicyCreateExtension | PackageAssetsExtension diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index 9f9f0dab6efac..54cb0846207a3 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -42,7 +42,7 @@ import { migrateSettingsToV7130, migrateOutputToV7130, } from './migrations/to_v7_13_0'; -import { migratePackagePolicyToV7140 } from './migrations/to_v7_14_0'; +import { migratePackagePolicyToV7140, migrateInstallationToV7140 } from './migrations/to_v7_14_0'; import { migratePackagePolicyToV7150 } from './migrations/to_v7_15_0'; /* @@ -320,6 +320,9 @@ const getSavedObjectTypes = ( install_source: { type: 'keyword' }, }, }, + migrations: { + '7.14.0': migrateInstallationToV7140, + }, }, [ASSETS_SAVED_OBJECT_TYPE]: { name: ASSETS_SAVED_OBJECT_TYPE, diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_10_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_10_0.ts index 39e65efcf2ab1..64338690977c9 100644 --- a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_10_0.ts +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_10_0.ts @@ -15,7 +15,6 @@ import type { EnrollmentAPIKey, Settings, AgentAction, - Installation, } from '../../types'; export const migrateAgentToV7100: SavedObjectMigrationFn< @@ -127,12 +126,3 @@ export const migrateAgentActionToV7100 = ( }, }); }; - -export const migrateInstallationToV7100: SavedObjectMigrationFn< - Exclude, - Installation -> = (installationDoc) => { - installationDoc.attributes.install_source = 'registry'; - - return installationDoc; -}; diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_14_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_14_0.ts index 3255e15c6ceec..90c9ac5f8e89b 100644 --- a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_14_0.ts +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_14_0.ts @@ -7,7 +7,7 @@ import type { SavedObjectMigrationFn } from 'kibana/server'; -import type { PackagePolicy } from '../../../common'; +import type { PackagePolicy, Installation } from '../../../common'; import { migrateEndpointPackagePolicyToV7140 } from './security_solution'; @@ -27,3 +27,17 @@ export const migratePackagePolicyToV7140: SavedObjectMigrationFn = ( + doc +) => { + // Fix a missing migration for user that used Fleet before 7.9 + if (!doc.attributes.install_source) { + doc.attributes.install_source = 'registry'; + } + if (!doc.attributes.install_version) { + doc.attributes.install_version = doc.attributes.version; + } + + return doc; +}; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts index 7fc8d59628738..6febd27286ad1 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts @@ -19,7 +19,7 @@ jest.mock('./common', () => { }); import { ResponseError } from '@elastic/elasticsearch/lib/errors'; -import type { DeeplyMockedKeys } from '@kbn/utility-types/target/jest'; +import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import type { ElasticsearchClient, SavedObject, SavedObjectsClientContract } from 'kibana/server'; import { ElasticsearchAssetType } from '../../../../types'; diff --git a/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts b/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts index 8c637006fb0cd..568aafddecbad 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts @@ -35,9 +35,6 @@ export const getRegistryUrl = (): string => { const isEnterprise = licenseService.isEnterprise(); if (customUrl && isEnterprise) { - appContextService - .getLogger() - .info('Custom registry url is an experimental feature and is unsupported.'); return customUrl; } diff --git a/x-pack/plugins/fleet/server/services/fleet_server/index.ts b/x-pack/plugins/fleet/server/services/fleet_server/index.ts index 076bee7d0e7a4..733d962a86e9e 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server/index.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server/index.ts @@ -59,6 +59,13 @@ export async function startFleetServerSetup() { try { // We need licence to be initialized before using the SO service. await licenseService.getLicenseInformation$()?.pipe(first())?.toPromise(); + + const customUrl = appContextService.getConfig()?.registryUrl; + const isEnterprise = licenseService.isEnterprise(); + if (customUrl && isEnterprise) { + logger.info('Custom registry url is an experimental feature and is unsupported.'); + } + await runFleetServerMigration(); _isFleetServerSetup = true; } catch (err) { diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts index ed7db67433130..5ea4b896e7fa1 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts @@ -84,6 +84,9 @@ function getPutPreconfiguredPackagesMock() { references: [], }; }); + + soClient.delete.mockResolvedValue({}); + return soClient; } @@ -239,7 +242,7 @@ describe('policy preconfiguration', () => { ); }); - it('should return nonFatalErrors', async () => { + it('should not create a policy if we are not able to add packages ', async () => { const soClient = getPutPreconfiguredPackagesMock(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const policies: PreconfiguredAgentPolicy[] = [ @@ -256,17 +259,21 @@ describe('policy preconfiguration', () => { }, ]; - const { nonFatalErrors } = await ensurePreconfiguredPackagesAndPolicies( - soClient, - esClient, - policies, - [{ name: 'CANNOT_MATCH', version: 'x.y.z' }], - mockDefaultOutput - ); + let error; + try { + await ensurePreconfiguredPackagesAndPolicies( + soClient, + esClient, + policies, + [{ name: 'CANNOT_MATCH', version: 'x.y.z' }], + mockDefaultOutput + ); + } catch (err) { + error = err; + } - expect(nonFatalErrors.length).toBe(1); - expect(nonFatalErrors[0].agentPolicy).toEqual({ name: 'Test policy' }); - expect(nonFatalErrors[0].error.message).toEqual( + expect(error).toBeDefined(); + expect(error.message).toEqual( 'Test policy could not be added. test_package is not installed, add test_package to `xpack.fleet.packages` or remove it from Test package.' ); }); diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index 28f21f38b48ee..334df17a8d3a8 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -18,6 +18,8 @@ import type { PreconfiguredPackage, PreconfigurationError, } from '../../common'; +import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../common'; + import { PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, PRECONFIGURATION_LATEST_KEYWORD, @@ -32,6 +34,7 @@ import { bulkInstallPackages } from './epm/packages/bulk_install_packages'; import { agentPolicyService, addPackageToAgentPolicy } from './agent_policy'; import type { InputsOverride } from './package_policy'; import { overridePackageInputs } from './package_policy'; +import { appContextService } from './app_context'; interface PreconfigurationResult { policies: Array<{ id: string; updated_at: string }>; @@ -153,36 +156,10 @@ export async function ensurePreconfiguredPackagesAndPolicies( } return { created, policy }; } - const { package_policies: packagePolicies } = preconfiguredAgentPolicy; - - const installedPackagePolicies = await Promise.all( - packagePolicies.map(async ({ package: pkg, name, ...newPackagePolicy }) => { - const installedPackage = await getInstallation({ - savedObjectsClient: soClient, - pkgName: pkg.name, - }); - if (!installedPackage) { - throw new Error( - i18n.translate('xpack.fleet.preconfiguration.packageMissingError', { - defaultMessage: - '{agentPolicyName} could not be added. {pkgName} is not installed, add {pkgName} to `{packagesConfigValue}` or remove it from {packagePolicyName}.', - values: { - agentPolicyName: preconfiguredAgentPolicy.name, - packagePolicyName: name, - pkgName: pkg.name, - packagesConfigValue: 'xpack.fleet.packages', - }, - }) - ); - } - return { name, installedPackage, ...newPackagePolicy }; - }) - ); return { created, policy, - installedPackagePolicies, shouldAddIsManagedFlag: preconfiguredAgentPolicy.is_managed, }; }) @@ -200,20 +177,51 @@ export async function ensurePreconfiguredPackagesAndPolicies( continue; } fulfilledPolicies.push(policyResult.value); - const { - created, - policy, - installedPackagePolicies, - shouldAddIsManagedFlag, - } = policyResult.value; + const { created, policy, shouldAddIsManagedFlag } = policyResult.value; if (created) { - await addPreconfiguredPolicyPackages( - soClient, - esClient, - policy!, - installedPackagePolicies!, - defaultOutput - ); + try { + const preconfiguredAgentPolicy = policies[i]; + const { package_policies: packagePolicies } = preconfiguredAgentPolicy; + + const installedPackagePolicies = await Promise.all( + packagePolicies.map(async ({ package: pkg, name, ...newPackagePolicy }) => { + const installedPackage = await getInstallation({ + savedObjectsClient: soClient, + pkgName: pkg.name, + }); + if (!installedPackage) { + throw new Error( + i18n.translate('xpack.fleet.preconfiguration.packageMissingError', { + defaultMessage: + '{agentPolicyName} could not be added. {pkgName} is not installed, add {pkgName} to `{packagesConfigValue}` or remove it from {packagePolicyName}.', + values: { + agentPolicyName: preconfiguredAgentPolicy.name, + packagePolicyName: name, + pkgName: pkg.name, + packagesConfigValue: 'xpack.fleet.packages', + }, + }) + ); + } + return { name, installedPackage, ...newPackagePolicy }; + }) + ); + await addPreconfiguredPolicyPackages( + soClient, + esClient, + policy!, + installedPackagePolicies!, + defaultOutput + ); + // If ann error happens while adding a package to the policy we will delete the policy so the setup can be retried later + } catch (err) { + await soClient + .delete(AGENT_POLICY_SAVED_OBJECT_TYPE, policy!.id) + // swallow error + .catch((deleteErr) => appContextService.getLogger().error(deleteErr)); + + throw err; + } // Add the is_managed flag after configuring package policies to avoid errors if (shouldAddIsManagedFlag) { agentPolicyService.update(soClient, esClient, policy!.id, { is_managed: true }); diff --git a/x-pack/plugins/graph/kibana.json b/x-pack/plugins/graph/kibana.json index 4e653393100c9..e13cd8a0adba1 100644 --- a/x-pack/plugins/graph/kibana.json +++ b/x-pack/plugins/graph/kibana.json @@ -7,5 +7,9 @@ "requiredPlugins": ["licensing", "data", "navigation", "savedObjects", "kibanaLegacy"], "optionalPlugins": ["home", "features"], "configPath": ["xpack", "graph"], - "requiredBundles": ["kibanaUtils", "kibanaReact", "home"] + "requiredBundles": ["kibanaUtils", "kibanaReact", "home"], + "owner": { + "name": "Kibana App", + "githubTeam": "kibana-app" + } } diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap index c3136a7ae671f..97d1c1757e85a 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap +++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap @@ -106,6 +106,36 @@ exports[`policy table should show empty state when there are not any policies 1`
`; +exports[`policy table should sort when linked index templates header is clicked 1`] = ` +Array [ + "testy1", + "testy3", + "testy5", + "testy7", + "testy9", + "testy11", + "testy13", + "testy15", + "testy17", + "testy19", +] +`; + +exports[`policy table should sort when linked index templates header is clicked 2`] = ` +Array [ + "testy0", + "testy104", + "testy102", + "testy100", + "testy98", + "testy96", + "testy94", + "testy92", + "testy90", + "testy88", +] +`; + exports[`policy table should sort when linked indices header is clicked 1`] = ` Array [ "testy1", diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/init_test_bed.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/init_test_bed.tsx index 4f057e04c85d4..54d68edc7382f 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/init_test_bed.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/init_test_bed.tsx @@ -23,9 +23,6 @@ const getTestBedConfig = (testBedConfigArgs?: Partial): TestBedCo initialEntries: [`/policies/edit/${POLICY_NAME}`], componentRoutePath: `/policies/edit/:policyName`, }, - defaultProps: { - getUrlForApp: () => {}, - }, ...testBedConfigArgs, }; }; @@ -38,6 +35,7 @@ const EditPolicyContainer = ({ appServicesContext, ...rest }: any) => { services={{ breadcrumbService, license: licensingMock.createLicense({ license: { type: 'enterprise' } }), + getUrlForApp: () => {}, ...appServicesContext, }} > diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/rollover_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/rollover_actions.ts index 6d05f3d63f577..798b74e40055f 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/rollover_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/rollover_actions.ts @@ -15,39 +15,65 @@ const createSetPrimaryShardSizeAction = (testBed: TestBed) => async ( units?: string ) => { const { find, component } = testBed; + await act(async () => { find('hot-selectedMaxPrimaryShardSize').simulate('change', { target: { value } }); - if (units) { - find('hot-selectedMaxPrimaryShardSize.select').simulate('change', { - target: { value: units }, - }); - } }); component.update(); + + if (units) { + act(() => { + find('hot-selectedMaxPrimaryShardSize.show-filters-button').simulate('click'); + }); + component.update(); + + act(() => { + find(`hot-selectedMaxPrimaryShardSize.filter-option-${units}`).simulate('click'); + }); + component.update(); + } }; const createSetMaxAgeAction = (testBed: TestBed) => async (value: string, units?: string) => { const { find, component } = testBed; + await act(async () => { find('hot-selectedMaxAge').simulate('change', { target: { value } }); - if (units) { - find('hot-selectedMaxAgeUnits.select').simulate('change', { target: { value: units } }); - } }); component.update(); + + if (units) { + act(() => { + find('hot-selectedMaxAgeUnits.show-filters-button').simulate('click'); + }); + component.update(); + + act(() => { + find(`hot-selectedMaxAgeUnits.filter-option-${units}`).simulate('click'); + }); + component.update(); + } }; const createSetMaxSizeAction = (testBed: TestBed) => async (value: string, units?: string) => { const { find, component } = testBed; + await act(async () => { find('hot-selectedMaxSizeStored').simulate('change', { target: { value } }); - if (units) { - find('hot-selectedMaxSizeStoredUnits.select').simulate('change', { - target: { value: units }, - }); - } }); component.update(); + + if (units) { + act(() => { + find('hot-selectedMaxSizeStoredUnits.show-filters-button').simulate('click'); + }); + component.update(); + + act(() => { + find(`hot-selectedMaxSizeStoredUnits.filter-option-${units}`).simulate('click'); + }); + component.update(); + } }; export const createRolloverActions = (testBed: TestBed) => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx index 048776d7850ab..2fafbc6de98e4 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx @@ -14,7 +14,6 @@ import { findTestSubject, takeMountedSnapshot } from '@elastic/eui/lib/test'; import { fatalErrorsServiceMock, injectedMetadataServiceMock, - scopedHistoryMock, } from '../../../../src/core/public/mocks'; import { HttpService } from '../../../../src/core/public/http'; import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/public/mocks'; @@ -23,6 +22,7 @@ import { PolicyFromES } from '../common/types'; import { PolicyTable } from '../public/application/sections/policy_table/policy_table'; import { init as initHttp } from '../public/application/services/http'; import { init as initUiMetric } from '../public/application/services/ui_metric'; +import { KibanaContextProvider } from '../public/shared_imports'; initHttp( new HttpService().setup({ @@ -36,12 +36,25 @@ initUiMetric(usageCollectionPluginMock.createSetupContract()); const testDate = '2020-07-21T14:16:58.666Z'; const testDateFormatted = moment(testDate).format('YYYY-MM-DD HH:mm:ss'); -const policies: PolicyFromES[] = []; -for (let i = 0; i < 105; i++) { +const testPolicy = { + version: 0, + modifiedDate: testDate, + indices: [`index1`], + indexTemplates: [`indexTemplate1`, `indexTemplate2`, `indexTemplate3`, `indexTemplate4`], + name: `testy0`, + policy: { + name: `testy0`, + phases: {}, + }, +}; + +const policies: PolicyFromES[] = [testPolicy]; +for (let i = 1; i < 105; i++) { policies.push({ version: i, - modifiedDate: i === 0 ? testDate : moment().subtract(i, 'days').toISOString(), + modifiedDate: moment().subtract(i, 'days').toISOString(), indices: i % 2 === 0 ? [`index${i}`] : [], + indexTemplates: i % 2 === 0 ? [`indexTemplate${i}`] : [], name: `testy${i}`, policy: { name: `testy${i}`, @@ -49,7 +62,14 @@ for (let i = 0; i < 105; i++) { }, }); } -jest.mock(''); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: () => ({ + createHref: jest.fn(), + }), +})); + let component: ReactElement; const snapshot = (rendered: string[]) => { @@ -88,24 +108,14 @@ const openContextMenu = (buttonIndex: number) => { describe('policy table', () => { beforeEach(() => { component = ( - + '' }}> + + ); }); test('should show empty state when there are not any policies', () => { - component = ( - - ); + component = ; const rendered = mountWithIntl(component); mountedSnapshot(rendered); }); @@ -147,6 +157,9 @@ describe('policy table', () => { test('should sort when linked indices header is clicked', () => { testSort('indices'); }); + test('should sort when linked index templates header is clicked', () => { + testSort('indexTemplates'); + }); test('should have proper actions in context menu when there are linked indices', () => { const rendered = openContextMenu(0); const buttons = rendered.find('button.euiContextMenuItem'); @@ -180,9 +193,21 @@ describe('policy table', () => { }); test('displays policy properties', () => { const rendered = mountWithIntl(component); - const firstRow = findTestSubject(rendered, 'policyTableRow').at(0).text(); - const version = 0; - const numberOfIndices = 1; - expect(firstRow).toBe(`testy0${numberOfIndices}${version}${testDateFormatted}Actions`); + const firstRow = findTestSubject(rendered, 'policyTableRow-testy0').text(); + const numberOfIndices = testPolicy.indices.length; + const numberOfIndexTemplates = testPolicy.indexTemplates.length; + expect(firstRow).toBe( + `testy0${numberOfIndices}${numberOfIndexTemplates}${testPolicy.version}${testDateFormatted}Actions` + ); + }); + test('opens a flyout with index templates', () => { + const rendered = mountWithIntl(component); + const indexTemplatesButton = findTestSubject(rendered, 'viewIndexTemplates').at(0); + indexTemplatesButton.simulate('click'); + rendered.update(); + const flyoutTitle = findTestSubject(rendered, 'indexTemplatesFlyoutHeader').text(); + expect(flyoutTitle).toContain('testy0'); + const indexTemplatesLinks = findTestSubject(rendered, 'indexTemplateLink'); + expect(indexTemplatesLinks.length).toBe(testPolicy.indexTemplates.length); }); }); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/app.tsx b/x-pack/plugins/index_lifecycle_management/public/application/app.tsx index 3335720ab3065..4bc4e62ac52b6 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/app.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/app.tsx @@ -7,50 +7,25 @@ import React, { useEffect } from 'react'; import { Router, Switch, Route, Redirect } from 'react-router-dom'; -import { ScopedHistory, ApplicationStart } from 'kibana/public'; +import { ScopedHistory } from 'kibana/public'; import { METRIC_TYPE } from '@kbn/analytics'; -import { UIM_APP_LOAD } from './constants/ui_metric'; +import { UIM_APP_LOAD } from './constants'; import { EditPolicy } from './sections/edit_policy'; import { PolicyTable } from './sections/policy_table'; import { trackUiMetric } from './services/ui_metric'; import { ROUTES } from './services/navigation'; -export const AppWithRouter = ({ - history, - navigateToApp, - getUrlForApp, -}: { - history: ScopedHistory; - navigateToApp: ApplicationStart['navigateToApp']; - getUrlForApp: ApplicationStart['getUrlForApp']; -}) => ( - - - -); - -export const App = ({ - navigateToApp, - getUrlForApp, -}: { - navigateToApp: ApplicationStart['navigateToApp']; - getUrlForApp: ApplicationStart['getUrlForApp']; -}) => { +export const App = ({ history }: { history: ScopedHistory }) => { useEffect(() => trackUiMetric(METRIC_TYPE.LOADED, UIM_APP_LOAD), []); return ( - - - } - /> - } - /> - + + + + + + + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/components/index_templates_flyout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/components/index_templates_flyout.tsx new file mode 100644 index 0000000000000..abfda9fce4ea4 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/components/index_templates_flyout.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + EuiButtonEmpty, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiInMemoryTable, + EuiLink, + EuiTitle, +} from '@elastic/eui'; +import { PolicyFromES } from '../../../common/types'; +import { useKibana } from '../../shared_imports'; +import { getTemplateDetailsLink } from '../../../../index_management/public/'; + +interface Props { + policy: PolicyFromES; + close: () => void; +} +export const IndexTemplatesFlyout: FunctionComponent = ({ policy, close }) => { + const { + services: { getUrlForApp }, + } = useKibana(); + const getUrlForIndexTemplate = (name: string) => { + return getUrlForApp('management', { + path: `data/index_management${getTemplateDetailsLink(name)}`, + }); + }; + return ( + + + +

+ +

+
+
+ + { + return ( + + {value} + + ); + }, + }, + ]} + /> + + + + + + +
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/index.tsx b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx index ed0a4a83b06db..e158be26dfcc7 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/index.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx @@ -14,30 +14,31 @@ import { ILicense } from '../../../licensing/public'; import { KibanaContextProvider } from '../shared_imports'; -import { AppWithRouter } from './app'; +import { App } from './app'; import { BreadcrumbService } from './services/breadcrumbs'; +import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public'; export const renderApp = ( element: Element, I18nContext: I18nStart['Context'], history: ScopedHistory, - navigateToApp: ApplicationStart['navigateToApp'], - getUrlForApp: ApplicationStart['getUrlForApp'], + application: ApplicationStart, breadcrumbService: BreadcrumbService, license: ILicense, cloud?: CloudSetup ): UnmountCallback => { + const { navigateToApp, getUrlForApp } = application; render( - - - - - , + + + + + + + , element ); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_age_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_age_field.tsx index 2d3704e252ac8..7fbdaf344b8fa 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_age_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_age_field.tsx @@ -9,16 +9,17 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { NumericField, SelectField } from '../../../../../../../shared_imports'; +import { NumericField } from '../../../../../../../shared_imports'; import { UseField } from '../../../../form'; import { ROLLOVER_FORM_PATHS } from '../../../../constants'; +import { UnitField } from './unit_field'; import { maxAgeUnits } from '../constants'; export const MaxAgeField: FunctionComponent = () => { return ( - - + + { euiFieldProps: { 'data-test-subj': `hot-selectedMaxAge`, min: 1, - }, - }} - /> - - - ), - options: maxAgeUnits, }, }} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_document_count_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_document_count_field.tsx index c6f8abef3f2c2..e847d773e2a88 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_document_count_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_document_count_field.tsx @@ -14,8 +14,8 @@ import { ROLLOVER_FORM_PATHS } from '../../../../constants'; export const MaxDocumentCountField: FunctionComponent = () => { return ( - - + + { return ( - - + + { content={i18nTexts.deprecationMessage} /> ), - min: 1, - }, - }} - /> - - - + ), }, }} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_primary_shard_size_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_primary_shard_size_field.tsx index eed23bd48a0fa..d9c7ed5a24b99 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_primary_shard_size_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/max_primary_shard_size_field.tsx @@ -9,16 +9,17 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { NumericField, SelectField } from '../../../../../../../shared_imports'; +import { NumericField } from '../../../../../../../shared_imports'; import { UseField } from '../../../../form'; import { ROLLOVER_FORM_PATHS } from '../../../../constants'; +import { UnitField } from './unit_field'; import { maxSizeStoredUnits } from '../constants'; export const MaxPrimaryShardSizeField: FunctionComponent = () => { return ( - - + + { euiFieldProps: { 'data-test-subj': 'hot-selectedMaxPrimaryShardSize', min: 1, - }, - }} - /> - - - ), }, }} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/unit_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/unit_field.tsx new file mode 100644 index 0000000000000..2ef8917d53989 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/components/unit_field.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent, useState } from 'react'; +import { EuiFilterSelectItem, EuiPopover, EuiButtonEmpty } from '@elastic/eui'; + +import { UseField } from '../../../../form'; + +interface Props { + path: string; + euiFieldProps?: Record; + options: Array<{ + value: string; + text: string; + }>; +} + +export const UnitField: FunctionComponent = ({ path, options, euiFieldProps }) => { + const [open, setOpen] = useState(false); + + return ( + + {(field) => { + const onSelect = (option: string) => { + field.setValue(option); + setOpen(false); + }; + + return ( + setOpen((x) => !x)} + data-test-subj="show-filters-button" + > + {options.find((x) => x.value === field.value)?.text} + + } + ownFocus + panelPaddingSize="none" + isOpen={open} + closePopover={() => setOpen(false)} + {...euiFieldProps} + > + {options.map((item) => ( + onSelect(item.value)} + data-test-subj={`filter-option-${item.value}`} + > + {item.text} + + ))} + + ); + }} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx index 3c30c6d3a678f..d6a36b99c20aa 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx @@ -135,6 +135,7 @@ export const HotPhase: FunctionComponent = () => { {showEmptyRolloverFieldsError && ( <> = ({ canBeDisabled = true, }) => { const { - services: { cloud }, + services: { cloud, getUrlForApp }, } = useKibana(); - const { getUrlForApp, policy, license, isNewPolicy } = useEditPolicyContext(); + const { policy, license, isNewPolicy } = useEditPolicyContext(); const { isUsingSearchableSnapshotInHotPhase } = useConfiguration(); const searchableSnapshotRepoPath = `phases.${phase}.actions.searchable_snapshot.snapshot_repository`; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/snapshot_policies_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/snapshot_policies_field.tsx index 21dd083ccf7c5..720d39695cf0e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/snapshot_policies_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/snapshot_policies_field.tsx @@ -18,10 +18,9 @@ import { EuiSpacer, } from '@elastic/eui'; -import { ComboBoxField, useFormData } from '../../../../../../shared_imports'; +import { ComboBoxField, useFormData, useKibana } from '../../../../../../shared_imports'; import { useLoadSnapshotPolicies } from '../../../../../services/api'; -import { useEditPolicyContext } from '../../../edit_policy_context'; import { UseField } from '../../../form'; import { FieldLoadingError, LearnMoreLink, OptionalLabel } from '../../'; @@ -29,7 +28,9 @@ import { FieldLoadingError, LearnMoreLink, OptionalLabel } from '../../'; const waitForSnapshotFormField = 'phases.delete.actions.wait_for_snapshot.policy'; export const SnapshotPoliciesField: React.FunctionComponent = () => { - const { getUrlForApp } = useEditPolicyContext(); + const { + services: { getUrlForApp }, + } = useKibana(); const { error, isLoading, data, resendRequest } = useLoadSnapshotPolicies(); const [formData] = useFormData({ watch: waitForSnapshotFormField, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss index 983ef0ab20f69..a69f244a25aea 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss @@ -10,6 +10,7 @@ $ilmTimelineBarHeight: $euiSizeS; */ display: inline-block; width: 100%; + margin-top: $euiSizeS; &__phase:first-child { padding-left: 0; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx index da63bc87424d6..90a3b0f14459d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx @@ -24,22 +24,10 @@ interface RouterProps { policyName: string; } -interface Props { - getUrlForApp: ( - appId: string, - options?: { - path?: string; - absolute?: boolean; - } - ) => string; -} - -export const EditPolicy: React.FunctionComponent> = ({ +export const EditPolicy: React.FunctionComponent> = ({ match: { params: { policyName }, }, - getUrlForApp, - history, }) => { const { services: { breadcrumbService, license }, @@ -105,13 +93,12 @@ export const EditPolicy: React.FunctionComponent license.hasAtLeast(MIN_SEARCHABLE_SNAPSHOT_LICENSE), }, }} > - + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx index 172e8259b87af..371be58920d07 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx @@ -10,7 +10,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment, useEffect, useMemo, useState } from 'react'; import { get } from 'lodash'; -import { RouteComponentProps } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; import { EuiButton, @@ -50,13 +50,9 @@ import { import { useEditPolicyContext } from './edit_policy_context'; import { FormInternal } from './types'; -export interface Props { - history: RouteComponentProps['history']; -} - const policyNamePath = 'name'; -export const EditPolicy: React.FunctionComponent = ({ history }) => { +export const EditPolicy: React.FunctionComponent = () => { useEffect(() => { window.scrollTo(0, 0); }, []); @@ -119,6 +115,7 @@ export const EditPolicy: React.FunctionComponent = ({ history }) => { [originalPolicyName, existingPolicies, saveAsNew] ); + const history = useHistory(); const backToPolicyList = () => { history.push('/policies'); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx index 45f0fe8544c98..9414f27c72ea9 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx @@ -6,7 +6,6 @@ */ import React, { createContext, ReactChild, useContext } from 'react'; -import { ApplicationStart } from 'kibana/public'; import { PolicyFromES, SerializedPolicy } from '../../../../common/types'; @@ -14,7 +13,6 @@ export interface EditPolicyContextValue { isNewPolicy: boolean; policy: SerializedPolicy; existingPolicies: PolicyFromES[]; - getUrlForApp: ApplicationStart['getUrlForApp']; license: { canUseSearchableSnapshot: () => boolean; }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx index 3d81bd03c3655..94d815b7ef932 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx @@ -20,7 +20,6 @@ import { EuiTablePagination, EuiTableRow, EuiTableRowCell, - EuiText, Pager, EuiContextMenuPanelDescriptor, } from '@elastic/eui'; @@ -30,12 +29,12 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; import moment from 'moment'; -import { ApplicationStart } from 'kibana/public'; import { METRIC_TYPE } from '@kbn/analytics'; -import { RouteComponentProps } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; import { getIndexListUri } from '../../../../../../index_management/public'; import { PolicyFromES } from '../../../../../common/types'; +import { useKibana } from '../../../../shared_imports'; import { getPolicyEditPath } from '../../../services/navigation'; import { sortTable } from '../../../services'; import { trackUiMetric } from '../../../services/ui_metric'; @@ -44,6 +43,7 @@ import { UIM_EDIT_CLICK } from '../../../constants'; import { TableColumn } from '../index'; import { AddPolicyToTemplateConfirmModal } from './add_policy_to_template_confirm_modal'; import { ConfirmDelete } from './confirm_delete'; +import { IndexTemplatesFlyout } from '../../../components/index_templates_flyout'; const COLUMNS: Array<[TableColumn, { label: string; width: number }]> = [ [ @@ -64,6 +64,18 @@ const COLUMNS: Array<[TableColumn, { label: string; width: number }]> = [ width: 120, }, ], + [ + 'indexTemplates', + { + label: i18n.translate( + 'xpack.indexLifecycleMgmt.policyTable.headers.linkedIndexTemplatesHeader', + { + defaultMessage: 'Linked index templates', + } + ), + width: 160, + }, + ], [ 'version', { @@ -87,18 +99,14 @@ const COLUMNS: Array<[TableColumn, { label: string; width: number }]> = [ interface Props { policies: PolicyFromES[]; totalNumber: number; - navigateToApp: ApplicationStart['navigateToApp']; setConfirmModal: (modal: ReactElement | null) => void; handleDelete: () => void; - history: RouteComponentProps['history']; } export const TableContent: React.FunctionComponent = ({ policies, totalNumber, - navigateToApp, setConfirmModal, handleDelete, - history, }) => { const [popoverPolicy, setPopoverPolicy] = useState(); const [sort, setSort] = useState<{ sortField: TableColumn; isSortAscending: boolean }>({ @@ -107,6 +115,10 @@ export const TableContent: React.FunctionComponent = ({ }); const [pageSize, setPageSize] = useState(10); const [currentPage, setCurrentPage] = useState(0); + const history = useHistory(); + const { + services: { navigateToApp }, + } = useKibana(); let sortedPolicies = sortTable(policies, sort.sortField, sort.isSortAscending); const pager = new Pager(totalNumber, pageSize, currentPage); @@ -224,7 +236,11 @@ export const TableContent: React.FunctionComponent = ({ return [panelTree]; }; - const renderRowCell = (fieldName: string, value: string | number | string[]): ReactNode => { + const renderRowCell = ( + fieldName: string, + value: string | number | string[], + policy?: PolicyFromES + ): ReactNode => { if (fieldName === 'name') { return ( = ({ ); } else if (fieldName === 'indices') { - return ( - - {value ? (value as string[]).length : '0'} - + return value ? (value as string[]).length : '0'; + } else if (fieldName === 'indexTemplates' && policy) { + return value && (value as string[]).length > 0 ? ( + setConfirmModal(renderIndexTemplatesFlyout(policy))} + > + {(value as string[]).length} + + ) : ( + '0' ); } else if (fieldName === 'modifiedDate' && value) { return moment(value).format('YYYY-MM-DD HH:mm:ss'); @@ -276,7 +300,7 @@ export const TableContent: React.FunctionComponent = ({ className={'policyTable__content--' + fieldName} width={width} > - {renderRowCell(fieldName, value)} + {renderRowCell(fieldName, value, policy)} ); } @@ -319,7 +343,7 @@ export const TableContent: React.FunctionComponent = ({ const rows = sortedPolicies.map((policy) => { const { name } = policy; return ( - + {renderRowCells(policy)} ); @@ -343,6 +367,17 @@ export const TableContent: React.FunctionComponent = ({ ); }; + const renderIndexTemplatesFlyout = (policy: PolicyFromES): ReactElement => { + return ( + { + setConfirmModal(null); + }} + /> + ); + }; + const renderPager = (): ReactNode => { return ( ; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.container.tsx index ad8d1ed87f5f2..376832bc99061 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.container.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.container.tsx @@ -6,22 +6,13 @@ */ import React, { useEffect } from 'react'; -import { ApplicationStart } from 'kibana/public'; -import { RouteComponentProps } from 'react-router-dom'; import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner, EuiPageContent } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { PolicyTable as PresentationComponent } from './policy_table'; import { useKibana } from '../../../shared_imports'; import { useLoadPoliciesList } from '../../services/api'; -interface Props { - navigateToApp: ApplicationStart['navigateToApp']; -} - -export const PolicyTable: React.FunctionComponent = ({ - navigateToApp, - history, -}) => { +export const PolicyTable: React.FunctionComponent = () => { const { services: { breadcrumbService }, } = useKibana(); @@ -77,12 +68,5 @@ export const PolicyTable: React.FunctionComponent = ); } - return ( - - ); + return ; }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.tsx index 30a2b9e68d69d..28cc2b17dcbff 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.tsx @@ -19,8 +19,7 @@ import { EuiPageHeader, EuiPageContent, } from '@elastic/eui'; -import { ApplicationStart } from 'kibana/public'; -import { RouteComponentProps } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; import { PolicyFromES } from '../../../../common/types'; import { filterItems } from '../../services'; @@ -29,19 +28,13 @@ import { getPolicyCreatePath } from '../../services/navigation'; interface Props { policies: PolicyFromES[]; - history: RouteComponentProps['history']; - navigateToApp: ApplicationStart['navigateToApp']; updatePolicies: () => void; } -export const PolicyTable: React.FunctionComponent = ({ - policies, - history, - navigateToApp, - updatePolicies, -}) => { +export const PolicyTable: React.FunctionComponent = ({ policies, updatePolicies }) => { const [confirmModal, setConfirmModal] = useState(); const [filter, setFilter] = useState(''); + const history = useHistory(); const createPolicyButton = ( = ({ { updatePolicies(); setConfirmModal(null); }} - history={history} /> ); } else { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.ts index a1e43d3b0d745..8c381f1e90daf 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.ts @@ -15,7 +15,7 @@ export const sortTable = ( isSortAscending: boolean ): PolicyFromES[] => { let sorter; - if (sortField === 'indices') { + if (sortField === 'indices' || sortField === 'indexTemplates') { sorter = (item: PolicyFromES) => (item[sortField] || []).length; } else { sorter = (item: PolicyFromES) => item[sortField]; diff --git a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx index 163fe2b3d9b5c..8381122b49b5e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx @@ -55,7 +55,7 @@ export class IndexLifecycleManagementPlugin chrome: { docTitle }, i18n: { Context: I18nContext }, docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, - application: { navigateToApp, getUrlForApp }, + application, } = coreStart; const license = await licensing.license$.pipe(first()).toPromise(); @@ -74,8 +74,7 @@ export class IndexLifecycleManagementPlugin element, I18nContext, history, - navigateToApp, - getUrlForApp, + application, this.breadcrumbService, license, cloud diff --git a/x-pack/plugins/index_lifecycle_management/public/types.ts b/x-pack/plugins/index_lifecycle_management/public/types.ts index adfca9ad41b26..c54f5620a2859 100644 --- a/x-pack/plugins/index_lifecycle_management/public/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/types.ts @@ -4,6 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + +import { ApplicationStart } from 'kibana/public'; import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; import { ManagementSetup } from '../../../../src/plugins/management/public'; @@ -37,4 +39,6 @@ export interface AppServicesContext { breadcrumbService: BreadcrumbService; license: ILicense; cloud?: CloudSetup; + navigateToApp: ApplicationStart['navigateToApp']; + getUrlForApp: ApplicationStart['getUrlForApp']; } diff --git a/x-pack/plugins/index_lifecycle_management/server/plugin.ts b/x-pack/plugins/index_lifecycle_management/server/plugin.ts index 95793c0cad465..533d8736931a4 100644 --- a/x-pack/plugins/index_lifecycle_management/server/plugin.ts +++ b/x-pack/plugins/index_lifecycle_management/server/plugin.ts @@ -6,44 +6,36 @@ */ import { i18n } from '@kbn/i18n'; -import { - CoreSetup, - Plugin, - Logger, - PluginInitializerContext, - LegacyAPICaller, -} from 'src/core/server'; -import { handleEsError } from './shared_imports'; +import { CoreSetup, Plugin, Logger, PluginInitializerContext } from 'src/core/server'; +import { IScopedClusterClient } from 'kibana/server'; import { Index as IndexWithoutIlm } from '../../index_management/common/types'; import { PLUGIN } from '../common/constants'; -import { Index, IndexLifecyclePolicy } from '../common/types'; +import { Index } from '../common/types'; import { Dependencies } from './types'; import { registerApiRoutes } from './routes'; import { License } from './services'; import { IndexLifecycleManagementConfig } from './config'; +import { handleEsError } from './shared_imports'; const indexLifecycleDataEnricher = async ( indicesList: IndexWithoutIlm[], - // TODO replace deprecated ES client after Index Management is updated - callAsCurrentUser: LegacyAPICaller + client: IScopedClusterClient ): Promise => { if (!indicesList || !indicesList.length) { return []; } - const params = { - path: '/*/_ilm/explain', - method: 'GET', - }; - - const { indices: ilmIndicesData } = await callAsCurrentUser<{ - indices: { [indexName: string]: IndexLifecyclePolicy }; - }>('transport.request', params); + const { + body: { indices: ilmIndicesData }, + } = await client.asCurrentUser.ilm.explainLifecycle({ + index: '*', + }); return indicesList.map((index: IndexWithoutIlm) => { return { ...index, + // @ts-expect-error @elastic/elasticsearch Element implicitly has an 'any' type ilm: { ...(ilmIndicesData[index.name] || {}) }, }; }); diff --git a/x-pack/plugins/index_management/README.md b/x-pack/plugins/index_management/README.md index 07c5b9317b5cb..39f4821403a8d 100644 --- a/x-pack/plugins/index_management/README.md +++ b/x-pack/plugins/index_management/README.md @@ -1,5 +1,16 @@ # Index Management UI +## Indices tab + +### Quick steps for testing + +Create an index with special characters and verify it renders correctly: + +``` +# Renders as %{[@metadata][beat]}-%{[@metadata][version]}-2020.08.23 +PUT %25%7B%5B%40metadata%5D%5Bbeat%5D%7D-%25%7B%5B%40metadata%5D%5Bversion%5D%7D-2020.08.23 +``` + ## Data streams tab ### Quick steps for testing @@ -19,4 +30,56 @@ POST ds/_doc { "@timestamp": "2020-01-27" } -``` \ No newline at end of file +``` + +Create a data stream with special characters and verify it renders correctly: + +``` +# Configure template for creating a data stream +PUT _index_template/special_ds +{ + "index_patterns": ["%{[@metadata][beat]}-%{[@metadata][version]}-2020.08.23"], + "data_stream": {} +} + +# Add a document to the data stream, which will render as %{[@metadata][beat]}-%{[@metadata][version]}-2020.08.23 +POST %25%7B%5B%40metadata%5D%5Bbeat%5D%7D-%25%7B%5B%40metadata%5D%5Bversion%5D%7D-2020.08.23/_doc +{ + "@timestamp": "2020-01-27" +} +``` + +## Index templates tab + +### Quick steps for testing + +By default, **legacy index templates** are not shown in the UI. Make them appear by creating one in Console: + +``` +PUT _template/template_1 +{ + "index_patterns": ["foo*"] +} +``` + +To test **Cloud-managed templates**: + +1. Add `cluster.metadata.managed_index_templates` setting via Dev Tools: +``` +PUT /_cluster/settings +{ + "persistent": { + "cluster.metadata.managed_index_templates": ".cloud-" + } +} +``` + +2. Create a template with the format: `.cloud-` via Dev Tools. +``` +PUT _template/.cloud-example +{ + "index_patterns": [ "foobar*"] +} +``` + +The UI will now prevent you from editing or deleting this template. \ No newline at end of file diff --git a/x-pack/plugins/index_management/public/application/components/index_templates/simulate_template/simulate_template.tsx b/x-pack/plugins/index_management/public/application/components/index_templates/simulate_template/simulate_template.tsx index 946ce46ba2626..82c3b35e0a91b 100644 --- a/x-pack/plugins/index_management/public/application/components/index_templates/simulate_template/simulate_template.tsx +++ b/x-pack/plugins/index_management/public/application/components/index_templates/simulate_template/simulate_template.tsx @@ -6,7 +6,6 @@ */ import React, { useState, useCallback, useEffect } from 'react'; -import uuid from 'uuid'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCodeBlock, EuiCallOut } from '@elastic/eui'; @@ -37,11 +36,6 @@ export const SimulateTemplate = React.memo(({ template, filters }: Props) => { } const indexTemplate = serializeTemplate(stripEmptyFields(template) as TemplateDeserialized); - - // Until ES fixes a bug on their side we will send a random index pattern to the simulate API. - // Issue: https://github.com/elastic/elasticsearch/issues/59152 - indexTemplate.index_patterns = [uuid.v4()]; - const { data, error } = await simulateIndexTemplate(indexTemplate); let filteredTemplate = data; diff --git a/x-pack/plugins/index_management/public/index.ts b/x-pack/plugins/index_management/public/index.ts index 3df5fef4b02fc..10feabf0a9d0f 100644 --- a/x-pack/plugins/index_management/public/index.ts +++ b/x-pack/plugins/index_management/public/index.ts @@ -15,6 +15,6 @@ export const plugin = () => { export { IndexManagementPluginSetup } from './types'; -export { getIndexListUri } from './application/services/routing'; +export { getIndexListUri, getTemplateDetailsLink } from './application/services/routing'; export type { Index } from '../common'; diff --git a/x-pack/plugins/index_management/server/client/elasticsearch.ts b/x-pack/plugins/index_management/server/client/elasticsearch.ts deleted file mode 100644 index 9585680ce3b6d..0000000000000 --- a/x-pack/plugins/index_management/server/client/elasticsearch.ts +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const elasticsearchJsPlugin = (Client: any, config: any, components: any) => { - const ca = components.clientAction.factory; - - Client.prototype.dataManagement = components.clientAction.namespaceFactory(); - const dataManagement = Client.prototype.dataManagement.prototype; - - // Data streams - - // We don't allow the user to create a data stream in the UI or API. We're just adding this here - // to enable the API integration tests. - dataManagement.createDataStream = ca({ - urls: [ - { - fmt: '/_data_stream/<%=name%>', - req: { - name: { - type: 'string', - }, - }, - }, - ], - method: 'PUT', - }); - - dataManagement.deleteDataStream = ca({ - urls: [ - { - fmt: '/_data_stream/<%=name%>', - req: { - name: { - type: 'string', - }, - }, - }, - ], - method: 'DELETE', - }); - - // Component templates - dataManagement.getComponentTemplates = ca({ - urls: [ - { - fmt: '/_component_template', - }, - ], - method: 'GET', - }); - - dataManagement.getComponentTemplate = ca({ - urls: [ - { - fmt: '/_component_template/<%=name%>', - req: { - name: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - dataManagement.saveComponentTemplate = ca({ - urls: [ - { - fmt: '/_component_template/<%=name%>', - req: { - name: { - type: 'string', - }, - }, - }, - ], - method: 'PUT', - }); - - dataManagement.deleteComponentTemplate = ca({ - urls: [ - { - fmt: '/_component_template/<%=name%>', - req: { - name: { - type: 'string', - }, - }, - }, - ], - method: 'DELETE', - }); - - // Composable index templates - dataManagement.getComposableIndexTemplates = ca({ - urls: [ - { - fmt: '/_index_template', - }, - ], - method: 'GET', - }); - - dataManagement.getComposableIndexTemplate = ca({ - urls: [ - { - fmt: '/_index_template/<%=name%>', - req: { - name: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - dataManagement.saveComposableIndexTemplate = ca({ - urls: [ - { - fmt: '/_index_template/<%=name%>', - req: { - name: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'PUT', - }); - - dataManagement.deleteComposableIndexTemplate = ca({ - urls: [ - { - fmt: '/_index_template/<%=name%>', - req: { - name: { - type: 'string', - }, - }, - }, - ], - method: 'DELETE', - }); - - dataManagement.existsTemplate = ca({ - urls: [ - { - fmt: '/_index_template/<%=name%>', - req: { - name: { - type: 'string', - }, - }, - }, - ], - method: 'HEAD', - }); - - dataManagement.simulateTemplate = ca({ - urls: [ - { - fmt: '/_index_template/_simulate', - }, - ], - needBody: true, - method: 'POST', - }); -}; diff --git a/x-pack/plugins/index_management/server/index.ts b/x-pack/plugins/index_management/server/index.ts index 1ca2705dc93ea..507401398a407 100644 --- a/x-pack/plugins/index_management/server/index.ts +++ b/x-pack/plugins/index_management/server/index.ts @@ -10,7 +10,7 @@ import { PluginInitializerContext } from 'src/core/server'; import { IndexMgmtServerPlugin } from './plugin'; import { configSchema } from './config'; -export const plugin = (ctx: PluginInitializerContext) => new IndexMgmtServerPlugin(ctx); +export const plugin = (context: PluginInitializerContext) => new IndexMgmtServerPlugin(context); export const config = { schema: configSchema, diff --git a/x-pack/plugins/index_management/server/lib/fetch_indices.ts b/x-pack/plugins/index_management/server/lib/fetch_indices.ts index b83843f0c615d..48f633a8dc102 100644 --- a/x-pack/plugins/index_management/server/lib/fetch_indices.ts +++ b/x-pack/plugins/index_management/server/lib/fetch_indices.ts @@ -5,99 +5,57 @@ * 2.0. */ -import { CatIndicesParams } from 'elasticsearch'; +import { IScopedClusterClient } from 'kibana/server'; import { IndexDataEnricher } from '../services'; -import { CallAsCurrentUser } from '../types'; import { Index } from '../index'; -interface Hit { - health: string; - status: string; - index: string; - uuid: string; - pri: string; - rep: string; - 'docs.count': any; - 'store.size': any; - sth: 'true' | 'false'; - hidden: boolean; -} - -interface IndexInfo { - aliases: { [aliasName: string]: unknown }; - mappings: unknown; - data_stream?: string; - settings: { - index: { - hidden: 'true' | 'false'; - }; - }; -} - -interface GetIndicesResponse { - [indexName: string]: IndexInfo; -} - async function fetchIndicesCall( - callAsCurrentUser: CallAsCurrentUser, + client: IScopedClusterClient, indexNames?: string[] ): Promise { const indexNamesString = indexNames && indexNames.length ? indexNames.join(',') : '*'; // This call retrieves alias and settings (incl. hidden status) information about indices - const indices: GetIndicesResponse = await callAsCurrentUser('transport.request', { - method: 'GET', - // transport.request doesn't do any URI encoding, unlike other JS client APIs. This enables - // working with Logstash indices with names like %{[@metadata][beat]}-%{[@metadata][version]}. - path: `/${encodeURIComponent(indexNamesString)}`, - query: { - expand_wildcards: 'hidden,all', - }, + const { body: indices } = await client.asCurrentUser.indices.get({ + index: indexNamesString, + expand_wildcards: 'hidden,all', }); if (!Object.keys(indices).length) { return []; } - const catQuery: Pick & { - expand_wildcards: string; - index?: string; - } = { + const { body: catHits } = await client.asCurrentUser.cat.indices({ format: 'json', h: 'health,status,index,uuid,pri,rep,docs.count,sth,store.size', expand_wildcards: 'hidden,all', index: indexNamesString, - }; - - // This call retrieves health and other high-level information about indices. - const catHits: Hit[] = await callAsCurrentUser('transport.request', { - method: 'GET', - path: '/_cat/indices', - query: catQuery, }); // System indices may show up in _cat APIs, as these APIs are primarily used for troubleshooting // For now, we filter them out and only return index information for the indices we have // In the future, we should migrate away from using cat APIs (https://github.com/elastic/kibana/issues/57286) return catHits.reduce((decoratedIndices, hit) => { - const index = indices[hit.index]; + const index = indices[hit.index!]; if (typeof index !== 'undefined') { - const aliases = Object.keys(index.aliases); + const aliases = Object.keys(index.aliases!); decoratedIndices.push({ - health: hit.health, - status: hit.status, - name: hit.index, - uuid: hit.uuid, - primary: hit.pri, - replica: hit.rep, + health: hit.health!, + status: hit.status!, + name: hit.index!, + uuid: hit.uuid!, + primary: hit.pri!, + replica: hit.rep!, documents: hit['docs.count'], size: hit['store.size'], isFrozen: hit.sth === 'true', // sth value coming back as a string from ES aliases: aliases.length ? aliases : 'none', + // @ts-expect-error @elastic/elasticsearch Property 'index' does not exist on type 'IndicesIndexSettings | IndicesIndexStatePrefixedSettings'. hidden: index.settings.index.hidden === 'true', - data_stream: index.data_stream, + // @ts-expect-error @elastic/elasticsearch Property 'data_stream' does not exist on type 'IndicesIndexState'. + data_stream: index.data_stream!, }); } @@ -106,10 +64,10 @@ async function fetchIndicesCall( } export const fetchIndices = async ( - callAsCurrentUser: CallAsCurrentUser, + client: IScopedClusterClient, indexDataEnricher: IndexDataEnricher, indexNames?: string[] ) => { - const indices = await fetchIndicesCall(callAsCurrentUser, indexNames); - return await indexDataEnricher.enrichIndices(indices, callAsCurrentUser); + const indices = await fetchIndicesCall(client, indexNames); + return await indexDataEnricher.enrichIndices(indices, client); }; diff --git a/x-pack/plugins/index_management/server/lib/get_managed_templates.ts b/x-pack/plugins/index_management/server/lib/get_managed_templates.ts index df0f4f12d9719..8e60044dc4951 100644 --- a/x-pack/plugins/index_management/server/lib/get_managed_templates.ts +++ b/x-pack/plugins/index_management/server/lib/get_managed_templates.ts @@ -5,16 +5,20 @@ * 2.0. */ +import { IScopedClusterClient } from 'kibana/server'; + // Cloud has its own system for managing templates and we want to make // this clear in the UI when a template is used in a Cloud deployment. export const getCloudManagedTemplatePrefix = async ( - callAsCurrentUser: any + client: IScopedClusterClient ): Promise => { try { - const { persistent, transient, defaults } = await callAsCurrentUser('cluster.getSettings', { - filterPath: '*.*managed_index_templates', - flatSettings: true, - includeDefaults: true, + const { + body: { persistent, transient, defaults }, + } = await client.asCurrentUser.cluster.getSettings({ + filter_path: '*.*managed_index_templates', + flat_settings: true, + include_defaults: true, }); const { 'cluster.metadata.managed_index_templates': managedTemplatesPrefix = undefined } = { diff --git a/x-pack/plugins/index_management/server/plugin.ts b/x-pack/plugins/index_management/server/plugin.ts index 35d25eb452b84..a339349c0a5a9 100644 --- a/x-pack/plugins/index_management/server/plugin.ts +++ b/x-pack/plugins/index_management/server/plugin.ts @@ -5,20 +5,13 @@ * 2.0. */ -import { - CoreSetup, - Plugin, - PluginInitializerContext, - ILegacyCustomClusterClient, -} from 'src/core/server'; +import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/server'; import { PLUGIN } from '../common/constants/plugin'; import { Dependencies } from './types'; import { ApiRoutes } from './routes'; import { IndexDataEnricher } from './services'; -import { isEsError, handleEsError, parseEsError } from './shared_imports'; -import { elasticsearchJsPlugin } from './client/elasticsearch'; -import type { IndexManagementRequestHandlerContext } from './types'; +import { handleEsError } from './shared_imports'; export interface IndexManagementPluginSetup { indexDataEnricher: { @@ -26,16 +19,9 @@ export interface IndexManagementPluginSetup { }; } -async function getCustomEsClient(getStartServices: CoreSetup['getStartServices']) { - const [core] = await getStartServices(); - const esClientConfig = { plugins: [elasticsearchJsPlugin] }; - return core.elasticsearch.legacy.createClient('dataManagement', esClientConfig); -} - export class IndexMgmtServerPlugin implements Plugin { private readonly apiRoutes: ApiRoutes; private readonly indexDataEnricher: IndexDataEnricher; - private dataManagementESClient?: ILegacyCustomClusterClient; constructor(initContext: PluginInitializerContext) { this.apiRoutes = new ApiRoutes(); @@ -46,8 +32,6 @@ export class IndexMgmtServerPlugin implements Plugin(); - features.registerElasticsearchFeature({ id: PLUGIN.id, management: { @@ -63,27 +47,13 @@ export class IndexMgmtServerPlugin implements Plugin( - 'dataManagement', - async (ctx, request) => { - this.dataManagementESClient = - this.dataManagementESClient ?? (await getCustomEsClient(getStartServices)); - - return { - client: this.dataManagementESClient.asScoped(request), - }; - } - ); - this.apiRoutes.setup({ - router, + router: http.createRouter(), config: { isSecurityEnabled: () => security !== undefined && security.license.isEnabled(), }, indexDataEnricher: this.indexDataEnricher, lib: { - isEsError, - parseEsError, handleEsError, }, }); @@ -97,9 +67,5 @@ export class IndexMgmtServerPlugin implements Plugin { +export const registerCreateRoute = ({ + router, + lib: { handleEsError }, +}: RouteDependencies): void => { router.post( { path: addBasePath('/component_templates'), @@ -20,24 +23,23 @@ export const registerCreateRoute = ({ router, lib: { isEsError } }: RouteDepende body: componentTemplateSchema, }, }, - async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.dataManagement!.client; + async (context, request, response) => { + const { client } = context.core.elasticsearch; - const serializedComponentTemplate = serializeComponentTemplate(req.body); + const serializedComponentTemplate = serializeComponentTemplate(request.body); - const { name } = req.body; + const { name } = request.body; try { // Check that a component template with the same name doesn't already exist - const componentTemplateResponse = await callAsCurrentUser( - 'dataManagement.getComponentTemplate', - { name } - ); - - const { component_templates: componentTemplates } = componentTemplateResponse; + const { + body: { component_templates: componentTemplates }, + } = await client.asCurrentUser.cluster.getComponentTemplate({ + name, + }); if (componentTemplates.length) { - return res.conflict({ + return response.conflict({ body: new Error( i18n.translate('xpack.idxMgmt.componentTemplates.createRoute.duplicateErrorMessage', { defaultMessage: "There is already a component template with name '{name}'.", @@ -53,21 +55,15 @@ export const registerCreateRoute = ({ router, lib: { isEsError } }: RouteDepende } try { - const response = await callAsCurrentUser('dataManagement.saveComponentTemplate', { + const { body: responseBody } = await client.asCurrentUser.cluster.putComponentTemplate({ name, + // @ts-expect-error @elastic/elasticsearch Type 'ComponentTemplateSerialized' is not assignable body: serializedComponentTemplate, }); - return res.ok({ body: response }); + return response.ok({ body: responseBody }); } catch (error) { - if (isEsError(error)) { - return res.customError({ - statusCode: error.statusCode, - body: error, - }); - } - - throw error; + return handleEsError({ error, response }); } } ); diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/delete.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/delete.ts index d30f54f6e44ad..67991ec708946 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/delete.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/delete.ts @@ -14,7 +14,10 @@ const paramsSchema = schema.object({ names: schema.string(), }); -export const registerDeleteRoute = ({ router }: RouteDependencies): void => { +export const registerDeleteRoute = ({ + router, + lib: { handleEsError }, +}: RouteDependencies): void => { router.delete( { path: addBasePath('/component_templates/{names}'), @@ -22,32 +25,34 @@ export const registerDeleteRoute = ({ router }: RouteDependencies): void => { params: paramsSchema, }, }, - async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.dataManagement!.client; - const { names } = req.params; + async (context, request, response) => { + const { client } = context.core.elasticsearch; + const { names } = request.params; const componentNames = names.split(','); - const response: { itemsDeleted: string[]; errors: any[] } = { + const responseBody: { itemsDeleted: string[]; errors: any[] } = { itemsDeleted: [], errors: [], }; await Promise.all( - componentNames.map((componentName) => { - return callAsCurrentUser('dataManagement.deleteComponentTemplate', { - name: componentName, - }) - .then(() => response.itemsDeleted.push(componentName)) - .catch((e) => - response.errors.push({ - name: componentName, - error: e, - }) - ); + componentNames.map(async (componentName) => { + try { + await client.asCurrentUser.cluster.deleteComponentTemplate({ + name: componentName, + }); + + return responseBody.itemsDeleted.push(componentName); + } catch (error) { + return responseBody.errors.push({ + name: componentName, + error: handleEsError({ error, response }), + }); + } }) ); - return res.ok({ body: response }); + return response.ok({ body: responseBody }); } ); }; diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts index a5d70e65f870a..a77aa90c52f73 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts @@ -19,25 +19,23 @@ const paramsSchema = schema.object({ name: schema.string(), }); -export function registerGetAllRoute({ router, lib: { isEsError } }: RouteDependencies) { +export function registerGetAllRoute({ router, lib: { handleEsError } }: RouteDependencies) { // Get all component templates router.get( { path: addBasePath('/component_templates'), validate: false }, - async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.dataManagement!.client; + async (context, request, response) => { + const { client } = context.core.elasticsearch; try { const { - component_templates: componentTemplates, - }: { component_templates: ComponentTemplateFromEs[] } = await callAsCurrentUser( - 'dataManagement.getComponentTemplates' - ); + body: { component_templates: componentTemplates }, + } = await client.asCurrentUser.cluster.getComponentTemplate(); - const { index_templates: indexTemplates } = await callAsCurrentUser( - 'dataManagement.getComposableIndexTemplates' - ); + const { + body: { index_templates: indexTemplates }, + } = await client.asCurrentUser.indices.getIndexTemplate(); - const body = componentTemplates.map((componentTemplate) => { + const body = componentTemplates.map((componentTemplate: ComponentTemplateFromEs) => { const deserializedComponentTemplateListItem = deserializeComponentTemplateList( componentTemplate, indexTemplates @@ -45,16 +43,9 @@ export function registerGetAllRoute({ router, lib: { isEsError } }: RouteDepende return deserializedComponentTemplateListItem; }); - return res.ok({ body }); + return response.ok({ body }); } catch (error) { - if (isEsError(error)) { - return res.customError({ - statusCode: error.statusCode, - body: error, - }); - } - - throw error; + return handleEsError({ error, response }); } } ); @@ -67,34 +58,26 @@ export function registerGetAllRoute({ router, lib: { isEsError } }: RouteDepende params: paramsSchema, }, }, - async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.dataManagement!.client; - const { name } = req.params; + async (context, request, response) => { + const { client } = context.core.elasticsearch; + const { name } = request.params; try { - const { component_templates: componentTemplates } = await callAsCurrentUser( - 'dataManagement.getComponentTemplates', - { - name, - } - ); + const { + body: { component_templates: componentTemplates }, + } = await client.asCurrentUser.cluster.getComponentTemplate({ + name, + }); - const { index_templates: indexTemplates } = await callAsCurrentUser( - 'dataManagement.getComposableIndexTemplates' - ); + const { + body: { index_templates: indexTemplates }, + } = await client.asCurrentUser.indices.getIndexTemplate(); - return res.ok({ + return response.ok({ body: deserializeComponentTemplate(componentTemplates[0], indexTemplates), }); } catch (error) { - if (isEsError(error)) { - return res.customError({ - statusCode: error.statusCode, - body: error, - }); - } - - throw error; + return handleEsError({ error, response }); } } ); diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.test.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.test.ts index eccf2d945785f..992c2f9ed29dd 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.test.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.test.ts @@ -18,17 +18,15 @@ const httpService = httpServiceMock.createSetupContract(); const mockedIndexDataEnricher = new IndexDataEnricher(); -const mockRouteContext = ({ - callAsCurrentUser, -}: { - callAsCurrentUser: any; -}): RequestHandlerContext => { +const mockRouteContext = ({ hasPrivileges }: { hasPrivileges: unknown }): RequestHandlerContext => { const routeContextMock = ({ core: { elasticsearch: { - legacy: { - client: { - callAsCurrentUser, + client: { + asCurrentUser: { + security: { + hasPrivileges, + }, }, }, }, @@ -51,8 +49,6 @@ describe('GET privileges', () => { }, indexDataEnricher: mockedIndexDataEnricher, lib: { - isEsError: jest.fn(), - parseEsError: jest.fn(), handleEsError: jest.fn(), }, }); @@ -62,15 +58,17 @@ describe('GET privileges', () => { it('should return the correct response when a user has privileges', async () => { const privilegesResponseMock = { - username: 'elastic', - has_all_requested: true, - cluster: { manage_index_templates: true }, - index: {}, - application: {}, + body: { + username: 'elastic', + has_all_requested: true, + cluster: { manage_index_templates: true }, + index: {}, + application: {}, + }, }; const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest.fn().mockResolvedValueOnce(privilegesResponseMock), + hasPrivileges: jest.fn().mockResolvedValueOnce(privilegesResponseMock), }); const request = httpServerMock.createKibanaRequest(); @@ -86,15 +84,17 @@ describe('GET privileges', () => { it('should return the correct response when a user does not have privileges', async () => { const privilegesResponseMock = { - username: 'elastic', - has_all_requested: false, - cluster: { manage_index_templates: false }, - index: {}, - application: {}, + body: { + username: 'elastic', + has_all_requested: false, + cluster: { manage_index_templates: false }, + index: {}, + application: {}, + }, }; const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest.fn().mockResolvedValueOnce(privilegesResponseMock), + hasPrivileges: jest.fn().mockResolvedValueOnce(privilegesResponseMock), }); const request = httpServerMock.createKibanaRequest(); @@ -119,8 +119,6 @@ describe('GET privileges', () => { }, indexDataEnricher: mockedIndexDataEnricher, lib: { - isEsError: jest.fn(), - parseEsError: jest.fn(), handleEsError: jest.fn(), }, }); @@ -130,7 +128,7 @@ describe('GET privileges', () => { it('should return the default privileges response', async () => { const routeContextMock = mockRouteContext({ - callAsCurrentUser: jest.fn(), + hasPrivileges: jest.fn(), }); const request = httpServerMock.createKibanaRequest(); diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.ts index 62ad93453091e..327e6421525c6 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.ts @@ -17,13 +17,17 @@ const extractMissingPrivileges = (privilegesObject: { [key: string]: boolean } = return privileges; }, []); -export const registerPrivilegesRoute = ({ router, config }: RouteDependencies) => { +export const registerPrivilegesRoute = ({ + router, + config, + lib: { handleEsError }, +}: RouteDependencies) => { router.get( { path: addBasePath('/component_templates/privileges'), validate: false, }, - async (ctx, req, res) => { + async (context, request, response) => { const privilegesResult: Privileges = { hasAllPrivileges: true, missingPrivileges: { @@ -33,38 +37,28 @@ export const registerPrivilegesRoute = ({ router, config }: RouteDependencies) = // Skip the privileges check if security is not enabled if (!config.isSecurityEnabled()) { - return res.ok({ body: privilegesResult }); + return response.ok({ body: privilegesResult }); } - const { - core: { - elasticsearch: { - legacy: { client }, - }, - }, - } = ctx; + const { client } = context.core.elasticsearch; try { - const { has_all_requested: hasAllPrivileges, cluster } = await client.callAsCurrentUser( - 'transport.request', - { - path: '/_security/user/_has_privileges', - method: 'POST', - body: { - cluster: ['manage_index_templates'], - }, - } - ); + const { + body: { has_all_requested: hasAllPrivileges, cluster }, + } = await client.asCurrentUser.security.hasPrivileges({ + body: { + cluster: ['manage_index_templates'], + }, + }); if (!hasAllPrivileges) { privilegesResult.missingPrivileges.cluster = extractMissingPrivileges(cluster); } privilegesResult.hasAllPrivileges = hasAllPrivileges; - - return res.ok({ body: privilegesResult }); - } catch (e) { - throw e; + return response.ok({ body: privilegesResult }); + } catch (error) { + return handleEsError({ error, response }); } } ); diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts index ee94b8f2b0082..17e14f1a39c42 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts @@ -15,7 +15,10 @@ const paramsSchema = schema.object({ name: schema.string(), }); -export const registerUpdateRoute = ({ router, lib: { isEsError } }: RouteDependencies): void => { +export const registerUpdateRoute = ({ + router, + lib: { handleEsError }, +}: RouteDependencies): void => { router.put( { path: addBasePath('/component_templates/{name}'), @@ -24,34 +27,28 @@ export const registerUpdateRoute = ({ router, lib: { isEsError } }: RouteDepende params: paramsSchema, }, }, - async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.dataManagement!.client; - const { name } = req.params; - const { template, version, _meta } = req.body; + async (context, request, response) => { + const { client } = context.core.elasticsearch; + const { name } = request.params; + const { template, version, _meta } = request.body; try { // Verify component exists; ES will throw 404 if not - await callAsCurrentUser('dataManagement.getComponentTemplate', { name }); + await client.asCurrentUser.cluster.getComponentTemplate({ name }); - const response = await callAsCurrentUser('dataManagement.saveComponentTemplate', { + const { body: responseBody } = await client.asCurrentUser.cluster.putComponentTemplate({ name, body: { + // @ts-expect-error @elastic/elasticsearch Not assignable to type 'IndicesIndexState' template, version, _meta, }, }); - return res.ok({ body: response }); + return response.ok({ body: responseBody }); } catch (error) { - if (isEsError(error)) { - return res.customError({ - statusCode: error.statusCode, - body: error, - }); - } - - throw error; + return handleEsError({ error, response }); } } ); diff --git a/x-pack/plugins/index_management/server/routes/api/data_streams/register_delete_route.ts b/x-pack/plugins/index_management/server/routes/api/data_streams/register_delete_route.ts index 49166f4823a02..9e7b57079cc3c 100644 --- a/x-pack/plugins/index_management/server/routes/api/data_streams/register_delete_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/data_streams/register_delete_route.ts @@ -9,23 +9,22 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; -import { wrapEsError } from '../../helpers'; const bodySchema = schema.object({ dataStreams: schema.arrayOf(schema.string()), }); -export function registerDeleteRoute({ router }: RouteDependencies) { +export function registerDeleteRoute({ router, lib: { handleEsError } }: RouteDependencies) { router.post( { path: addBasePath('/delete_data_streams'), validate: { body: bodySchema }, }, - async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.dataManagement!.client; - const { dataStreams } = req.body as TypeOf; + async (context, request, response) => { + const { client } = context.core.elasticsearch; + const { dataStreams } = request.body as TypeOf; - const response: { dataStreamsDeleted: string[]; errors: any[] } = { + const responseBody: { dataStreamsDeleted: string[]; errors: any[] } = { dataStreamsDeleted: [], errors: [], }; @@ -33,21 +32,21 @@ export function registerDeleteRoute({ router }: RouteDependencies) { await Promise.all( dataStreams.map(async (name: string) => { try { - await callAsCurrentUser('dataManagement.deleteDataStream', { + await client.asCurrentUser.indices.deleteDataStream({ name, }); - return response.dataStreamsDeleted.push(name); - } catch (e) { - return response.errors.push({ + return responseBody.dataStreamsDeleted.push(name); + } catch (error) { + return responseBody.errors.push({ name, - error: wrapEsError(e), + error: handleEsError({ error, response }), }); } }) ); - return res.ok({ body: response }); + return response.ok({ body: responseBody }); } ); } diff --git a/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts index 1ce7c14f0a209..c7b28b46e8f00 100644 --- a/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts @@ -7,7 +7,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; -import { ElasticsearchClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { deserializeDataStream, deserializeDataStreamList } from '../../../../common/lib'; import { DataStreamFromEs } from '../../../../common/types'; import { RouteDependencies } from '../../../types'; @@ -68,30 +68,23 @@ const enhanceDataStreams = ({ }); }; -const getDataStreams = (client: ElasticsearchClient, name = '*') => { - // TODO update when elasticsearch client has update requestParams for 'indices.getDataStream' - return client.transport.request({ - path: `/_data_stream/${encodeURIComponent(name)}`, - method: 'GET', - querystring: { - expand_wildcards: 'all', - }, +const getDataStreams = (client: IScopedClusterClient, name = '*') => { + return client.asCurrentUser.indices.getDataStream({ + name, + expand_wildcards: 'all', }); }; -const getDataStreamsStats = (client: ElasticsearchClient, name = '*') => { - return client.transport.request({ - path: `/_data_stream/${encodeURIComponent(name)}/_stats`, - method: 'GET', - querystring: { - human: true, - expand_wildcards: 'all', - }, +const getDataStreamsStats = (client: IScopedClusterClient, name = '*') => { + return client.asCurrentUser.indices.dataStreamsStats({ + name, + expand_wildcards: 'all', + human: true, }); }; -const getDataStreamsPrivileges = (client: ElasticsearchClient, names: string[]) => { - return client.security.hasPrivileges({ +const getDataStreamsPrivileges = (client: IScopedClusterClient, names: string[]) => { + return client.asCurrentUser.security.hasPrivileges({ body: { index: [ { @@ -109,15 +102,15 @@ export function registerGetAllRoute({ router, lib: { handleEsError }, config }: }); router.get( { path: addBasePath('/data_streams'), validate: { query: querySchema } }, - async (ctx, req, response) => { - const { asCurrentUser } = ctx.core.elasticsearch.client; + async (context, request, response) => { + const { client } = context.core.elasticsearch; - const includeStats = (req.query as TypeOf).includeStats === 'true'; + const includeStats = (request.query as TypeOf).includeStats === 'true'; try { - let { + const { body: { data_streams: dataStreams }, - } = await getDataStreams(asCurrentUser); + } = await getDataStreams(client); let dataStreamsStats; let dataStreamsPrivileges; @@ -125,24 +118,26 @@ export function registerGetAllRoute({ router, lib: { handleEsError }, config }: if (includeStats) { ({ body: { data_streams: dataStreamsStats }, - } = await getDataStreamsStats(asCurrentUser)); + } = await getDataStreamsStats(client)); } if (config.isSecurityEnabled() && dataStreams.length > 0) { ({ body: dataStreamsPrivileges } = await getDataStreamsPrivileges( - asCurrentUser, - dataStreams.map((dataStream: DataStreamFromEs) => dataStream.name) + client, + dataStreams.map((dataStream) => dataStream.name) )); } - dataStreams = enhanceDataStreams({ + const enhancedDataStreams = enhanceDataStreams({ + // @ts-expect-error @elastic/elasticsearch DataStreamFromEs incompatible with IndicesGetDataStreamIndicesGetDataStreamItem dataStreams, + // @ts-expect-error @elastic/elasticsearch StatsFromEs incompatible with IndicesDataStreamsStatsDataStreamsStatsItem dataStreamsStats, - // @ts-expect-error PrivilegesFromEs incompatible with ApplicationsPrivileges + // @ts-expect-error @elastic/elasticsearch PrivilegesFromEs incompatible with ApplicationsPrivileges dataStreamsPrivileges, }); - return response.ok({ body: deserializeDataStreamList(dataStreams) }); + return response.ok({ body: deserializeDataStreamList(enhancedDataStreams) }); } catch (error) { return handleEsError({ error, response }); } @@ -159,9 +154,9 @@ export function registerGetOneRoute({ router, lib: { handleEsError }, config }: path: addBasePath('/data_streams/{name}'), validate: { params: paramsSchema }, }, - async (ctx, req, response) => { - const { name } = req.params as TypeOf; - const { asCurrentUser } = ctx.core.elasticsearch.client; + async (context, request, response) => { + const { name } = request.params as TypeOf; + const { client } = context.core.elasticsearch; try { const [ { @@ -170,23 +165,22 @@ export function registerGetOneRoute({ router, lib: { handleEsError }, config }: { body: { data_streams: dataStreamsStats }, }, - ] = await Promise.all([ - getDataStreams(asCurrentUser, name), - getDataStreamsStats(asCurrentUser, name), - ]); + ] = await Promise.all([getDataStreams(client, name), getDataStreamsStats(client, name)]); if (dataStreams[0]) { let dataStreamsPrivileges; if (config.isSecurityEnabled()) { - ({ body: dataStreamsPrivileges } = await getDataStreamsPrivileges(asCurrentUser, [ + ({ body: dataStreamsPrivileges } = await getDataStreamsPrivileges(client, [ dataStreams[0].name, ])); } const enhancedDataStreams = enhanceDataStreams({ + // @ts-expect-error @elastic/elasticsearch DataStreamFromEs incompatible with IndicesGetDataStreamIndicesGetDataStreamItem dataStreams, + // @ts-expect-error @elastic/elasticsearch StatsFromEs incompatible with IndicesDataStreamsStatsDataStreamsStatsItem dataStreamsStats, - // @ts-expect-error PrivilegesFromEs incompatible with ApplicationsPrivileges + // @ts-expect-error @elastic/elasticsearch PrivilegesFromEs incompatible with ApplicationsPrivileges dataStreamsPrivileges, }); const body = deserializeDataStream(enhancedDataStreams[0]); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_clear_cache_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_clear_cache_route.ts index 593f0cda6886e..e9b34e9a72d9b 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_clear_cache_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_clear_cache_route.ts @@ -14,31 +14,24 @@ const bodySchema = schema.object({ indices: schema.arrayOf(schema.string()), }); -export function registerClearCacheRoute({ router, lib }: RouteDependencies) { +export function registerClearCacheRoute({ router, lib: { handleEsError } }: RouteDependencies) { router.post( { path: addBasePath('/indices/clear_cache'), validate: { body: bodySchema } }, - async (ctx, req, res) => { - const payload = req.body as typeof bodySchema.type; - const { indices = [] } = payload; + async (context, request, response) => { + const { client } = context.core.elasticsearch; + const { indices = [] } = request.body as typeof bodySchema.type; const params = { - expandWildcards: 'none', + expand_wildcards: 'none', format: 'json', index: indices, }; try { - await ctx.core.elasticsearch.legacy.client.callAsCurrentUser('indices.clearCache', params); - return res.ok(); - } catch (e) { - if (lib.isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + await client.asCurrentUser.indices.clearCache(params); + return response.ok(); + } catch (error) { + return handleEsError({ error, response }); } } ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_close_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_close_route.ts index 777adcd055709..9b9bb8238038a 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_close_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_close_route.ts @@ -14,31 +14,24 @@ const bodySchema = schema.object({ indices: schema.arrayOf(schema.string()), }); -export function registerCloseRoute({ router, lib }: RouteDependencies) { +export function registerCloseRoute({ router, lib: { handleEsError } }: RouteDependencies) { router.post( { path: addBasePath('/indices/close'), validate: { body: bodySchema } }, - async (ctx, req, res) => { - const payload = req.body as typeof bodySchema.type; - const { indices = [] } = payload; + async (context, request, response) => { + const { client } = context.core.elasticsearch; + const { indices = [] } = request.body as typeof bodySchema.type; const params = { - expandWildcards: 'none', + expand_wildcards: 'none', format: 'json', index: indices, }; try { - await ctx.core.elasticsearch.legacy.client.callAsCurrentUser('indices.close', params); - return res.ok(); - } catch (e) { - if (lib.isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + await client.asCurrentUser.indices.close(params); + return response.ok(); + } catch (error) { + return handleEsError({ error, response }); } } ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_delete_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_delete_route.ts index 914835089a438..2bd564e8a4c92 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_delete_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_delete_route.ts @@ -14,31 +14,24 @@ const bodySchema = schema.object({ indices: schema.arrayOf(schema.string()), }); -export function registerDeleteRoute({ router, lib }: RouteDependencies) { +export function registerDeleteRoute({ router, lib: { handleEsError } }: RouteDependencies) { router.post( { path: addBasePath('/indices/delete'), validate: { body: bodySchema } }, - async (ctx, req, res) => { - const body = req.body as typeof bodySchema.type; - const { indices = [] } = body; + async (context, request, response) => { + const { client } = context.core.elasticsearch; + const { indices = [] } = request.body as typeof bodySchema.type; const params = { - expandWildcards: 'none', + expand_wildcards: 'none', format: 'json', index: indices, }; try { - await ctx.core.elasticsearch.legacy.client.callAsCurrentUser('indices.delete', params); - return res.ok(); - } catch (e) { - if (lib.isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + await client.asCurrentUser.indices.delete(params); + return response.ok(); + } catch (error) { + return handleEsError({ error, response }); } } ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_flush_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_flush_route.ts index bb1759a034cc7..b008494ab8157 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_flush_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_flush_route.ts @@ -14,31 +14,24 @@ const bodySchema = schema.object({ indices: schema.arrayOf(schema.string()), }); -export function registerFlushRoute({ router, lib }: RouteDependencies) { +export function registerFlushRoute({ router, lib: { handleEsError } }: RouteDependencies) { router.post( { path: addBasePath('/indices/flush'), validate: { body: bodySchema } }, - async (ctx, req, res) => { - const body = req.body as typeof bodySchema.type; - const { indices = [] } = body; + async (context, request, response) => { + const { client } = context.core.elasticsearch; + const { indices = [] } = request.body as typeof bodySchema.type; const params = { - expandWildcards: 'none', + expand_wildcards: 'none', format: 'json', index: indices, }; try { - await ctx.core.elasticsearch.legacy.client.callAsCurrentUser('indices.flush', params); - return res.ok(); - } catch (e) { - if (lib.isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + await client.asCurrentUser.indices.flush(params); + return response.ok(); + } catch (error) { + return handleEsError({ error, response }); } } ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_forcemerge_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_forcemerge_route.ts index 6f0e8f0fec567..48d0e1bc974c6 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_forcemerge_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_forcemerge_route.ts @@ -15,7 +15,7 @@ const bodySchema = schema.object({ maxNumSegments: schema.maybe(schema.number()), }); -export function registerForcemergeRoute({ router, lib }: RouteDependencies) { +export function registerForcemergeRoute({ router, lib: { handleEsError } }: RouteDependencies) { router.post( { path: addBasePath('/indices/forcemerge'), @@ -23,10 +23,11 @@ export function registerForcemergeRoute({ router, lib }: RouteDependencies) { body: bodySchema, }, }, - async (ctx, req, res) => { - const { maxNumSegments, indices = [] } = req.body as typeof bodySchema.type; + async (context, request, response) => { + const { client } = context.core.elasticsearch; + const { maxNumSegments, indices = [] } = request.body as typeof bodySchema.type; const params = { - expandWildcards: 'none', + expand_wildcards: 'none', index: indices, }; @@ -35,17 +36,10 @@ export function registerForcemergeRoute({ router, lib }: RouteDependencies) { } try { - await ctx.core.elasticsearch.legacy.client.callAsCurrentUser('indices.forcemerge', params); - return res.ok(); - } catch (e) { - if (lib.isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + await client.asCurrentUser.indices.forcemerge(params); + return response.ok(); + } catch (error) { + return handleEsError({ error, response }); } } ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_freeze_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_freeze_route.ts index 4b1281e0f2121..fcab1d6338b6f 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_freeze_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_freeze_route.ts @@ -14,33 +14,20 @@ const bodySchema = schema.object({ indices: schema.arrayOf(schema.string()), }); -export function registerFreezeRoute({ router, lib }: RouteDependencies) { +export function registerFreezeRoute({ router, lib: { handleEsError } }: RouteDependencies) { router.post( { path: addBasePath('/indices/freeze'), validate: { body: bodySchema } }, - async (ctx, req, res) => { - const body = req.body as typeof bodySchema.type; - const { indices = [] } = body; - - const params = { - path: `/${encodeURIComponent(indices.join(','))}/_freeze`, - method: 'POST', - }; + async (context, request, response) => { + const { client } = context.core.elasticsearch; + const { indices = [] } = request.body as typeof bodySchema.type; try { - await await ctx.core.elasticsearch.legacy.client.callAsCurrentUser( - 'transport.request', - params - ); - return res.ok(); - } catch (e) { - if (lib.isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + await client.asCurrentUser.indices.freeze({ + index: indices.join(','), + }); + return response.ok(); + } catch (error) { + return handleEsError({ error, response }); } } ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_list_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_list_route.ts index 47c454e96c8e2..d8a7fcb29bb35 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_list_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_list_route.ts @@ -9,23 +9,21 @@ import { fetchIndices } from '../../../lib/fetch_indices'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; -export function registerListRoute({ router, indexDataEnricher, lib }: RouteDependencies) { - router.get({ path: addBasePath('/indices'), validate: false }, async (ctx, req, res) => { - try { - const indices = await fetchIndices( - ctx.core.elasticsearch.legacy.client.callAsCurrentUser, - indexDataEnricher - ); - return res.ok({ body: indices }); - } catch (e) { - if (lib.isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); +export function registerListRoute({ + router, + indexDataEnricher, + lib: { handleEsError }, +}: RouteDependencies) { + router.get( + { path: addBasePath('/indices'), validate: false }, + async (context, request, response) => { + const { client } = context.core.elasticsearch; + try { + const indices = await fetchIndices(client, indexDataEnricher); + return response.ok({ body: indices }); + } catch (error) { + return handleEsError({ error, response }); } - // Case: default - throw e; } - }); + ); } diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_open_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_open_route.ts index cad57ce60de65..be4b84fdcda82 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_open_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_open_route.ts @@ -14,31 +14,24 @@ const bodySchema = schema.object({ indices: schema.arrayOf(schema.string()), }); -export function registerOpenRoute({ router, lib }: RouteDependencies) { +export function registerOpenRoute({ router, lib: { handleEsError } }: RouteDependencies) { router.post( { path: addBasePath('/indices/open'), validate: { body: bodySchema } }, - async (ctx, req, res) => { - const body = req.body as typeof bodySchema.type; - const { indices = [] } = body; + async (context, request, response) => { + const { client } = context.core.elasticsearch; + const { indices = [] } = request.body as typeof bodySchema.type; const params = { - expandWildcards: 'none', + expand_wildcards: 'none', format: 'json', index: indices, }; try { - await await ctx.core.elasticsearch.legacy.client.callAsCurrentUser('indices.open', params); - return res.ok(); - } catch (e) { - if (lib.isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + await client.asCurrentUser.indices.open(params); + return response.ok(); + } catch (error) { + return handleEsError({ error, response }); } } ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_refresh_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_refresh_route.ts index e2c0155e28086..c747653f0bb80 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_refresh_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_refresh_route.ts @@ -14,31 +14,24 @@ const bodySchema = schema.object({ indices: schema.arrayOf(schema.string()), }); -export function registerRefreshRoute({ router, lib }: RouteDependencies) { +export function registerRefreshRoute({ router, lib: { handleEsError } }: RouteDependencies) { router.post( { path: addBasePath('/indices/refresh'), validate: { body: bodySchema } }, - async (ctx, req, res) => { - const body = req.body as typeof bodySchema.type; - const { indices = [] } = body; + async (context, request, response) => { + const { client } = context.core.elasticsearch; + const { indices = [] } = request.body as typeof bodySchema.type; const params = { - expandWildcards: 'none', + expand_wildcards: 'none', format: 'json', index: indices, }; try { - await ctx.core.elasticsearch.legacy.client.callAsCurrentUser('indices.refresh', params); - return res.ok(); - } catch (e) { - if (lib.isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + await client.asCurrentUser.indices.refresh(params); + return response.ok(); + } catch (error) { + return handleEsError({ error, response }); } } ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_reload_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_reload_route.ts index 8d83cd21f427d..4c5bd1fb030e8 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_reload_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_reload_route.ts @@ -17,28 +17,22 @@ const bodySchema = schema.maybe( }) ); -export function registerReloadRoute({ router, indexDataEnricher, lib }: RouteDependencies) { +export function registerReloadRoute({ + router, + indexDataEnricher, + lib: { handleEsError }, +}: RouteDependencies) { router.post( { path: addBasePath('/indices/reload'), validate: { body: bodySchema } }, - async (ctx, req, res) => { - const { indexNames = [] } = (req.body as typeof bodySchema.type) ?? {}; + async (context, request, response) => { + const { client } = context.core.elasticsearch; + const { indexNames = [] } = (request.body as typeof bodySchema.type) ?? {}; try { - const indices = await fetchIndices( - ctx.core.elasticsearch.legacy.client.callAsCurrentUser, - indexDataEnricher, - indexNames - ); - return res.ok({ body: indices }); - } catch (e) { - if (lib.isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + const indices = await fetchIndices(client, indexDataEnricher, indexNames); + return response.ok({ body: indices }); + } catch (error) { + return handleEsError({ error, response }); } } ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_unfreeze_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_unfreeze_route.ts index 45102f4874129..71a7c27ad2cba 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_unfreeze_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_unfreeze_route.ts @@ -14,28 +14,20 @@ const bodySchema = schema.object({ indices: schema.arrayOf(schema.string()), }); -export function registerUnfreezeRoute({ router, lib }: RouteDependencies) { +export function registerUnfreezeRoute({ router, lib: { handleEsError } }: RouteDependencies) { router.post( { path: addBasePath('/indices/unfreeze'), validate: { body: bodySchema } }, - async (ctx, req, res) => { - const { indices = [] } = req.body as typeof bodySchema.type; - const params = { - path: `/${encodeURIComponent(indices.join(','))}/_unfreeze`, - method: 'POST', - }; + async (context, request, response) => { + const { client } = context.core.elasticsearch; + const { indices = [] } = request.body as typeof bodySchema.type; try { - await ctx.core.elasticsearch.legacy.client.callAsCurrentUser('transport.request', params); - return res.ok(); - } catch (e) { - if (lib.isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + await client.asCurrentUser.indices.unfreeze({ + index: indices.join(','), + }); + return response.ok(); + } catch (error) { + return handleEsError({ error, response }); } } ); diff --git a/x-pack/plugins/index_management/server/routes/api/mapping/register_mapping_route.ts b/x-pack/plugins/index_management/server/routes/api/mapping/register_mapping_route.ts index 406ceba16c8bd..b5891e579a1f6 100644 --- a/x-pack/plugins/index_management/server/routes/api/mapping/register_mapping_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/mapping/register_mapping_route.ts @@ -21,32 +21,23 @@ function formatHit(hit: { [key: string]: { mappings: any } }, indexName: string) }; } -export function registerMappingRoute({ router, lib }: RouteDependencies) { +export function registerMappingRoute({ router, lib: { handleEsError } }: RouteDependencies) { router.get( { path: addBasePath('/mapping/{indexName}'), validate: { params: paramsSchema } }, - async (ctx, req, res) => { - const { indexName } = req.params as typeof paramsSchema.type; + async (context, request, response) => { + const { client } = context.core.elasticsearch; + const { indexName } = request.params as typeof paramsSchema.type; const params = { expand_wildcards: 'none', index: indexName, }; try { - const hit = await ctx.core.elasticsearch.legacy.client.callAsCurrentUser( - 'indices.getMapping', - params - ); - const response = formatHit(hit, indexName); - return res.ok({ body: response }); - } catch (e) { - if (lib.isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + const { body: hit } = await client.asCurrentUser.indices.getMapping(params); + const responseBody = formatHit(hit, indexName); + return response.ok({ body: responseBody }); + } catch (error) { + return handleEsError({ error, response }); } } ); diff --git a/x-pack/plugins/index_management/server/routes/api/settings/register_load_route.ts b/x-pack/plugins/index_management/server/routes/api/settings/register_load_route.ts index 276b326929e8f..a819315f5231f 100644 --- a/x-pack/plugins/index_management/server/routes/api/settings/register_load_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/settings/register_load_route.ts @@ -21,34 +21,25 @@ function formatHit(hit: { [key: string]: {} }) { return hit[key]; } -export function registerLoadRoute({ router, lib }: RouteDependencies) { +export function registerLoadRoute({ router, lib: { handleEsError } }: RouteDependencies) { router.get( { path: addBasePath('/settings/{indexName}'), validate: { params: paramsSchema } }, - async (ctx, req, res) => { - const { indexName } = req.params as typeof paramsSchema.type; + async (context, request, response) => { + const { client } = context.core.elasticsearch; + const { indexName } = request.params as typeof paramsSchema.type; const params = { - expandWildcards: 'none', - flatSettings: false, + expand_wildcards: 'none', + flat_settings: false, local: false, - includeDefaults: true, + include_defaults: true, index: indexName, }; try { - const hit = await ctx.core.elasticsearch.legacy.client.callAsCurrentUser( - 'indices.getSettings', - params - ); - return res.ok({ body: formatHit(hit) }); - } catch (e) { - if (lib.isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + const { body: hit } = await client.asCurrentUser.indices.getSettings(params); + return response.ok({ body: formatHit(hit) }); + } catch (error) { + return handleEsError({ error, response }); } } ); diff --git a/x-pack/plugins/index_management/server/routes/api/settings/register_update_route.ts b/x-pack/plugins/index_management/server/routes/api/settings/register_update_route.ts index b4f12b91083df..5dc825738cfaa 100644 --- a/x-pack/plugins/index_management/server/routes/api/settings/register_update_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/settings/register_update_route.ts @@ -16,37 +16,28 @@ const paramsSchema = schema.object({ indexName: schema.string(), }); -export function registerUpdateRoute({ router, lib }: RouteDependencies) { +export function registerUpdateRoute({ router, lib: { handleEsError } }: RouteDependencies) { router.put( { path: addBasePath('/settings/{indexName}'), validate: { body: bodySchema, params: paramsSchema }, }, - async (ctx, req, res) => { - const { indexName } = req.params as typeof paramsSchema.type; + async (context, request, response) => { + const { client } = context.core.elasticsearch; + const { indexName } = request.params as typeof paramsSchema.type; const params = { - ignoreUnavailable: true, - allowNoIndices: false, - expandWildcards: 'none', + ignore_unavailable: true, + allow_no_indices: false, + expand_wildcards: 'none', index: indexName, - body: req.body, + body: request.body, }; try { - const response = await ctx.core.elasticsearch.legacy.client.callAsCurrentUser( - 'indices.putSettings', - params - ); - return res.ok({ body: response }); - } catch (e) { - if (lib.isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + const { body: responseBody } = await client.asCurrentUser.indices.putSettings(params); + return response.ok({ body: responseBody }); + } catch (error) { + return handleEsError({ error, response }); } } ); diff --git a/x-pack/plugins/index_management/server/routes/api/stats/register_stats_route.ts b/x-pack/plugins/index_management/server/routes/api/stats/register_stats_route.ts index 42a3012ea8e17..7458b98f5092f 100644 --- a/x-pack/plugins/index_management/server/routes/api/stats/register_stats_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/stats/register_stats_route.ts @@ -6,6 +6,7 @@ */ import { schema } from '@kbn/config-schema'; +import type { estypes } from '@elastic/elasticsearch'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; @@ -14,40 +15,37 @@ const paramsSchema = schema.object({ indexName: schema.string(), }); -function formatHit(hit: { _shards: any; indices: { [key: string]: any } }, indexName: string) { +interface Hit { + _shards: unknown; + indices?: Record; +} + +function formatHit(hit: Hit, indexName: string) { const { _shards, indices } = hit; - const stats = indices[indexName]; + const stats = indices![indexName]; return { _shards, stats, }; } -export function registerStatsRoute({ router, lib }: RouteDependencies) { +export function registerStatsRoute({ router, lib: { handleEsError } }: RouteDependencies) { router.get( { path: addBasePath('/stats/{indexName}'), validate: { params: paramsSchema } }, - async (ctx, req, res) => { - const { indexName } = req.params as typeof paramsSchema.type; + async (context, request, response) => { + const { client } = context.core.elasticsearch; + const { indexName } = request.params as typeof paramsSchema.type; const params = { expand_wildcards: 'none', index: indexName, }; try { - const hit = await ctx.core.elasticsearch.legacy.client.callAsCurrentUser( - 'indices.stats', - params - ); - return res.ok({ body: formatHit(hit, indexName) }); - } catch (e) { - if (lib.isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + const { body: hit } = await client.asCurrentUser.indices.stats(params); + + return response.ok({ body: formatHit(hit, indexName) }); + } catch (error) { + return handleEsError({ error, response }); } } ); diff --git a/x-pack/plugins/index_management/server/routes/api/templates/lib.ts b/x-pack/plugins/index_management/server/routes/api/templates/lib.ts index d64bb719d23eb..ef2642399d76d 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/lib.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/lib.ts @@ -5,32 +5,33 @@ * 2.0. */ +import { IScopedClusterClient } from 'kibana/server'; import { serializeTemplate, serializeLegacyTemplate } from '../../../../common/lib'; import { TemplateDeserialized, LegacyTemplateSerialized } from '../../../../common'; -import { CallAsCurrentUser } from '../../../types'; export const doesTemplateExist = async ({ name, - callAsCurrentUser, + client, isLegacy, }: { name: string; - callAsCurrentUser: CallAsCurrentUser; + client: IScopedClusterClient; isLegacy?: boolean; }) => { if (isLegacy) { - return await callAsCurrentUser('indices.existsTemplate', { name }); + return await client.asCurrentUser.indices.existsTemplate({ name }); } - return await callAsCurrentUser('dataManagement.existsTemplate', { name }); + + return await client.asCurrentUser.indices.existsIndexTemplate({ name }); }; export const saveTemplate = async ({ template, - callAsCurrentUser, + client, isLegacy, }: { template: TemplateDeserialized; - callAsCurrentUser: CallAsCurrentUser; + client: IScopedClusterClient; isLegacy?: boolean; }) => { const serializedTemplate = isLegacy @@ -48,8 +49,9 @@ export const saveTemplate = async ({ aliases, } = serializedTemplate as LegacyTemplateSerialized; - return await callAsCurrentUser('indices.putTemplate', { + return await client.asCurrentUser.indices.putTemplate({ name: template.name, + // @ts-expect-error @elastic/elasticsearch not assignable to parameter of type 'IndicesPutTemplateRequest' order, body: { index_patterns, @@ -61,8 +63,9 @@ export const saveTemplate = async ({ }); } - return await callAsCurrentUser('dataManagement.saveComposableIndexTemplate', { + return await client.asCurrentUser.indices.putIndexTemplate({ name: template.name, + // @ts-expect-error @elastic/elasticsearch Type 'LegacyTemplateSerialized | TemplateSerialized' is not assignable body: serializedTemplate, }); }; diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts index d8a236bdebd15..21be254eb9d73 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts @@ -15,54 +15,44 @@ import { saveTemplate, doesTemplateExist } from './lib'; const bodySchema = templateSchema; -export function registerCreateRoute({ router, lib }: RouteDependencies) { +export function registerCreateRoute({ router, lib: { handleEsError } }: RouteDependencies) { router.post( { path: addBasePath('/index_templates'), validate: { body: bodySchema } }, - async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.dataManagement!.client; - const template = req.body as TemplateDeserialized; - const { - _kbnMeta: { isLegacy }, - } = template; - - // Check that template with the same name doesn't already exist - const templateExists = await doesTemplateExist({ - name: template.name, - callAsCurrentUser, - isLegacy, - }); - - if (templateExists) { - return res.conflict({ - body: new Error( - i18n.translate('xpack.idxMgmt.createRoute.duplicateTemplateIdErrorMessage', { - defaultMessage: "There is already a template with name '{name}'.", - values: { - name: template.name, - }, - }) - ), - }); - } + async (context, request, response) => { + const { client } = context.core.elasticsearch; + const template = request.body as TemplateDeserialized; try { - // Otherwise create new index template - const response = await saveTemplate({ template, callAsCurrentUser, isLegacy }); + const { + _kbnMeta: { isLegacy }, + } = template; + + // Check that template with the same name doesn't already exist + const { body: templateExists } = await doesTemplateExist({ + name: template.name, + client, + isLegacy, + }); - return res.ok({ body: response }); - } catch (e) { - if (lib.isEsError(e)) { - const error = lib.parseEsError(e.response); - return res.customError({ - statusCode: e.statusCode, - body: { - message: error.message, - attributes: error, - }, + if (templateExists) { + return response.conflict({ + body: new Error( + i18n.translate('xpack.idxMgmt.createRoute.duplicateTemplateIdErrorMessage', { + defaultMessage: "There is already a template with name '{name}'.", + values: { + name: template.name, + }, + }) + ), }); } - // Case: default - throw e; + + // Otherwise create new index template + const { body: responseBody } = await saveTemplate({ template, client, isLegacy }); + + return response.ok({ body: responseBody }); + } catch (error) { + return handleEsError({ error, response }); } } ); diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts index 083964dec9edc..fbfcab3a8f5ed 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts @@ -9,7 +9,6 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; -import { wrapEsError } from '../../helpers'; import { TemplateDeserialized } from '../../../../common'; @@ -22,16 +21,19 @@ const bodySchema = schema.object({ ), }); -export function registerDeleteRoute({ router }: RouteDependencies) { +export function registerDeleteRoute({ router, lib: { handleEsError } }: RouteDependencies) { router.post( { path: addBasePath('/delete_index_templates'), validate: { body: bodySchema }, }, - async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.dataManagement!.client; - const { templates } = req.body as TypeOf; - const response: { templatesDeleted: Array; errors: any[] } = { + async (context, request, response) => { + const { client } = context.core.elasticsearch; + const { templates } = request.body as TypeOf; + const responseBody: { + templatesDeleted: Array; + errors: any[]; + } = { templatesDeleted: [], errors: [], }; @@ -40,26 +42,26 @@ export function registerDeleteRoute({ router }: RouteDependencies) { templates.map(async ({ name, isLegacy }) => { try { if (isLegacy) { - await callAsCurrentUser('indices.deleteTemplate', { + await client.asCurrentUser.indices.deleteTemplate({ name, }); } else { - await callAsCurrentUser('dataManagement.deleteComposableIndexTemplate', { + await client.asCurrentUser.indices.deleteIndexTemplate({ name, }); } - return response.templatesDeleted.push(name); - } catch (e) { - return response.errors.push({ + return responseBody.templatesDeleted.push(name); + } catch (error) { + return responseBody.errors.push({ name, - error: wrapEsError(e), + error: handleEsError({ error, response }), }); } }) ); - return res.ok({ body: response }); + return response.ok({ body: responseBody }); } ); } diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts index 231a2764d2710..9d0b7302b5587 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts @@ -17,41 +17,37 @@ import { getCloudManagedTemplatePrefix } from '../../../lib/get_managed_template import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; -export function registerGetAllRoute({ router, lib: { isEsError } }: RouteDependencies) { - router.get({ path: addBasePath('/index_templates'), validate: false }, async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.dataManagement!.client; - - try { - const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(callAsCurrentUser); - - const legacyTemplatesEs = await callAsCurrentUser('indices.getTemplate'); - const { index_templates: templatesEs } = await callAsCurrentUser( - 'dataManagement.getComposableIndexTemplates' - ); - - const legacyTemplates = deserializeLegacyTemplateList( - legacyTemplatesEs, - cloudManagedTemplatePrefix - ); - const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix); - - const body = { - templates, - legacyTemplates, - }; - - return res.ok({ body }); - } catch (error) { - if (isEsError(error)) { - return res.customError({ - statusCode: error.statusCode, - body: error, - }); +export function registerGetAllRoute({ router, lib: { handleEsError } }: RouteDependencies) { + router.get( + { path: addBasePath('/index_templates'), validate: false }, + async (context, request, response) => { + const { client } = context.core.elasticsearch; + + try { + const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(client); + + const { body: legacyTemplatesEs } = await client.asCurrentUser.indices.getTemplate(); + const { + body: { index_templates: templatesEs }, + } = await client.asCurrentUser.indices.getIndexTemplate(); + + const legacyTemplates = deserializeLegacyTemplateList( + legacyTemplatesEs, + cloudManagedTemplatePrefix + ); + const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix); + + const body = { + templates, + legacyTemplates, + }; + + return response.ok({ body }); + } catch (error) { + return handleEsError({ error, response }); } - // Case: default - throw error; } - }); + ); } const paramsSchema = schema.object({ @@ -63,26 +59,27 @@ const querySchema = schema.object({ legacy: schema.maybe(schema.oneOf([schema.literal('true'), schema.literal('false')])), }); -export function registerGetOneRoute({ router, lib }: RouteDependencies) { +export function registerGetOneRoute({ router, lib: { handleEsError } }: RouteDependencies) { router.get( { path: addBasePath('/index_templates/{name}'), validate: { params: paramsSchema, query: querySchema }, }, - async (ctx, req, res) => { - const { name } = req.params as TypeOf; - const { callAsCurrentUser } = ctx.dataManagement!.client; - - const isLegacy = (req.query as TypeOf).legacy === 'true'; + async (context, request, response) => { + const { client } = context.core.elasticsearch; + const { name } = request.params as TypeOf; + const isLegacy = (request.query as TypeOf).legacy === 'true'; try { - const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(callAsCurrentUser); + const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(client); if (isLegacy) { - const indexTemplateByName = await callAsCurrentUser('indices.getTemplate', { name }); + const { body: indexTemplateByName } = await client.asCurrentUser.indices.getTemplate({ + name, + }); if (indexTemplateByName[name]) { - return res.ok({ + return response.ok({ body: deserializeLegacyTemplate( { ...indexTemplateByName[name], name }, cloudManagedTemplatePrefix @@ -91,11 +88,11 @@ export function registerGetOneRoute({ router, lib }: RouteDependencies) { } } else { const { - index_templates: indexTemplates, - } = await callAsCurrentUser('dataManagement.getComposableIndexTemplate', { name }); + body: { index_templates: indexTemplates }, + } = await client.asCurrentUser.indices.getIndexTemplate({ name }); if (indexTemplates.length > 0) { - return res.ok({ + return response.ok({ body: deserializeTemplate( { ...indexTemplates[0].index_template, name }, cloudManagedTemplatePrefix @@ -104,16 +101,9 @@ export function registerGetOneRoute({ router, lib }: RouteDependencies) { } } - return res.notFound(); - } catch (e) { - if (lib.isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return response.notFound(); + } catch (error) { + return handleEsError({ error, response }); } } ); diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts index 0c3d8faea628c..cd363cbd7d003 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts @@ -12,35 +12,30 @@ import { addBasePath } from '../index'; const bodySchema = schema.object({}, { unknowns: 'allow' }); -export function registerSimulateRoute({ router, lib }: RouteDependencies) { +export function registerSimulateRoute({ router, lib: { handleEsError } }: RouteDependencies) { router.post( { path: addBasePath('/index_templates/simulate'), validate: { body: bodySchema }, }, - async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.dataManagement!.client; - const template = req.body as TypeOf; + async (context, request, response) => { + const { client } = context.core.elasticsearch; + const template = request.body as TypeOf; try { - const templatePreview = await callAsCurrentUser('dataManagement.simulateTemplate', { - body: template, + const { body: templatePreview } = await client.asCurrentUser.indices.simulateTemplate({ + body: { + ...template, + // Until ES fixes a bug on their side we need to send a fake index pattern + // that won't match any indices. + // Issue: https://github.com/elastic/elasticsearch/issues/59152 + index_patterns: ['a_fake_index_pattern_that_wont_match_any_indices'], + }, }); - return res.ok({ body: templatePreview }); - } catch (e) { - if (lib.isEsError(e)) { - const error = lib.parseEsError(e.response); - return res.customError({ - statusCode: e.statusCode, - body: { - message: error.message, - attributes: error, - }, - }); - } - // Case: default - throw e; + return response.ok({ body: templatePreview }); + } catch (error) { + return handleEsError({ error, response }); } } ); diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts index 07a7d457f0473..669a1fff66317 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts @@ -18,45 +18,35 @@ const paramsSchema = schema.object({ name: schema.string(), }); -export function registerUpdateRoute({ router, lib }: RouteDependencies) { +export function registerUpdateRoute({ router, lib: { handleEsError } }: RouteDependencies) { router.put( { path: addBasePath('/index_templates/{name}'), validate: { body: bodySchema, params: paramsSchema }, }, - async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.dataManagement!.client; - const { name } = req.params as typeof paramsSchema.type; - const template = req.body as TemplateDeserialized; - const { - _kbnMeta: { isLegacy }, - } = template; - - // Verify the template exists (ES will throw 404 if not) - const doesExist = await doesTemplateExist({ name, callAsCurrentUser, isLegacy }); - - if (!doesExist) { - return res.notFound(); - } + async (context, request, response) => { + const { client } = context.core.elasticsearch; + const { name } = request.params as typeof paramsSchema.type; + const template = request.body as TemplateDeserialized; try { - // Next, update index template - const response = await saveTemplate({ template, callAsCurrentUser, isLegacy }); - - return res.ok({ body: response }); - } catch (e) { - if (lib.isEsError(e)) { - const error = lib.parseEsError(e.response); - return res.customError({ - statusCode: e.statusCode, - body: { - message: error.message, - attributes: error, - }, - }); + const { + _kbnMeta: { isLegacy }, + } = template; + + // Verify the template exists (ES will throw 404 if not) + const { body: templateExists } = await doesTemplateExist({ name, client, isLegacy }); + + if (!templateExists) { + return response.notFound(); } - // Case: default - throw e; + + // Next, update index template + const { body: responseBody } = await saveTemplate({ template, client, isLegacy }); + + return response.ok({ body: responseBody }); + } catch (error) { + return handleEsError({ error, response }); } } ); diff --git a/x-pack/plugins/index_management/server/routes/helpers.ts b/x-pack/plugins/index_management/server/routes/helpers.ts deleted file mode 100644 index 1f5d0af18279d..0000000000000 --- a/x-pack/plugins/index_management/server/routes/helpers.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -const extractCausedByChain = (causedBy: any = {}, accumulator: any[] = []): any => { - const { reason, caused_by } = causedBy; // eslint-disable-line @typescript-eslint/naming-convention - - if (reason) { - accumulator.push(reason); - } - - if (caused_by) { - return extractCausedByChain(caused_by, accumulator); - } - - return accumulator; -}; - -/** - * Wraps an error thrown by the ES JS client into a Boom error response and returns it - * - * @param err Object Error thrown by ES JS client - * @param statusCodeToMessageMap Object Optional map of HTTP status codes => error messages - * @return Object Boom error response - */ -export const wrapEsError = (err: any, statusCodeToMessageMap: any = {}) => { - const { statusCode, response } = err; - - const { - error: { - root_cause = [], // eslint-disable-line @typescript-eslint/naming-convention - caused_by = {}, // eslint-disable-line @typescript-eslint/naming-convention - } = {}, - } = JSON.parse(response); - - // If no custom message if specified for the error's status code, just - // wrap the error as a Boom error response, include the additional information from ES, and return it - if (!statusCodeToMessageMap[statusCode]) { - // const boomError = Boom.boomify(err, { statusCode }); - const error: any = { statusCode }; - - // The caused_by chain has the most information so use that if it's available. If not then - // settle for the root_cause. - const causedByChain = extractCausedByChain(caused_by); - const defaultCause = root_cause.length ? extractCausedByChain(root_cause[0]) : undefined; - - error.cause = causedByChain.length ? causedByChain : defaultCause; - return error; - } - - // Otherwise, use the custom message to create a Boom error response and - // return it - const message = statusCodeToMessageMap[statusCode]; - return { message, statusCode }; -}; diff --git a/x-pack/plugins/index_management/server/services/index_data_enricher.ts b/x-pack/plugins/index_management/server/services/index_data_enricher.ts index eb2169aebf2c5..532485201b99e 100644 --- a/x-pack/plugins/index_management/server/services/index_data_enricher.ts +++ b/x-pack/plugins/index_management/server/services/index_data_enricher.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { CallAsCurrentUser } from '../types'; +import { IScopedClusterClient } from 'kibana/server'; import { Index } from '../index'; -export type Enricher = (indices: Index[], callAsCurrentUser: CallAsCurrentUser) => Promise; +export type Enricher = (indices: Index[], client: IScopedClusterClient) => Promise; export class IndexDataEnricher { private readonly _enrichers: Enricher[] = []; @@ -19,14 +19,14 @@ export class IndexDataEnricher { public enrichIndices = async ( indices: Index[], - callAsCurrentUser: CallAsCurrentUser + client: IScopedClusterClient ): Promise => { let enrichedIndices = indices; for (let i = 0; i < this.enrichers.length; i++) { const dataEnricher = this.enrichers[i]; try { - const dataEnricherResponse = await dataEnricher(enrichedIndices, callAsCurrentUser); + const dataEnricherResponse = await dataEnricher(enrichedIndices, client); enrichedIndices = dataEnricherResponse; } catch (e) { // silently swallow enricher response errors diff --git a/x-pack/plugins/index_management/server/shared_imports.ts b/x-pack/plugins/index_management/server/shared_imports.ts index 6daa94900e559..7f55d189457c7 100644 --- a/x-pack/plugins/index_management/server/shared_imports.ts +++ b/x-pack/plugins/index_management/server/shared_imports.ts @@ -5,8 +5,4 @@ * 2.0. */ -export { - isEsError, - parseEsError, - handleEsError, -} from '../../../../src/plugins/es_ui_shared/server'; +export { handleEsError } from '../../../../src/plugins/es_ui_shared/server'; diff --git a/x-pack/plugins/index_management/server/types.ts b/x-pack/plugins/index_management/server/types.ts index c980279d5bf30..ba40d22f2eafc 100644 --- a/x-pack/plugins/index_management/server/types.ts +++ b/x-pack/plugins/index_management/server/types.ts @@ -5,17 +5,13 @@ * 2.0. */ -import type { - LegacyScopedClusterClient, - ILegacyScopedClusterClient, - IRouter, - RequestHandlerContext, -} from 'src/core/server'; +import { IRouter } from 'src/core/server'; + import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { SecurityPluginSetup } from '../../security/server'; import { IndexDataEnricher } from './services'; -import { isEsError, parseEsError, handleEsError } from './shared_imports'; +import { handleEsError } from './shared_imports'; export interface Dependencies { security: SecurityPluginSetup; @@ -24,39 +20,12 @@ export interface Dependencies { } export interface RouteDependencies { - router: IndexManagementRouter; + router: IRouter; config: { isSecurityEnabled: () => boolean; }; indexDataEnricher: IndexDataEnricher; lib: { - isEsError: typeof isEsError; - parseEsError: typeof parseEsError; handleEsError: typeof handleEsError; }; } - -export type CallAsCurrentUser = LegacyScopedClusterClient['callAsCurrentUser']; - -export interface DataManagementContext { - client: ILegacyScopedClusterClient; -} - -/** - * @internal - */ -export interface IndexManagementApiRequestHandlerContext { - client: ILegacyScopedClusterClient; -} - -/** - * @internal - */ -export interface IndexManagementRequestHandlerContext extends RequestHandlerContext { - dataManagement: IndexManagementApiRequestHandlerContext; -} - -/** - * @internal - */ -export type IndexManagementRouter = IRouter; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts index efd38ef0bb13b..2dd35c20a5632 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts @@ -7,9 +7,9 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; +import { ObservabilityRuleTypeModel } from '../../../../observability/public'; import { validateMetricThreshold } from './components/validation'; +import { formatReason } from './rule_data_formatters'; import { AlertTypeParams } from '../../../../alerting/common'; import { MetricExpressionParams, @@ -21,7 +21,7 @@ interface MetricThresholdAlertTypeParams extends AlertTypeParams { criteria: MetricExpressionParams[]; } -export function createMetricThresholdAlertType(): AlertTypeModel { +export function createMetricThresholdAlertType(): ObservabilityRuleTypeModel { return { id: METRIC_THRESHOLD_ALERT_TYPE_ID, description: i18n.translate('xpack.infra.metrics.alertFlyout.alertDescription', { @@ -44,5 +44,6 @@ Reason: } ), requiresAppContext: false, + format: formatReason, }; } diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/rule_data_formatters.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/rule_data_formatters.ts new file mode 100644 index 0000000000000..7a0140ab05652 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/rule_data_formatters.ts @@ -0,0 +1,19 @@ +/* + * 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 { ALERT_REASON } from '@kbn/rule-data-utils'; +import { ObservabilityRuleTypeFormatter } from '../../../../observability/public'; + +export const formatReason: ObservabilityRuleTypeFormatter = ({ fields }) => { + const reason = fields[ALERT_REASON] ?? '-'; + const link = '/app/metrics/explorer'; // TODO https://github.com/elastic/kibana/issues/106497 & https://github.com/elastic/kibana/issues/106958 + + return { + reason, + link, + }; +}; diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 3c22c1ad7a76d..76e3e777e6378 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -41,8 +41,9 @@ export class Plugin implements InfraClientPluginClass { pluginsSetup.observability.observabilityRuleTypeRegistry.register( createLogThresholdAlertType() ); - pluginsSetup.triggersActionsUi.ruleTypeRegistry.register(createMetricThresholdAlertType()); - + pluginsSetup.observability.observabilityRuleTypeRegistry.register( + createMetricThresholdAlertType() + ); pluginsSetup.observability.dashboard.register({ appName: 'infra_logs', hasData: getLogsHasDataFetcher(core.getStartServices), diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 0ec071b97d7cf..f33bcd2fcab0c 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericParams, SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { Lifecycle } from '@hapi/hapi'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { JsonArray, JsonValue } from '@kbn/common-utils'; @@ -38,7 +38,7 @@ export interface InfraServerPluginStartDeps { data: DataPluginStart; } -export interface CallWithRequestParams extends GenericParams { +export interface CallWithRequestParams extends estypes.RequestBase { max_concurrent_shard_requests?: number; name?: string; index?: string | string[]; @@ -50,6 +50,7 @@ export interface CallWithRequestParams extends GenericParams { path?: string; query?: string | object; track_total_hits?: boolean | number; + body?: any; } export type InfraResponse = Lifecycle.ReturnValue; @@ -117,7 +118,7 @@ export interface InfraDatabaseGetIndicesResponse { }; } -export type SearchHit = SearchResponse['hits']['hits'][0]; +export type SearchHit = estypes.SearchHit; export interface SortedSearchHit extends SearchHit { sort: any[]; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index fd1dc43c191fb..18de1a2ad5c00 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -6,7 +6,6 @@ */ import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; -import { Comparator } from './types'; import * as mocks from './test_mocks'; // import { RecoveredActionGroup } from '../../../../../alerting/common'; import { @@ -14,10 +13,19 @@ import { AlertServicesMock, AlertInstanceMock, } from '../../../../../alerting/server/mocks'; +import { LifecycleAlertServices } from '../../../../../rule_registry/server'; +import { ruleRegistryMocks } from '../../../../../rule_registry/server/mocks'; +import { createLifecycleRuleExecutorMock } from '../../../../../rule_registry/server/utils/create_lifecycle_rule_executor_mock'; import { InfraSources } from '../../sources'; -import { MetricThresholdAlertExecutorOptions } from './register_metric_threshold_alert_type'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; +import { AlertInstanceContext, AlertInstanceState } from '../../../../../alerting/server'; +import { + Aggregators, + Comparator, + CountMetricExpressionParams, + NonCountMetricExpressionParams, +} from './types'; interface AlertTestInstance { instance: AlertInstanceMock; @@ -27,11 +35,33 @@ interface AlertTestInstance { let persistAlertInstances = false; // eslint-disable-line prefer-const +type TestRuleState = Record & { + aRuleStateKey: string; +}; + +const initialRuleState: TestRuleState = { + aRuleStateKey: 'INITIAL_RULE_STATE_VALUE', +}; + const mockOptions = { alertId: '', startedAt: new Date(), previousStartedAt: null, - state: {}, + state: { + wrapped: initialRuleState, + trackedAlerts: { + TEST_ALERT_0: { + alertId: 'TEST_ALERT_0', + alertUuid: 'TEST_ALERT_0_UUID', + started: '2020-01-01T12:00:00.000Z', + }, + TEST_ALERT_1: { + alertId: 'TEST_ALERT_1', + alertUuid: 'TEST_ALERT_1_UUID', + started: '2020-01-02T12:00:00.000Z', + }, + }, + }, spaceId: '', name: '', tags: [], @@ -62,22 +92,20 @@ describe('The metric threshold alert type', () => { describe('querying the entire infrastructure', () => { const instanceID = '*'; const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => - executor(({ + executor({ + ...mockOptions, services, params: { sourceId, criteria: [ { - ...baseCriterion, + ...baseNonCountCriterion, comparator, threshold, }, ], }, - /** - * TODO: Remove this use of `as` by utilizing a proper type - */ - } as unknown) as MetricThresholdAlertExecutorOptions); + }); test('alerts as expected with the > comparator', async () => { await execute(Comparator.GT, [0.75]); expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); @@ -129,7 +157,7 @@ describe('The metric threshold alert type', () => { }); describe('querying with a groupBy parameter', () => { - const execute = (comparator: Comparator, threshold: number[]) => + const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => executor({ ...mockOptions, services, @@ -137,7 +165,7 @@ describe('The metric threshold alert type', () => { groupBy: 'something', criteria: [ { - ...baseCriterion, + ...baseNonCountCriterion, comparator, threshold, }, @@ -173,21 +201,23 @@ describe('The metric threshold alert type', () => { comparator: Comparator, thresholdA: number[], thresholdB: number[], - groupBy: string = '' + groupBy: string = '', + sourceId: string = 'default' ) => executor({ ...mockOptions, services, params: { + sourceId, groupBy, criteria: [ { - ...baseCriterion, + ...baseNonCountCriterion, comparator, threshold: thresholdA, }, { - ...baseCriterion, + ...baseNonCountCriterion, comparator, threshold: thresholdB, metric: 'test.metric.2', @@ -228,19 +258,18 @@ describe('The metric threshold alert type', () => { }); describe('querying with the count aggregator', () => { const instanceID = '*'; - const execute = (comparator: Comparator, threshold: number[]) => + const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => executor({ ...mockOptions, services, params: { + sourceId, criteria: [ { - ...baseCriterion, + ...baseCountCriterion, comparator, threshold, - aggType: 'count', - metric: undefined, - }, + } as CountMetricExpressionParams, ], }, }); @@ -253,17 +282,17 @@ describe('The metric threshold alert type', () => { }); describe('querying with the p99 aggregator', () => { const instanceID = '*'; - const execute = (comparator: Comparator, threshold: number[]) => + const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => executor({ ...mockOptions, services, params: { criteria: [ { - ...baseCriterion, + ...baseNonCountCriterion, comparator, threshold, - aggType: 'p99', + aggType: Aggregators.P99, metric: 'test.metric.2', }, ], @@ -278,17 +307,18 @@ describe('The metric threshold alert type', () => { }); describe('querying with the p95 aggregator', () => { const instanceID = '*'; - const execute = (comparator: Comparator, threshold: number[]) => + const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => executor({ ...mockOptions, services, params: { + sourceId, criteria: [ { - ...baseCriterion, + ...baseNonCountCriterion, comparator, threshold, - aggType: 'p95', + aggType: Aggregators.P95, metric: 'test.metric.1', }, ], @@ -303,16 +333,17 @@ describe('The metric threshold alert type', () => { }); describe("querying a metric that hasn't reported data", () => { const instanceID = '*'; - const execute = (alertOnNoData: boolean) => + const execute = (alertOnNoData: boolean, sourceId: string = 'default') => executor({ ...mockOptions, services, params: { + sourceId, criteria: [ { - ...baseCriterion, + ...baseNonCountCriterion, comparator: Comparator.GT, - threshold: 1, + threshold: [1], metric: 'test.metric.3', }, ], @@ -331,18 +362,18 @@ describe('The metric threshold alert type', () => { describe("querying a rate-aggregated metric that hasn't reported data", () => { const instanceID = '*'; - const execute = () => + const execute = (sourceId: string = 'default') => executor({ ...mockOptions, services, params: { criteria: [ { - ...baseCriterion, + ...baseNonCountCriterion, comparator: Comparator.GT, - threshold: 1, + threshold: [1], metric: 'test.metric.3', - aggType: 'rate', + aggType: Aggregators.RATE, }, ], alertOnNoData: true, @@ -370,7 +401,7 @@ describe('The metric threshold alert type', () => { params: { criteria: [ { - ...baseCriterion, + ...baseNonCountCriterion, comparator: Comparator.GT, threshold, }, @@ -417,7 +448,7 @@ describe('The metric threshold alert type', () => { sourceId: 'default', criteria: [ { - ...baseCriterion, + ...baseNonCountCriterion, metric: 'test.metric.pct', comparator: Comparator.GT, threshold: [0.75], @@ -450,11 +481,19 @@ const mockLibs: any = { config: createMockStaticConfiguration({}), }), configuration: createMockStaticConfiguration({}), + metricsRules: { + createLifecycleRuleExecutor: createLifecycleRuleExecutorMock, + }, }; const executor = createMetricThresholdExecutor(mockLibs); -const services: AlertServicesMock = alertsMock.createAlertServices(); +const alertsServices = alertsMock.createAlertServices(); +const services: AlertServicesMock & + LifecycleAlertServices = { + ...alertsServices, + ...ruleRegistryMocks.createLifecycleAlertServices(alertsServices), +}; services.scopedClusterClient.asCurrentUser.search.mockImplementation((params?: any): any => { const from = params?.body.query.bool.filter[0]?.range['@timestamp'].gte; if (params.index === 'alternatebeat-*') return mocks.changedSourceIdResponse(from); @@ -527,9 +566,18 @@ function mostRecentAction(id: string) { return alertInstances.get(id)!.actionQueue.pop(); } -const baseCriterion = { - aggType: 'avg', +const baseNonCountCriterion: Pick< + NonCountMetricExpressionParams, + 'aggType' | 'metric' | 'timeSize' | 'timeUnit' +> = { + aggType: Aggregators.AVERAGE, metric: 'test.metric.1', timeSize: 1, timeUnit: 'm', }; + +const baseCountCriterion: Pick = { + aggType: Aggregators.COUNT, + timeSize: 1, + timeUnit: 'm', +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 190d8e028fe0d..259318b6c93a1 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -8,7 +8,14 @@ import { first, last } from 'lodash'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; -import { RecoveredActionGroup } from '../../../../../alerting/common'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; +import { + ActionGroupIdsOf, + RecoveredActionGroup, + AlertInstanceState, + AlertInstanceContext, +} from '../../../../../alerting/common'; +import { AlertTypeState, AlertInstance } from '../../../../../alerting/server'; import { InfraBackendLibs } from '../../infra_types'; import { buildErrorAlertReason, @@ -20,18 +27,48 @@ import { import { createFormatter } from '../../../../common/formatters'; import { AlertStates, Comparator } from './types'; import { evaluateAlert, EvaluatedAlertParams } from './lib/evaluate_alert'; -import { - MetricThresholdAlertExecutorOptions, - MetricThresholdAlertType, -} from './register_metric_threshold_alert_type'; -export const createMetricThresholdExecutor = ( - libs: InfraBackendLibs -): MetricThresholdAlertType['executor'] => - async function (options: MetricThresholdAlertExecutorOptions) { +export type MetricThresholdAlertTypeParams = Record; +export type MetricThresholdAlertTypeState = AlertTypeState; // no specific state used +export type MetricThresholdAlertInstanceState = AlertInstanceState; // no specific instace state used +export type MetricThresholdAlertInstanceContext = AlertInstanceContext; // no specific instace state used + +type MetricThresholdAllowedActionGroups = ActionGroupIdsOf< + typeof FIRED_ACTIONS | typeof WARNING_ACTIONS +>; + +type MetricThresholdAlertInstance = AlertInstance< + MetricThresholdAlertInstanceState, + MetricThresholdAlertInstanceContext, + MetricThresholdAllowedActionGroups +>; + +type MetricThresholdAlertInstanceFactory = ( + id: string, + reason: string, + threshold?: number | undefined, + value?: number | undefined +) => MetricThresholdAlertInstance; + +export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => + libs.metricsRules.createLifecycleRuleExecutor< + MetricThresholdAlertTypeParams, + MetricThresholdAlertTypeState, + MetricThresholdAlertInstanceState, + MetricThresholdAlertInstanceContext, + MetricThresholdAllowedActionGroups + >(async function (options) { const { services, params } = options; const { criteria } = params; if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions'); + const { alertWithLifecycle, savedObjectsClient } = services; + const alertInstanceFactory: MetricThresholdAlertInstanceFactory = (id, reason) => + alertWithLifecycle({ + id, + fields: { + [ALERT_REASON]: reason, + }, + }); const { sourceId, alertOnNoData } = params as { sourceId?: string; @@ -39,7 +76,7 @@ export const createMetricThresholdExecutor = ( }; const source = await libs.sources.getSourceConfiguration( - services.savedObjectsClient, + savedObjectsClient, sourceId || 'default' ); const config = source.configuration; @@ -114,8 +151,7 @@ export const createMetricThresholdExecutor = ( : nextState === AlertStates.WARNING ? WARNING_ACTIONS.id : FIRED_ACTIONS.id; - const alertInstance = services.alertInstanceFactory(`${group}`); - + const alertInstance = alertInstanceFactory(`${group}`, reason); alertInstance.scheduleActions(actionGroupId, { group, alertState: stateToAlertMessage[nextState], @@ -133,7 +169,7 @@ export const createMetricThresholdExecutor = ( }); } } - }; + }); export const FIRED_ACTIONS = { id: 'metrics.threshold.fired', diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts index 9418762d3e1bf..054585e541ba1 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -7,13 +7,8 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; -import { - AlertType, - AlertInstanceState, - AlertInstanceContext, - AlertExecutorOptions, - ActionGroupIdsOf, -} from '../../../../../alerting/server'; +import { ActionGroupIdsOf } from '../../../../../alerting/common'; +import { AlertType, PluginSetupContract } from '../../../../../alerting/server'; import { METRIC_EXPLORER_AGGREGATIONS } from '../../../../common/http_api/metrics_explorer'; import { createMetricThresholdExecutor, @@ -33,29 +28,17 @@ import { thresholdActionVariableDescription, } from '../common/messages'; -export type MetricThresholdAlertType = AlertType< - /** - * TODO: Remove this use of `any` by utilizing a proper type - */ - Record, - never, // Only use if defining useSavedObjectReferences hook - Record, - AlertInstanceState, - AlertInstanceContext, - ActionGroupIdsOf ->; -export type MetricThresholdAlertExecutorOptions = AlertExecutorOptions< - /** - * TODO: Remove this use of `any` by utilizing a proper type - */ - Record, - Record, - AlertInstanceState, - AlertInstanceContext, - ActionGroupIdsOf +type MetricThresholdAllowedActionGroups = ActionGroupIdsOf< + typeof FIRED_ACTIONS | typeof WARNING_ACTIONS >; +export type MetricThresholdAlertType = Omit & { + ActionGroupIdsOf: MetricThresholdAllowedActionGroups; +}; -export function registerMetricThresholdAlertType(libs: InfraBackendLibs): MetricThresholdAlertType { +export async function registerMetricThresholdAlertType( + alertingPlugin: PluginSetupContract, + libs: InfraBackendLibs +) { const baseCriterion = { threshold: schema.arrayOf(schema.number()), comparator: oneOfLiterals(Object.values(Comparator)), @@ -77,7 +60,7 @@ export function registerMetricThresholdAlertType(libs: InfraBackendLibs): Metric metric: schema.never(), }); - return { + alertingPlugin.registerType({ id: METRIC_THRESHOLD_ALERT_TYPE_ID, name: i18n.translate('xpack.infra.metrics.alertName', { defaultMessage: 'Metric threshold', @@ -115,5 +98,5 @@ export function registerMetricThresholdAlertType(libs: InfraBackendLibs): Metric ], }, producer: 'infrastructure', - }; + }); } diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts index 37f21022f183d..101be1f77b9d0 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts @@ -33,12 +33,12 @@ interface BaseMetricExpressionParams { warningThreshold?: number[]; } -interface NonCountMetricExpressionParams extends BaseMetricExpressionParams { +export interface NonCountMetricExpressionParams extends BaseMetricExpressionParams { aggType: Exclude; metric: string; } -interface CountMetricExpressionParams extends BaseMetricExpressionParams { +export interface CountMetricExpressionParams extends BaseMetricExpressionParams { aggType: Aggregators.COUNT; metric: never; } diff --git a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts index d7df2afd8038b..d0af9ac4ce669 100644 --- a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts @@ -20,10 +20,13 @@ const registerAlertTypes = ( ml?: MlPluginSetup ) => { if (alertingPlugin) { - alertingPlugin.registerType(registerMetricThresholdAlertType(libs)); alertingPlugin.registerType(registerMetricAnomalyAlertType(libs, ml)); - const registerFns = [registerLogThresholdAlertType, registerMetricInventoryThresholdAlertType]; + const registerFns = [ + registerLogThresholdAlertType, + registerMetricInventoryThresholdAlertType, + registerMetricThresholdAlertType, + ]; registerFns.forEach((fn) => { fn(alertingPlugin, libs); }); diff --git a/x-pack/plugins/lens/common/suffix_formatter/index.ts b/x-pack/plugins/lens/common/suffix_formatter/index.ts index 12a4e02a81ef2..97fa8c067331e 100644 --- a/x-pack/plugins/lens/common/suffix_formatter/index.ts +++ b/x-pack/plugins/lens/common/suffix_formatter/index.ts @@ -31,6 +31,7 @@ export const unitSuffixesLong: Record = { export function getSuffixFormatter(formatFactory: FormatFactory): FieldFormatInstanceType { return class SuffixFormatter extends FieldFormat { static id = 'suffix'; + static hidden = true; // Don't want this format to appear in index pattern editor static title = i18n.translate('xpack.lens.fieldFormats.suffix.title', { defaultMessage: 'Suffix', }); diff --git a/x-pack/plugins/lens/common/suffix_formatter/suffix_formatter.test.ts b/x-pack/plugins/lens/common/suffix_formatter/suffix_formatter.test.ts index c4379bdd1fb34..d08908ecde417 100644 --- a/x-pack/plugins/lens/common/suffix_formatter/suffix_formatter.test.ts +++ b/x-pack/plugins/lens/common/suffix_formatter/suffix_formatter.test.ts @@ -42,4 +42,11 @@ describe('suffix formatter', () => { expect(result).toEqual(''); }); + + it('should be a hidden formatter', () => { + const convertMock = jest.fn((x) => ''); + const formatFactory = jest.fn(() => ({ convert: convertMock })); + const SuffixFormatter = getSuffixFormatter((formatFactory as unknown) as FormatFactory); + expect(SuffixFormatter.hidden).toBe(true); + }); }); diff --git a/x-pack/plugins/lens/public/_mixins.scss b/x-pack/plugins/lens/public/_mixins.scss index f9b8ce466040e..5a798bcc6c23b 100644 --- a/x-pack/plugins/lens/public/_mixins.scss +++ b/x-pack/plugins/lens/public/_mixins.scss @@ -47,3 +47,45 @@ @mixin lnsDroppableNotAllowed { opacity: .5; } + +// Removes EUI focus ring +@mixin removeEuiFocusRing { + @include kbnThemeStyle('v7') { + animation: none !important; // sass-lint:disable-line no-important + } + + @include kbnThemeStyle('v8') { + outline: none; + + &:focus-visible { + outline-style: none; + } + } +} + +// Passes focus ring styles down to a child of a focused element +@mixin passDownFocusRing($target) { + @include removeEuiFocusRing; + + #{$target} { + @include euiFocusBackground; + + @include kbnThemeStyle('v7') { + @include euiFocusRing; + } + + @include kbnThemeStyle('v8') { + outline: $euiFocusRingSize solid currentColor; // Safari & Firefox + } + } + + @include kbnThemeStyle('v8') { + &:focus-visible #{$target} { + outline-style: auto; // Chrome + } + + &:not(:focus-visible) #{$target} { + outline: none; + } + } +} \ No newline at end of file diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 1c49527d9eca8..6bd75c585f954 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -30,7 +30,7 @@ import { esFilters, FilterManager, IFieldType, - IIndexPattern, + IndexPattern, Query, } from '../../../../../src/plugins/data/public'; import { TopNavMenuData } from '../../../../../src/plugins/navigation/public'; @@ -182,7 +182,7 @@ describe('Lens App', () => { it('updates global filters with store state', async () => { const services = makeDefaultServices(sessionIdSubject); - const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; + const indexPattern = ({ id: 'index1' } as unknown) as IndexPattern; const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType; const pinnedFilter = esFilters.buildExistsFilter(pinnedField, indexPattern); services.data.query.filterManager.getFilters = jest.fn().mockImplementation(() => { @@ -634,7 +634,7 @@ describe('Lens App', () => { }); it('saves app filters and does not save pinned filters', async () => { - const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; + const indexPattern = ({ id: 'index1' } as unknown) as IndexPattern; const field = ({ name: 'myfield' } as unknown) as IFieldType; const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType; const unpinned = esFilters.buildExistsFilter(field, indexPattern); @@ -816,7 +816,7 @@ describe('Lens App', () => { it('updates the filters when the user changes them', async () => { const { instance, services, lensStore } = await mountWith({}); - const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; + const indexPattern = ({ id: 'index1' } as unknown) as IndexPattern; const field = ({ name: 'myfield' } as unknown) as IFieldType; expect(lensStore.getState()).toEqual({ lens: expect.objectContaining({ @@ -871,7 +871,7 @@ describe('Lens App', () => { searchSessionId: `sessionId-3`, }), }); - const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; + const indexPattern = ({ id: 'index1' } as unknown) as IndexPattern; const field = ({ name: 'myfield' } as unknown) as IFieldType; act(() => services.data.query.filterManager.setFilters([ @@ -1006,7 +1006,7 @@ describe('Lens App', () => { query: { query: 'new', language: 'lucene' }, }) ); - const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; + const indexPattern = ({ id: 'index1' } as unknown) as IndexPattern; const field = ({ name: 'myfield' } as unknown) as IFieldType; const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType; const unpinned = esFilters.buildExistsFilter(field, indexPattern); @@ -1063,7 +1063,7 @@ describe('Lens App', () => { query: { query: 'new', language: 'lucene' }, }) ); - const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; + const indexPattern = ({ id: 'index1' } as unknown) as IndexPattern; const field = ({ name: 'myfield' } as unknown) as IFieldType; const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType; const unpinned = esFilters.buildExistsFilter(field, indexPattern); diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss index 9c5bc79ba044a..5522b65fca261 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss @@ -20,7 +20,15 @@ transform: translate(-12px, 8px); z-index: $lnsZLevel3; pointer-events: none; - box-shadow: 0 0 0 $euiFocusRingSize $euiFocusRingColor; + + @include kbnThemeStyle('v7') { + box-shadow: 0 0 0 $euiFocusRingSize $euiFocusRingColor; + } + + @include kbnThemeStyle('v8') { + outline: $euiFocusRingSize solid currentColor; // Safari & Firefox + outline-style: auto; // Chrome + } } // Draggable item @@ -133,6 +141,7 @@ &:focus-within { @include euiFocusRing; pointer-events: none; + z-index: $lnsZLevel2; } } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index c7147e75af59a..d4a9870056b34 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -199,7 +199,7 @@ export function LayerPanels( })} content={i18n.translate('xpack.lens.xyChart.addLayerTooltip', { defaultMessage: - 'Use multiple layers to combine chart types or visualize different index patterns.', + 'Use multiple layers to combine visualization types or visualize different index patterns.', })} position="bottom" > diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss index 788bf049b779b..fd37a7bada02f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss @@ -1,3 +1,5 @@ +@import '../../../mixins'; + .lnsLayerPanel { margin-bottom: $euiSizeS; @@ -132,12 +134,7 @@ width: 100%; &:focus { + @include passDownFocusRing('.lnsLayerPanel__triggerTextLabel'); background-color: transparent; - animation: none !important; // sass-lint:disable-line no-important - } - - &:focus .lnsLayerPanel__triggerTextLabel, - &:focus-within .lnsLayerPanel__triggerTextLabel { - background-color: transparentize($euiColorVis1, .9); } } \ No newline at end of file diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx index 6445038e40d7c..44fb47001631e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx @@ -16,7 +16,7 @@ import { } from '../../mocks'; import { act } from 'react-dom/test-utils'; import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; -import { esFilters, IFieldType, IIndexPattern } from '../../../../../../src/plugins/data/public'; +import { esFilters, IFieldType, IndexPattern } from '../../../../../../src/plugins/data/public'; import { SuggestionPanel, SuggestionPanelProps } from './suggestion_panel'; import { getSuggestions, Suggestion } from './suggestion_helpers'; import { EuiIcon, EuiPanel, EuiToolTip } from '@elastic/eui'; @@ -291,7 +291,7 @@ describe('suggestion_panel', () => { (mockVisualization.toPreviewExpression as jest.Mock).mockReturnValueOnce('test | expression'); mockDatasource.toExpression.mockReturnValue('datasource_expression'); - const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; + const indexPattern = ({ id: 'index1' } as unknown) as IndexPattern; const field = ({ name: 'myfield' } as unknown) as IFieldType; mountWithProvider( diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx index f948ec6a59687..314989ecc9758 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx @@ -370,7 +370,7 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) { 'xpack.lens.chartSwitch.dataLossDescription', { defaultMessage: - 'Selecting this chart type will result in a partial loss of currently applied configuration selections.', + 'Selecting this visualization type will result in a partial loss of currently applied configuration selections.', } )} iconProps={{ @@ -439,8 +439,8 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) { - {i18n.translate('xpack.lens.configPanel.chartType', { - defaultMessage: 'Chart type', + {i18n.translate('xpack.lens.configPanel.visualizationType', { + defaultMessage: 'Visualization type', })} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index 4feb13fcfffd9..784455cc9f6d1 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -28,7 +28,7 @@ import { ReactWrapper } from 'enzyme'; import { DragDrop, ChildDragDropProvider } from '../../../drag_drop'; import { fromExpression } from '@kbn/interpreter/common'; import { coreMock } from 'src/core/public/mocks'; -import { esFilters, IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/public'; +import { esFilters, IFieldType, IndexPattern } from '../../../../../../../src/plugins/data/public'; import { UiActionsStart } from '../../../../../../../src/plugins/ui_actions/public'; import { uiActionsPluginMock } from '../../../../../../../src/plugins/ui_actions/public/mocks'; import { TriggerContract } from '../../../../../../../src/plugins/ui_actions/public/triggers'; @@ -443,7 +443,7 @@ describe('workspace_panel', () => { expect(expressionRendererMock).toHaveBeenCalledTimes(1); - const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; + const indexPattern = ({ id: 'index1' } as unknown) as IndexPattern; const field = ({ name: 'myfield' } as unknown) as IFieldType; await act(async () => { diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 77b2b06389240..e26466be6f81b 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -12,7 +12,6 @@ import type { ExecutionContextServiceStart } from 'src/core/public'; import { ExecutionContextSearch, Filter, - IIndexPattern, Query, TimefilterContract, TimeRange, @@ -83,7 +82,7 @@ export type LensByReferenceInput = SavedObjectEmbeddableInput & LensBaseEmbeddab export type LensEmbeddableInput = LensByValueInput | LensByReferenceInput; export interface LensEmbeddableOutput extends EmbeddableOutput { - indexPatterns?: IIndexPattern[]; + indexPatterns?: IndexPattern[]; } export interface LensEmbeddableDeps { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx index 0a41e7e65212a..e643ea12528ee 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx @@ -70,7 +70,7 @@ export function ChangeIndexPattern({
{i18n.translate('xpack.lens.indexPattern.changeIndexPatternTitle', { - defaultMessage: 'Change index pattern', + defaultMessage: 'Index pattern', })} = { // Wrapper around esQuery.buildEsQuery, handling errors (e.g. because a query can't be parsed) by // returning a query dsl object not matching anything function buildSafeEsQuery( - indexPattern: IIndexPattern, + indexPattern: IndexPattern, query: Query, filters: Filter[], queryConfig: EsQueryConfig @@ -164,7 +164,7 @@ export function IndexPatternDataPanel({ })); const dslQuery = buildSafeEsQuery( - indexPatterns[currentIndexPatternId] as IIndexPattern, + indexPatterns[currentIndexPatternId], query, filters, esQuery.getEsQueryConfig(core.uiSettings) @@ -269,7 +269,7 @@ const defaultFieldGroups: { }; const fieldFiltersLabel = i18n.translate('xpack.lens.indexPatterns.fieldFiltersLabel', { - defaultMessage: 'Field filters', + defaultMessage: 'Filter by type', }); const htmlId = htmlIdGenerator('datapanel'); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index d757d8573f25a..5318255792641 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -6,6 +6,7 @@ */ import { ReactWrapper, ShallowWrapper } from 'enzyme'; +import 'jest-canvas-mock'; import React, { ChangeEvent, MouseEvent, ReactElement } from 'react'; import { act } from 'react-dom/test-utils'; import { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx index 2c6463f6b8e96..21251b59d4533 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx @@ -10,6 +10,7 @@ import { ReactWrapper, ShallowWrapper } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { EuiComboBox } from '@elastic/eui'; import { mountWithIntl as mount } from '@kbn/test/jest'; +import 'jest-canvas-mock'; import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import type { DataPublicPluginStart } from 'src/plugins/data/public'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.scss b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.scss index a652a18752949..b96a670144d37 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.scss +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.scss @@ -1,5 +1,23 @@ +@import '../mixins'; + .lnsFieldItem { width: 100%; + + &.kbnFieldButton { + &:focus-within, + &-isActive { + @include removeEuiFocusRing; + } + } + + .kbnFieldButton__button:focus { + @include passDownFocusRing('.kbnFieldButton__name > span'); + + .kbnFieldButton__name > span { + text-decoration: underline; + } + } + .lnsFieldItem__infoIcon { visibility: hidden; opacity: 0; @@ -14,25 +32,6 @@ transition: opacity $euiAnimSpeedFast ease-in-out 1s; } } - - &:focus, - &:focus-within, - .kbnFieldButton__button:focus:focus-visible, - &.kbnFieldButton-isActive { - @include kbnThemeStyle('v7') { - animation: none !important; // sass-lint:disable-line no-important - } - @include kbnThemeStyle('v8') { - outline: none !important; // sass-lint:disable-line no-important - } - } - - &:focus .kbnFieldButton__name span, - &:focus-within .kbnFieldButton__name span, - &.kbnFieldButton-isActive .kbnFieldButton__name span { - background-color: transparentize($euiColorVis1, .9) !important; - text-decoration: underline !important; - } } .kbnFieldButton.lnsDragDrop_ghost { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index 013bb46500d0d..5ceb452038426 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -44,7 +44,6 @@ import { ES_FIELD_TYPES, Filter, esQuery, - IIndexPattern, } from '../../../../../src/plugins/data/public'; import { FieldButton } from '../../../../../src/plugins/kibana_react/public'; import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; @@ -169,7 +168,7 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { .post(`/api/lens/index_stats/${indexPattern.id}/field`, { body: JSON.stringify({ dslQuery: esQuery.buildEsQuery( - indexPattern as IIndexPattern, + indexPattern, query, filters, esQuery.getEsQueryConfig(core.uiSettings) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index bf0d022f0ad9b..261a73287dba9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -5,6 +5,8 @@ * 2.0. */ +import React from 'react'; +import 'jest-canvas-mock'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { getIndexPatternDatasource, IndexPatternColumn } from './indexpattern'; import { DatasourcePublicAPI, Operation, Datasource, FramePublicAPI } from '../types'; @@ -18,8 +20,6 @@ import { operationDefinitionMap, getErrorMessages } from './operations'; import { createMockedFullReference } from './operations/mocks'; import { indexPatternFieldEditorPluginMock } from 'src/plugins/index_pattern_field_editor/public/mocks'; import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/public/mocks'; -import React from 'react'; - jest.mock('./loader'); jest.mock('../id_generator'); jest.mock('./operations'); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index 93ea3069894d3..4b8bbc09c6799 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -8,6 +8,7 @@ import { DatasourceSuggestion } from '../types'; import { generateId } from '../id_generator'; import type { IndexPatternPrivateState } from './types'; +import 'jest-canvas-mock'; import { getDatasourceSuggestionsForField, getDatasourceSuggestionsFromCurrentState, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index a458a1edcfa16..4e2f69c927a18 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -19,11 +19,7 @@ import { import { uniq } from 'lodash'; import { CoreStart } from 'kibana/public'; import { FieldStatsResponse } from '../../../../../common'; -import { - AggFunctionsMapping, - esQuery, - IIndexPattern, -} from '../../../../../../../../src/plugins/data/public'; +import { AggFunctionsMapping, esQuery } from '../../../../../../../../src/plugins/data/public'; import { buildExpressionFunction } from '../../../../../../../../src/plugins/expressions/public'; import { updateColumnParam, isReferenced } from '../../layer_helpers'; import { DataType, FramePublicAPI } from '../../../../types'; @@ -99,7 +95,7 @@ function getDisallowedTermsMessage( body: JSON.stringify({ fieldName, dslQuery: esQuery.buildEsQuery( - indexPattern as IIndexPattern, + indexPattern, frame.query, frame.filters, esQuery.getEsQueryConfig(core.uiSettings) diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index ac0aa6cd4b1f1..d25726951ea8f 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -250,7 +250,7 @@ export function PieComponent( { visible: true, }); }); + + test('it should format the boolean values correctly', () => { + const data: LensMultiTable = { + type: 'lens_multitable', + tables: { + first: { + type: 'datatable', + columns: [ + { + id: 'a', + name: 'a', + meta: { type: 'number', params: { id: 'number', params: { pattern: '0,0.000' } } }, + }, + { + id: 'b', + name: 'b', + meta: { type: 'number', params: { id: 'number', params: { pattern: '000,0' } } }, + }, + { + id: 'c', + name: 'c', + meta: { + type: 'boolean', + params: { id: 'boolean' }, + }, + }, + ], + rows: [ + { a: 5, b: 2, c: 0 }, + { a: 19, b: 5, c: 1 }, + ], + }, + }, + dateRange: { + fromDate: new Date('2019-01-02T05:00:00.000Z'), + toDate: new Date('2019-01-03T05:00:00.000Z'), + }, + }; + const timeSampleLayer: LayerArgs = { + layerId: 'first', + seriesType: 'line', + xAccessor: 'c', + accessors: ['a', 'b'], + xScaleType: 'ordinal', + yScaleType: 'linear', + isHistogram: false, + palette: mockPaletteOutput, + }; + const args = createArgsWithLayers([timeSampleLayer]); + + const getCustomFormatSpy = jest.fn(); + getCustomFormatSpy.mockReturnValue({ convert: jest.fn((x) => Boolean(x)) }); + + const component = shallow( + + ); + + expect(component.find(LineSeries).at(1).prop('data')).toEqual([ + { + a: 5, + b: 2, + c: false, + }, + { + a: 19, + b: 5, + c: true, + }, + ]); + }); }); describe('calculateMinInterval', () => { diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 56867c625bb6f..b7f22ebf8968d 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -167,7 +167,7 @@ export const getXyChartRenderer = (dependencies: { }); function getValueLabelsStyling(isHorizontal: boolean) { - const VALUE_LABELS_MAX_FONTSIZE = 15; + const VALUE_LABELS_MAX_FONTSIZE = 12; const VALUE_LABELS_MIN_FONTSIZE = 10; const VALUE_LABELS_VERTICAL_OFFSET = -10; const VALUE_LABELS_HORIZONTAL_OFFSET = 10; @@ -175,7 +175,7 @@ function getValueLabelsStyling(isHorizontal: boolean) { return { displayValue: { fontSize: { min: VALUE_LABELS_MIN_FONTSIZE, max: VALUE_LABELS_MAX_FONTSIZE }, - fill: { textInverted: true, textBorder: 2 }, + fill: { textContrast: true, textInverted: false, textBorder: 0 }, alignment: isHorizontal ? { vertical: VerticalAlignment.Middle, @@ -616,7 +616,7 @@ export function XYChart({ for (const column of table.columns) { const record = newRow[column.id]; if ( - record && + record != null && // pre-format values for ordinal x axes because there can only be a single x axis formatter on chart level (!isPrimitive(record) || (column.id === xAccessor && xScaleType === 'ordinal')) ) { @@ -792,9 +792,12 @@ export function XYChart({ // * in some scenarios value labels are not strings, and this breaks the elastic-chart lib valueFormatter: (d: unknown) => yAxis?.formatter?.convert(d) || '', showValueLabel: shouldShowValueLabels && valueLabels !== 'hide', + isValueContainedInElement: false, isAlternatingValueLabel: false, - isValueContainedInElement: true, - overflowConstraints: [LabelOverflowConstraint.ChartEdges], + overflowConstraints: [ + LabelOverflowConstraint.ChartEdges, + LabelOverflowConstraint.BarGeometry, + ], }, }; return ; diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx index 69ec0740948fc..f0cec4abf0b14 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx @@ -10,7 +10,7 @@ import React, { ReactElement } from 'react'; import { i18n } from '@kbn/i18n'; import rison from 'rison-node'; import { Feature } from 'geojson'; -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { convertCompositeRespToGeoJson, convertRegularRespToGeoJson, @@ -274,7 +274,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle const requestId: string = afterKey ? `${this.getId()} afterKey ${afterKey.geoSplit}` : this.getId(); - const esResponse: SearchResponse = await this._runEsQuery({ + const esResponse: estypes.SearchResponse = await this._runEsQuery({ requestId, requestName: `${layerName} (${requestCount})`, searchSource, @@ -291,8 +291,10 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle features.push(...convertCompositeRespToGeoJson(esResponse, this._descriptor.requestType)); - afterKey = esResponse.aggregations.compositeSplit.after_key; - if (esResponse.aggregations.compositeSplit.buckets.length < gridsPerRequest) { + const aggr = esResponse.aggregations + ?.compositeSplit as estypes.AggregationsCompositeBucketAggregate; + afterKey = aggr.after_key; + if (aggr.buckets.length < gridsPerRequest) { // Finished because request did not get full resultset back break; } diff --git a/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.ts b/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.ts index 9b637ab4bab99..3e85c5db28a6c 100644 --- a/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.ts +++ b/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.ts @@ -87,7 +87,7 @@ export class ESTooltipProperty implements ITooltipProperty { existsFilter.meta.negate = true; return [existsFilter]; } else { - return [esFilters.buildPhraseFilter(indexPatternField, value, this._indexPattern)]; + return [esFilters.buildPhraseFilter(indexPatternField, value as string, this._indexPattern)]; } } } diff --git a/x-pack/plugins/maps/public/embeddable/types.ts b/x-pack/plugins/maps/public/embeddable/types.ts index dfd87bca19ac3..fd8160c567530 100644 --- a/x-pack/plugins/maps/public/embeddable/types.ts +++ b/x-pack/plugins/maps/public/embeddable/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IIndexPattern } from '../../../../../src/plugins/data/common/index_patterns'; +import type { IndexPattern } from '../../../../../src/plugins/data/common/index_patterns'; import { EmbeddableInput, EmbeddableOutput, @@ -41,5 +41,5 @@ export type MapByReferenceInput = SavedObjectEmbeddableInput & { export type MapEmbeddableInput = MapByValueInput | MapByReferenceInput; export type MapEmbeddableOutput = EmbeddableOutput & { - indexPatterns: IIndexPattern[]; + indexPatterns: IndexPattern[]; }; diff --git a/x-pack/plugins/ml/common/constants/index_patterns.ts b/x-pack/plugins/ml/common/constants/index_patterns.ts index cec692217546d..d7d6c343e282b 100644 --- a/x-pack/plugins/ml/common/constants/index_patterns.ts +++ b/x-pack/plugins/ml/common/constants/index_patterns.ts @@ -11,4 +11,3 @@ export const ML_ANNOTATIONS_INDEX_PATTERN = '.ml-annotations-6'; export const ML_RESULTS_INDEX_PATTERN = '.ml-anomalies-*'; export const ML_NOTIFICATION_INDEX_PATTERN = '.ml-notifications*'; -export const ML_NOTIFICATION_INDEX_02 = '.ml-notifications-000002'; diff --git a/x-pack/plugins/ml/common/types/data_frame_analytics.ts b/x-pack/plugins/ml/common/types/data_frame_analytics.ts index 3305eeaaf4794..ec2a244c75468 100644 --- a/x-pack/plugins/ml/common/types/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/types/data_frame_analytics.ts @@ -80,9 +80,9 @@ export interface DataFrameAnalyticsConfig { runtime_mappings?: RuntimeMappings; }; analysis: AnalysisConfig; - analyzed_fields: { - includes: string[]; - excludes: string[]; + analyzed_fields?: { + includes?: string[]; + excludes?: string[]; }; model_memory_limit: string; max_num_threads?: number; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx index 6c158f103aade..48477acfe7be8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx @@ -116,8 +116,8 @@ export const ExplorationPageWrapper: FC = ({ const resultsField = jobConfig?.dest.results_field ?? ''; const scatterplotFieldOptions = useScatterplotFieldOptions( indexPattern, - jobConfig?.analyzed_fields.includes, - jobConfig?.analyzed_fields.excludes, + jobConfig?.analyzed_fields?.includes, + jobConfig?.analyzed_fields?.excludes, resultsField ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx index 5f013c634e4c4..abd1870babfb9 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx @@ -92,8 +92,8 @@ export const OutlierExploration: FC = React.memo(({ jobId }) = const scatterplotFieldOptions = useScatterplotFieldOptions( indexPattern, - jobConfig?.analyzed_fields.includes, - jobConfig?.analyzed_fields.excludes, + jobConfig?.analyzed_fields?.includes, + jobConfig?.analyzed_fields?.excludes, resultsField ); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx index 9a4d6036428f8..92662f409d0f3 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx @@ -30,6 +30,7 @@ export const JobMessagesPane: FC = React.memo( const canCreateJob = checkPermission('canCreateJob'); const [messages, setMessages] = useState([]); + const [notificationIndices, setNotificationIndices] = useState([]); const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const [isClearing, setIsClearing] = useState(false); @@ -42,7 +43,10 @@ export const JobMessagesPane: FC = React.memo( const fetchMessages = async () => { setIsLoading(true); try { - setMessages(await ml.jobs.jobAuditMessages({ jobId, start, end })); + const messagesResp = await ml.jobs.jobAuditMessages({ jobId, start, end }); + + setMessages(messagesResp.messages); + setNotificationIndices(messagesResp.notificationIndices); setIsLoading(false); } catch (error) { setIsLoading(false); @@ -63,7 +67,7 @@ export const JobMessagesPane: FC = React.memo( const clearMessages = useCallback(async () => { setIsClearing(true); try { - await clearJobAuditMessages(jobId); + await clearJobAuditMessages(jobId, notificationIndices); setIsClearing(false); if (typeof refreshJobList === 'function') { refreshJobList(); @@ -77,13 +81,13 @@ export const JobMessagesPane: FC = React.memo( }) ); } - }, [jobId]); + }, [jobId, JSON.stringify(notificationIndices)]); useEffect(() => { fetchMessages(); }, []); - const disabled = messages.length > 0 && messages[0].clearable === false; + const disabled = notificationIndices.length === 0; const clearButton = ( ({ ...(start !== undefined && end !== undefined ? { start, end } : {}), }; - return httpService.http({ + return httpService.http<{ messages: JobMessage[]; notificationIndices: string[] }>({ path: `${ML_BASE_PATH}/job_audit_messages/messages${jobIdString}`, method: 'GET', query, }); }, - clearJobAuditMessages(jobId: string) { - const body = JSON.stringify({ jobId }); + clearJobAuditMessages(jobId: string, notificationIndices: string[]) { + const body = JSON.stringify({ jobId, notificationIndices }); return httpService.http<{ success: boolean; latest_cleared: number }>({ path: `${ML_BASE_PATH}/job_audit_messages/clear_messages`, method: 'PUT', diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts index 216a4379c7c89..b39debbe664d3 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts @@ -448,7 +448,7 @@ export async function validateAnalyticsJob( ) { const messages = await getValidationCheckMessages( client.asCurrentUser, - job.analyzed_fields.includes, + job?.analyzed_fields?.includes || [], job.analysis, job.source ); diff --git a/x-pack/plugins/ml/server/models/job_audit_messages/is_clearable.test.ts b/x-pack/plugins/ml/server/models/job_audit_messages/is_clearable.test.ts new file mode 100644 index 0000000000000..8b84bfeb888b2 --- /dev/null +++ b/x-pack/plugins/ml/server/models/job_audit_messages/is_clearable.test.ts @@ -0,0 +1,35 @@ +/* + * 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 { isClearable } from './job_audit_messages'; + +const supportedNotificationIndices = [ + '.ml-notifications-000002', + '.ml-notifications-000003', + '.ml-notifications-000004', +]; + +const unsupportedIndices = ['.ml-notifications-000001', 'index-does-not-exist']; + +describe('jobAuditMessages - isClearable', () => { + it('should return true for indices ending in a six digit number with the last number >= 2', () => { + supportedNotificationIndices.forEach((index) => { + expect(isClearable(index)).toEqual(true); + }); + }); + + it('should return false for indices not ending in a six digit number with the last number >= 2', () => { + unsupportedIndices.forEach((index) => { + expect(isClearable(index)).toEqual(false); + }); + }); + + it('should return false for empty string or missing argument', () => { + expect(isClearable('')).toEqual(false); + expect(isClearable()).toEqual(false); + }); +}); diff --git a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts index 60ea866978f1a..d3748163957db 100644 --- a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts +++ b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts @@ -8,6 +8,9 @@ import { IScopedClusterClient } from 'kibana/server'; import type { MlClient } from '../../lib/ml_client'; import type { JobSavedObjectService } from '../../saved_objects'; +import { JobMessage } from '../../../common/types/audit_message'; + +export function isClearable(index?: string): boolean; export function jobAuditMessagesProvider( client: IScopedClusterClient, @@ -21,7 +24,10 @@ export function jobAuditMessagesProvider( start?: string; end?: string; } - ) => any; + ) => { messages: JobMessage[]; notificationIndices: string[] }; getAuditMessagesSummary: (jobIds?: string[]) => any; - clearJobAuditMessages: (jobId: string) => any; + clearJobAuditMessages: ( + jobId: string, + notificationIndices: string[] + ) => { success: boolean; last_cleared: number }; }; diff --git a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js index 137df3a6f3151..311df2ac418c0 100644 --- a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js +++ b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js @@ -5,10 +5,7 @@ * 2.0. */ -import { - ML_NOTIFICATION_INDEX_PATTERN, - ML_NOTIFICATION_INDEX_02, -} from '../../../common/constants/index_patterns'; +import { ML_NOTIFICATION_INDEX_PATTERN } from '../../../common/constants/index_patterns'; import { MESSAGE_LEVEL } from '../../../common/constants/message_levels'; import moment from 'moment'; @@ -39,6 +36,14 @@ const anomalyDetectorTypeFilter = { }, }; +export function isClearable(index) { + if (typeof index === 'string') { + const match = index.match(/\d{6}$/); + return match !== null && match.length && Number(match[match.length - 1]) >= 2; + } + return false; +} + export function jobAuditMessagesProvider({ asInternalUser }, mlClient) { // search for audit messages, // jobId is optional. without it, all jobs will be listed. @@ -126,18 +131,25 @@ export function jobAuditMessagesProvider({ asInternalUser }, mlClient) { }); let messages = []; + const notificationIndices = []; + if (body.hits.total.value > 0) { - messages = body.hits.hits.map((hit) => ({ - clearable: hit._index === ML_NOTIFICATION_INDEX_02, - ...hit._source, - })); + let notificationIndex; + body.hits.hits.forEach((hit) => { + if (notificationIndex !== hit._index && isClearable(hit._index)) { + notificationIndices.push(hit._index); + notificationIndex = hit._index; + } + + messages.push(hit._source); + }); } messages = await jobSavedObjectService.filterJobsForSpace( 'anomaly-detector', messages, 'job_id' ); - return messages; + return { messages, notificationIndices }; } // search highest, most recent audit messages for all jobs for the last 24hrs. @@ -281,7 +293,7 @@ export function jobAuditMessagesProvider({ asInternalUser }, mlClient) { const clearedTime = new Date().getTime(); // Sets 'cleared' to true for messages in the last 24hrs and index new message for clear action - async function clearJobAuditMessages(jobId) { + async function clearJobAuditMessages(jobId, notificationIndices) { const newClearedMessage = { job_id: jobId, job_type: 'anomaly_detection', @@ -309,9 +321,9 @@ export function jobAuditMessagesProvider({ asInternalUser }, mlClient) { }, }; - await Promise.all([ + const promises = [ asInternalUser.updateByQuery({ - index: ML_NOTIFICATION_INDEX_02, + index: notificationIndices.join(','), ignore_unavailable: true, refresh: false, conflicts: 'proceed', @@ -323,12 +335,16 @@ export function jobAuditMessagesProvider({ asInternalUser }, mlClient) { }, }, }), - asInternalUser.index({ - index: ML_NOTIFICATION_INDEX_02, - body: newClearedMessage, - refresh: 'wait_for', - }), - ]); + ...notificationIndices.map((index) => + asInternalUser.index({ + index, + body: newClearedMessage, + refresh: 'wait_for', + }) + ), + ]; + + await Promise.all(promises); return { success: true, last_cleared: clearedTime }; } diff --git a/x-pack/plugins/ml/server/routes/job_audit_messages.ts b/x-pack/plugins/ml/server/routes/job_audit_messages.ts index 1548427797e16..4dcaca573fc17 100644 --- a/x-pack/plugins/ml/server/routes/job_audit_messages.ts +++ b/x-pack/plugins/ml/server/routes/job_audit_messages.ts @@ -121,8 +121,8 @@ export function jobAuditMessagesRoutes({ router, routeGuard }: RouteInitializati async ({ client, mlClient, request, response, jobSavedObjectService }) => { try { const { clearJobAuditMessages } = jobAuditMessagesProvider(client, mlClient); - const { jobId } = request.body; - const resp = await clearJobAuditMessages(jobId); + const { jobId, notificationIndices } = request.body; + const resp = await clearJobAuditMessages(jobId, notificationIndices); return response.ok({ body: resp, diff --git a/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.ts index 525ac73fde120..aeff76f057fc6 100644 --- a/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.ts @@ -20,4 +20,5 @@ export const jobAuditMessagesQuerySchema = schema.object({ export const clearJobAuditMessagesBodySchema = schema.object({ jobId: schema.string(), + notificationIndices: schema.arrayOf(schema.string()), }); diff --git a/x-pack/plugins/monitoring/common/index.js b/x-pack/plugins/monitoring/common/index.ts similarity index 95% rename from x-pack/plugins/monitoring/common/index.js rename to x-pack/plugins/monitoring/common/index.ts index b71419e8c3dd9..371a4172ebbc0 100644 --- a/x-pack/plugins/monitoring/common/index.js +++ b/x-pack/plugins/monitoring/common/index.ts @@ -5,4 +5,5 @@ * 2.0. */ +// @ts-ignore export { formatTimestampToDuration } from './format_timestamp_to_duration'; diff --git a/x-pack/plugins/monitoring/public/alerts/enable_alerts_modal.tsx b/x-pack/plugins/monitoring/public/alerts/enable_alerts_modal.tsx index fadf4c5872507..827ce958deb11 100644 --- a/x-pack/plugins/monitoring/public/alerts/enable_alerts_modal.tsx +++ b/x-pack/plugins/monitoring/public/alerts/enable_alerts_modal.tsx @@ -134,7 +134,10 @@ export const EnableAlertsModal: React.FC = ({ alerts }: Props) => { - + { triggeredMS: 0, }; let alertStates: AlertState[] = []; - const licenseService = null; const rulesClient = { find: jest.fn(() => ({ total: 1, @@ -74,7 +73,7 @@ describe('fetchStatus', () => { }); it('should fetch from the alerts client', async () => { - const status = await fetchStatus(rulesClient as any, licenseService as any, alertTypes, [ + const status = await fetchStatus(rulesClient as any, alertTypes, [ defaultClusterState.clusterUuid, ]); expect(status).toEqual({ @@ -96,7 +95,7 @@ describe('fetchStatus', () => { }, ]; - const status = await fetchStatus(rulesClient as any, licenseService as any, alertTypes, [ + const status = await fetchStatus(rulesClient as any, alertTypes, [ defaultClusterState.clusterUuid, ]); expect(Object.values(status).length).toBe(1); @@ -105,9 +104,7 @@ describe('fetchStatus', () => { }); it('should pass in the right filter to the alerts client', async () => { - await fetchStatus(rulesClient as any, licenseService as any, alertTypes, [ - defaultClusterState.clusterUuid, - ]); + await fetchStatus(rulesClient as any, alertTypes, [defaultClusterState.clusterUuid]); expect((rulesClient.find as jest.Mock).mock.calls[0][0].options.filter).toBe( `alert.attributes.alertTypeId:${alertType}` ); @@ -118,7 +115,7 @@ describe('fetchStatus', () => { alertTypeState: null, })) as any; - const status = await fetchStatus(rulesClient as any, licenseService as any, alertTypes, [ + const status = await fetchStatus(rulesClient as any, alertTypes, [ defaultClusterState.clusterUuid, ]); expect(status[alertType].states.length).toEqual(0); @@ -130,7 +127,7 @@ describe('fetchStatus', () => { data: [], })) as any; - const status = await fetchStatus(rulesClient as any, licenseService as any, alertTypes, [ + const status = await fetchStatus(rulesClient as any, alertTypes, [ defaultClusterState.clusterUuid, ]); expect(status).toEqual({}); @@ -146,7 +143,6 @@ describe('fetchStatus', () => { }; await fetchStatus( rulesClient as any, - customLicenseService as any, [ALERT_CLUSTER_HEALTH], [defaultClusterState.clusterUuid] ); @@ -183,7 +179,6 @@ describe('fetchStatus', () => { }; const status = await fetchStatus( customRulesClient as any, - licenseService as any, [ALERT_CPU_USAGE, ALERT_DISK_USAGE, ALERT_MISSING_MONITORING_DATA], [defaultClusterState.clusterUuid] ); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts index f4fd792ddf922..4a20dcb0fdf2d 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -14,11 +14,9 @@ import { CommonAlertFilter, } from '../../../common/types/alerts'; import { ALERTS } from '../../../common/constants'; -import { MonitoringLicenseService } from '../../types'; export async function fetchStatus( rulesClient: RulesClient, - licenseService: MonitoringLicenseService, alertTypes: string[] | undefined, clusterUuids: string[], filters: CommonAlertFilter[] = [] diff --git a/x-pack/plugins/monitoring/server/lib/apm/_apm_stats.js b/x-pack/plugins/monitoring/server/lib/apm/_apm_stats.ts similarity index 73% rename from x-pack/plugins/monitoring/server/lib/apm/_apm_stats.js rename to x-pack/plugins/monitoring/server/lib/apm/_apm_stats.ts index 64450405b3268..0068d521055ef 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/_apm_stats.js +++ b/x-pack/plugins/monitoring/server/lib/apm/_apm_stats.ts @@ -5,14 +5,14 @@ * 2.0. */ -import { get } from 'lodash'; +import type { ElasticsearchResponse } from '../../../common/types/es'; -const getMemPath = (cgroup) => +const getMemPath = (cgroup?: string) => cgroup ? 'beats_stats.metrics.beat.cgroup.memory.mem.usage.bytes' : 'beats_stats.metrics.beat.memstats.rss'; -export const getDiffCalculation = (max, min) => { +export const getDiffCalculation = (max: number | null, min: number | null) => { // no need to test max >= 0, but min <= 0 which is normal for a derivative after restart // because we are aggregating/collapsing on ephemeral_ids if (max !== null && min !== null && max >= 0 && min >= 0 && max >= min) { @@ -30,7 +30,7 @@ export const apmAggFilterPath = [ 'aggregations.max_mem_total.value', 'aggregations.versions.buckets', ]; -export const apmUuidsAgg = (maxBucketSize, cgroup) => ({ +export const apmUuidsAgg = (maxBucketSize?: string, cgroup?: string) => ({ total: { cardinality: { field: 'beats_stats.beat.uuid', @@ -92,14 +92,16 @@ export const apmUuidsAgg = (maxBucketSize, cgroup) => ({ }, }); -export const apmAggResponseHandler = (response) => { - const apmTotal = get(response, 'aggregations.total.value', 0); +export const apmAggResponseHandler = (response: ElasticsearchResponse) => { + const apmTotal = response.aggregations?.total.value ?? 0; - const eventsTotalMax = get(response, 'aggregations.max_events_total.value', 0); - const eventsTotalMin = get(response, 'aggregations.min_events_total.value', 0); - const memMax = get(response, 'aggregations.max_mem_total.value', 0); - const memMin = get(response, 'aggregations.min_mem_total.value', 0); - const versions = get(response, 'aggregations.versions.buckets', []).map(({ key }) => key); + const eventsTotalMax = response.aggregations?.max_events_total.value ?? 0; + const eventsTotalMin = response.aggregations?.min_events_total.value ?? 0; + const memMax = response.aggregations?.max_mem_total.value ?? 0; + const memMin = response.aggregations?.min_mem_total.value ?? 0; + const versions = (response.aggregations?.versions.buckets ?? []).map( + ({ key }: { key: string }) => key + ); return { apmTotal, diff --git a/x-pack/plugins/monitoring/server/lib/apm/_get_time_of_last_event.ts b/x-pack/plugins/monitoring/server/lib/apm/_get_time_of_last_event.ts index 398428f89a4ba..0ad1ff7370a9a 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/_get_time_of_last_event.ts +++ b/x-pack/plugins/monitoring/server/lib/apm/_get_time_of_last_event.ts @@ -5,9 +5,7 @@ * 2.0. */ -// @ts-ignore import { createApmQuery } from './create_apm_query'; -// @ts-ignore import { ApmClusterMetric } from '../metrics'; import { LegacyRequest } from '../../types'; import { ElasticsearchResponse } from '../../../common/types/es'; diff --git a/x-pack/plugins/monitoring/server/lib/apm/create_apm_query.js b/x-pack/plugins/monitoring/server/lib/apm/create_apm_query.ts similarity index 63% rename from x-pack/plugins/monitoring/server/lib/apm/create_apm_query.js rename to x-pack/plugins/monitoring/server/lib/apm/create_apm_query.ts index 1680fcdfdb228..63c56607a68e6 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/create_apm_query.js +++ b/x-pack/plugins/monitoring/server/lib/apm/create_apm_query.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { defaults } from 'lodash'; -import { ApmMetric } from '../metrics'; +import { ApmMetric, ApmMetricFields } from '../metrics'; import { createQuery } from '../create_query'; /** @@ -14,14 +13,23 @@ import { createQuery } from '../create_query'; * * @param {Object} options The options to pass to {@code createQuery} */ -export function createApmQuery(options = {}) { - options = defaults(options, { - filters: [], +export function createApmQuery(options: { + filters?: any[]; + types?: string[]; + metric?: ApmMetricFields; + uuid?: string; + clusterUuid: string; + start?: number; + end?: number; +}) { + const opts = { + filters: [] as any[], metric: ApmMetric.getMetricFields(), types: ['stats', 'beats_stats'], - }); + ...(options ?? {}), + }; - options.filters.push({ + opts.filters.push({ bool: { must: { term: { @@ -31,5 +39,5 @@ export function createApmQuery(options = {}) { }, }); - return createQuery(options); + return createQuery(opts); } diff --git a/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.ts b/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.ts index 3721bf873a417..5b2f8424a566d 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.ts +++ b/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.ts @@ -66,7 +66,7 @@ export function handleResponse( eventsTotal: getDiffCalculation(eventsTotalLast, eventsTotalFirst), eventsEmitted: getDiffCalculation(eventsEmittedLast, eventsEmittedFirst), eventsDropped: getDiffCalculation(eventsDroppedLast, eventsDroppedFirst), - bytesWritten: getDiffCalculation(bytesWrittenLast, bytesWrittenFirst), + bytesWritten: getDiffCalculation(Number(bytesWrittenLast), Number(bytesWrittenFirst)), config: { container: config.get('monitoring.ui.container.apm.enabled'), }, diff --git a/x-pack/plugins/monitoring/server/lib/apm/get_apms.ts b/x-pack/plugins/monitoring/server/lib/apm/get_apms.ts index be3bb6fdfd661..7089a0507107a 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/get_apms.ts +++ b/x-pack/plugins/monitoring/server/lib/apm/get_apms.ts @@ -49,9 +49,11 @@ export function handleResponse(response: ElasticsearchResponse, start: number, e // add the beat const rateOptions = { - hitTimestamp: stats?.timestamp ?? hit._source['@timestamp'], + hitTimestamp: stats?.timestamp ?? hit._source['@timestamp'] ?? null, earliestHitTimestamp: - earliestStats?.timestamp ?? hit.inner_hits?.earliest.hits?.hits[0]._source['@timestamp'], + earliestStats?.timestamp ?? + hit.inner_hits?.earliest.hits?.hits[0]._source['@timestamp'] ?? + null, timeWindowMin: start, timeWindowMax: end, }; diff --git a/x-pack/plugins/monitoring/server/lib/apm/get_apms_for_clusters.js b/x-pack/plugins/monitoring/server/lib/apm/get_apms_for_clusters.ts similarity index 85% rename from x-pack/plugins/monitoring/server/lib/apm/get_apms_for_clusters.js rename to x-pack/plugins/monitoring/server/lib/apm/get_apms_for_clusters.ts index 0c96e0e230585..e99ce0da1ef10 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/get_apms_for_clusters.js +++ b/x-pack/plugins/monitoring/server/lib/apm/get_apms_for_clusters.ts @@ -5,14 +5,15 @@ * 2.0. */ -import { get } from 'lodash'; +import { LegacyRequest, Cluster } from '../../types'; import { checkParam } from '../error_missing_required'; import { createApmQuery } from './create_apm_query'; import { ApmMetric } from '../metrics'; import { apmAggResponseHandler, apmUuidsAgg, apmAggFilterPath } from './_apm_stats'; import { getTimeOfLastEvent } from './_get_time_of_last_event'; +import { ElasticsearchResponse } from '../../../common/types/es'; -export function handleResponse(clusterUuid, response) { +export function handleResponse(clusterUuid: string, response: ElasticsearchResponse) { const { apmTotal, totalEvents, memRss, versions } = apmAggResponseHandler(response); // combine stats @@ -31,7 +32,11 @@ export function handleResponse(clusterUuid, response) { }; } -export function getApmsForClusters(req, apmIndexPattern, clusters) { +export function getApmsForClusters( + req: LegacyRequest, + apmIndexPattern: string, + clusters: Cluster[] +) { checkParam(apmIndexPattern, 'apmIndexPattern in apms/getApmsForClusters'); const start = req.payload.timeRange.min; @@ -42,7 +47,7 @@ export function getApmsForClusters(req, apmIndexPattern, clusters) { return Promise.all( clusters.map(async (cluster) => { - const clusterUuid = get(cluster, 'elasticsearch.cluster.id', cluster.cluster_uuid); + const clusterUuid = cluster.elasticsearch?.cluster?.id ?? cluster.cluster_uuid; const params = { index: apmIndexPattern, size: 0, diff --git a/x-pack/plugins/monitoring/server/lib/apm/get_stats.js b/x-pack/plugins/monitoring/server/lib/apm/get_stats.ts similarity index 80% rename from x-pack/plugins/monitoring/server/lib/apm/get_stats.js rename to x-pack/plugins/monitoring/server/lib/apm/get_stats.ts index 2abd81e325f5e..ea71229bac816 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/get_stats.js +++ b/x-pack/plugins/monitoring/server/lib/apm/get_stats.ts @@ -6,16 +6,17 @@ */ import moment from 'moment'; +import { LegacyRequest } from '../../types'; import { checkParam } from '../error_missing_required'; import { createApmQuery } from './create_apm_query'; import { apmAggFilterPath, apmUuidsAgg, apmAggResponseHandler } from './_apm_stats'; import { getTimeOfLastEvent } from './_get_time_of_last_event'; +import type { ElasticsearchResponse } from '../../../common/types/es'; -export function handleResponse(...args) { - const { apmTotal, totalEvents, bytesSent } = apmAggResponseHandler(...args); +export function handleResponse(response: ElasticsearchResponse) { + const { apmTotal, totalEvents } = apmAggResponseHandler(response); return { - bytesSent, totalEvents, apms: { total: apmTotal, @@ -23,7 +24,7 @@ export function handleResponse(...args) { }; } -export async function getStats(req, apmIndexPattern, clusterUuid) { +export async function getStats(req: LegacyRequest, apmIndexPattern: string, clusterUuid: string) { checkParam(apmIndexPattern, 'apmIndexPattern in getBeats'); const config = req.server.config(); @@ -60,7 +61,7 @@ export async function getStats(req, apmIndexPattern, clusterUuid) { }), ]); - const formattedResponse = handleResponse(response, start, end); + const formattedResponse = handleResponse(response); return { ...formattedResponse, timeOfLastEvent, diff --git a/x-pack/plugins/monitoring/server/lib/apm/index.js b/x-pack/plugins/monitoring/server/lib/apm/index.ts similarity index 100% rename from x-pack/plugins/monitoring/server/lib/apm/index.js rename to x-pack/plugins/monitoring/server/lib/apm/index.ts diff --git a/x-pack/plugins/monitoring/server/lib/beats/__fixtures__/get_listing_response.js b/x-pack/plugins/monitoring/server/lib/beats/__fixtures__/get_listing_response.ts similarity index 100% rename from x-pack/plugins/monitoring/server/lib/beats/__fixtures__/get_listing_response.js rename to x-pack/plugins/monitoring/server/lib/beats/__fixtures__/get_listing_response.ts diff --git a/x-pack/plugins/monitoring/server/lib/beats/_beats_stats.js b/x-pack/plugins/monitoring/server/lib/beats/_beats_stats.ts similarity index 74% rename from x-pack/plugins/monitoring/server/lib/beats/_beats_stats.js rename to x-pack/plugins/monitoring/server/lib/beats/_beats_stats.ts index 0d4dc0ba59d18..c2ddadcf7ebf4 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/_beats_stats.js +++ b/x-pack/plugins/monitoring/server/lib/beats/_beats_stats.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { upperFirst, get } from 'lodash'; +import { upperFirst } from 'lodash'; +import type { BeatsElasticsearchResponse, BucketCount } from './types'; -export const getDiffCalculation = (max, min) => { +export const getDiffCalculation = (max: number | null, min: number | null) => { // no need to test max >= 0, but min <= 0 which is normal for a derivative after restart // because we are aggregating/collapsing on ephemeral_ids if (max !== null && min !== null && max >= 0 && min >= 0 && max >= min) { @@ -27,7 +28,7 @@ export const beatsAggFilterPath = [ 'aggregations.max_bytes_sent_total.value', ]; -export const beatsUuidsAgg = (maxBucketSize) => ({ +export const beatsUuidsAgg = (maxBucketSize: string) => ({ types: { terms: { field: 'beats_stats.beat.type', @@ -98,24 +99,24 @@ export const beatsUuidsAgg = (maxBucketSize) => ({ }, }); -export const beatsAggResponseHandler = (response) => { +export const beatsAggResponseHandler = (response?: BeatsElasticsearchResponse) => { // beat types stat - const buckets = get(response, 'aggregations.types.buckets', []); - const beatTotal = get(response, 'aggregations.total.value', 0); - const beatTypes = buckets.reduce((types, typeBucket) => { + const buckets = response?.aggregations?.types?.buckets ?? []; + const beatTotal = response?.aggregations?.total.value ?? 0; + const beatTypes = buckets.reduce((types: BucketCount<{ type: string }>, typeBucket) => { return [ ...types, { type: upperFirst(typeBucket.key), - count: get(typeBucket, 'uuids.buckets.length'), + count: typeBucket.uuids.buckets.length, }, ]; }, []); - const eventsTotalMax = get(response, 'aggregations.max_events_total.value', 0); - const eventsTotalMin = get(response, 'aggregations.min_events_total.value', 0); - const bytesSentMax = get(response, 'aggregations.max_bytes_sent_total.value', 0); - const bytesSentMin = get(response, 'aggregations.min_bytes_sent_total.value', 0); + const eventsTotalMax = response?.aggregations?.max_events_total.value ?? 0; + const eventsTotalMin = response?.aggregations?.min_events_total.value ?? 0; + const bytesSentMax = response?.aggregations?.max_bytes_sent_total.value ?? 0; + const bytesSentMin = response?.aggregations?.min_bytes_sent_total.value ?? 0; return { beatTotal, diff --git a/x-pack/plugins/monitoring/server/lib/beats/create_beats_query.js b/x-pack/plugins/monitoring/server/lib/beats/create_beats_query.ts similarity index 71% rename from x-pack/plugins/monitoring/server/lib/beats/create_beats_query.js rename to x-pack/plugins/monitoring/server/lib/beats/create_beats_query.ts index c6ec39ed3ba2b..b013cd8234c40 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/create_beats_query.js +++ b/x-pack/plugins/monitoring/server/lib/beats/create_beats_query.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { defaults } from 'lodash'; -import { BeatsMetric } from '../metrics'; +import { BeatsMetric, BeatsMetricFields } from '../metrics'; import { createQuery } from '../create_query'; /** @@ -17,15 +16,24 @@ import { createQuery } from '../create_query'; * * @param {Object} options The options to pass to {@code createQuery} */ -export function createBeatsQuery(options = {}) { - options = defaults(options, { - filters: [], +export function createBeatsQuery(options: { + filters?: any[]; + types?: string[]; + metric?: BeatsMetricFields; + uuid?: string; + clusterUuid: string; + start?: number; + end?: number; +}) { + const opts = { + filters: [] as any[], metric: BeatsMetric.getMetricFields(), types: ['stats', 'beats_stats'], - }); + ...(options ?? {}), + }; // avoid showing APM Server stats alongside other Beats because APM Server will have its own UI - options.filters.push({ + opts.filters.push({ bool: { must_not: { term: { @@ -35,5 +43,5 @@ export function createBeatsQuery(options = {}) { }, }); - return createQuery(options); + return createQuery(opts); } diff --git a/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.ts b/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.ts index d67f32e64ba71..07169b54cb61b 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.ts +++ b/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.ts @@ -11,7 +11,7 @@ import { ElasticsearchResponse } from '../../../common/types/es'; // @ts-ignore import { checkParam } from '../error_missing_required'; // @ts-ignore -import { createBeatsQuery } from './create_beats_query.js'; +import { createBeatsQuery } from './create_beats_query'; // @ts-ignore import { getDiffCalculation } from './_beats_stats'; @@ -67,7 +67,7 @@ export function handleResponse(response: ElasticsearchResponse, beatUuid: string eventsTotal: getDiffCalculation(eventsTotalLast, eventsTotalFirst) ?? null, eventsEmitted: getDiffCalculation(eventsEmittedLast, eventsEmittedFirst) ?? null, eventsDropped: getDiffCalculation(eventsDroppedLast, eventsDroppedFirst) ?? null, - bytesWritten: getDiffCalculation(bytesWrittenLast, bytesWrittenFirst) ?? null, + bytesWritten: getDiffCalculation(Number(bytesWrittenLast), Number(bytesWrittenFirst)) ?? null, handlesHardLimit, handlesSoftLimit, }; diff --git a/x-pack/plugins/monitoring/server/lib/beats/get_beats.ts b/x-pack/plugins/monitoring/server/lib/beats/get_beats.ts index fff2b55cf2616..85a4bb61f5573 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/get_beats.ts +++ b/x-pack/plugins/monitoring/server/lib/beats/get_beats.ts @@ -19,14 +19,14 @@ import { LegacyRequest } from '../../types'; import { ElasticsearchResponse } from '../../../common/types/es'; interface Beat { - uuid: string | undefined; - name: string | undefined; - type: string | undefined; - output: string | undefined; + uuid?: string; + name?: string; + type?: string; + output?: string; total_events_rate: number; bytes_sent_rate: number; - memory: number | undefined; - version: string | undefined; + memory?: number; + version?: string; errors: any; } @@ -63,9 +63,9 @@ export function handleResponse(response: ElasticsearchResponse, start: number, e // add the beat const rateOptions = { - hitTimestamp: stats?.timestamp ?? hit._source['@timestamp'], + hitTimestamp: stats?.timestamp ?? hit._source['@timestamp']!, earliestHitTimestamp: - earliestStats?.timestamp ?? hit.inner_hits?.earliest.hits?.hits[0]._source['@timestamp'], + earliestStats?.timestamp ?? hit.inner_hits?.earliest.hits?.hits[0]._source['@timestamp']!, timeWindowMin: start, timeWindowMax: end, }; @@ -96,8 +96,8 @@ export function handleResponse(response: ElasticsearchResponse, start: number, e name: stats?.beat?.name, type: upperFirst(stats?.beat?.type), output: upperFirst(statsMetrics?.libbeat?.output?.type), - total_events_rate: totalEventsRate, - bytes_sent_rate: bytesSentRate, + total_events_rate: totalEventsRate!, + bytes_sent_rate: bytesSentRate!, errors, memory: hit._source.beats_stats?.metrics?.beat?.memstats?.memory_alloc ?? diff --git a/x-pack/plugins/monitoring/server/lib/beats/get_beats_for_clusters.js b/x-pack/plugins/monitoring/server/lib/beats/get_beats_for_clusters.ts similarity index 79% rename from x-pack/plugins/monitoring/server/lib/beats/get_beats_for_clusters.js rename to x-pack/plugins/monitoring/server/lib/beats/get_beats_for_clusters.ts index e7c4771dd601c..3a0720f7ca195 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/get_beats_for_clusters.js +++ b/x-pack/plugins/monitoring/server/lib/beats/get_beats_for_clusters.ts @@ -5,13 +5,14 @@ * 2.0. */ -import { get } from 'lodash'; import { checkParam } from '../error_missing_required'; import { BeatsClusterMetric } from '../metrics'; import { createBeatsQuery } from './create_beats_query'; import { beatsAggFilterPath, beatsUuidsAgg, beatsAggResponseHandler } from './_beats_stats'; +import type { ElasticsearchResponse } from '../../../common/types/es'; +import { LegacyRequest, Cluster } from '../../types'; -export function handleResponse(clusterUuid, response) { +export function handleResponse(clusterUuid: string, response: ElasticsearchResponse) { const { beatTotal, beatTypes, totalEvents, bytesSent } = beatsAggResponseHandler(response); // combine stats @@ -30,7 +31,11 @@ export function handleResponse(clusterUuid, response) { }; } -export function getBeatsForClusters(req, beatsIndexPattern, clusters) { +export function getBeatsForClusters( + req: LegacyRequest, + beatsIndexPattern: string, + clusters: Cluster[] +) { checkParam(beatsIndexPattern, 'beatsIndexPattern in beats/getBeatsForClusters'); const start = req.payload.timeRange.min; @@ -40,7 +45,7 @@ export function getBeatsForClusters(req, beatsIndexPattern, clusters) { return Promise.all( clusters.map(async (cluster) => { - const clusterUuid = get(cluster, 'elasticsearch.cluster.id', cluster.cluster_uuid); + const clusterUuid = cluster.elasticsearch?.cluster?.id ?? cluster.cluster_uuid; const params = { index: beatsIndexPattern, size: 0, @@ -53,7 +58,7 @@ export function getBeatsForClusters(req, beatsIndexPattern, clusters) { clusterUuid, metric: BeatsClusterMetric.getMetricFields(), // override default of BeatMetric.getMetricFields }), - aggs: beatsUuidsAgg(maxBucketSize), + aggs: beatsUuidsAgg(maxBucketSize!), }, }; diff --git a/x-pack/plugins/monitoring/server/lib/beats/get_latest_stats.js b/x-pack/plugins/monitoring/server/lib/beats/get_latest_stats.ts similarity index 72% rename from x-pack/plugins/monitoring/server/lib/beats/get_latest_stats.js rename to x-pack/plugins/monitoring/server/lib/beats/get_latest_stats.ts index fb40df115d19a..684b0d6301e1f 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/get_latest_stats.js +++ b/x-pack/plugins/monitoring/server/lib/beats/get_latest_stats.ts @@ -5,17 +5,19 @@ * 2.0. */ -import { upperFirst, get } from 'lodash'; +import { upperFirst } from 'lodash'; +import { LegacyRequest } from '../../types'; import { checkParam } from '../error_missing_required'; import { createBeatsQuery } from './create_beats_query'; +import type { BeatsElasticsearchResponse, BucketCount } from './types'; -export function handleResponse(response) { - const aggs = get(response, 'aggregations'); +export function handleResponse(response?: BeatsElasticsearchResponse) { + const aggs = response?.aggregations; - const getTimeRangeCount = (name) => { - const lastActiveBuckets = get(aggs, 'active_counts.buckets', []); + const getTimeRangeCount = (name: string) => { + const lastActiveBuckets = aggs?.active_counts?.buckets ?? []; const rangeBucket = lastActiveBuckets.find((bucket) => bucket.key === name); - return get(rangeBucket, 'uuids.buckets.length'); + return rangeBucket?.uuids.buckets.length; }; // aggregations are not ordered, so we find the bucket for each timestamp range @@ -34,25 +36,31 @@ export function handleResponse(response) { { range: 'last1d', count: last1dCount }, ]; - const latestVersions = get(aggs, 'versions.buckets', []).reduce((accum, current) => { - return [ - ...accum, - { - version: current.key, - count: get(current, 'uuids.buckets.length'), - }, - ]; - }, []); + const latestVersions = (aggs?.versions?.buckets ?? []).reduce( + (accum: BucketCount<{ version: string }>, current) => { + return [ + ...accum, + { + version: current.key, + count: current.uuids.buckets.length, + }, + ]; + }, + [] + ); - const latestTypes = get(aggs, 'types.buckets', []).reduce((accum, current) => { - return [ - ...accum, - { - type: upperFirst(current.key), - count: get(current, 'uuids.buckets.length'), - }, - ]; - }, []); + const latestTypes = (aggs?.types?.buckets ?? []).reduce( + (accum: BucketCount<{ type: string }>, current) => { + return [ + ...accum, + { + type: upperFirst(current.key), + count: current.uuids.buckets.length, + }, + ]; + }, + [] + ); return { latestActive, @@ -61,7 +69,7 @@ export function handleResponse(response) { }; } -export function getLatestStats(req, beatsIndexPattern, clusterUuid) { +export function getLatestStats(req: LegacyRequest, beatsIndexPattern: string, clusterUuid: string) { checkParam(beatsIndexPattern, 'beatsIndexPattern in getBeats'); const config = req.server.config(); diff --git a/x-pack/plugins/monitoring/server/lib/beats/get_stats.js b/x-pack/plugins/monitoring/server/lib/beats/get_stats.ts similarity index 73% rename from x-pack/plugins/monitoring/server/lib/beats/get_stats.js rename to x-pack/plugins/monitoring/server/lib/beats/get_stats.ts index 3af51d909697f..be3f3d88e0709 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/get_stats.js +++ b/x-pack/plugins/monitoring/server/lib/beats/get_stats.ts @@ -6,12 +6,14 @@ */ import moment from 'moment'; +import type { BeatsElasticsearchResponse } from './types'; +import { LegacyRequest } from '../../types'; import { checkParam } from '../error_missing_required'; import { createBeatsQuery } from './create_beats_query'; import { beatsAggFilterPath, beatsUuidsAgg, beatsAggResponseHandler } from './_beats_stats'; -export function handleResponse(...args) { - const { beatTotal, beatTypes, totalEvents, bytesSent } = beatsAggResponseHandler(...args); +export function handleResponse(response: BeatsElasticsearchResponse) { + const { beatTotal, beatTypes, totalEvents, bytesSent } = beatsAggResponseHandler(response); return { total: beatTotal, @@ -23,7 +25,7 @@ export function handleResponse(...args) { }; } -export async function getStats(req, beatsIndexPattern, clusterUuid) { +export async function getStats(req: LegacyRequest, beatsIndexPattern: string, clusterUuid: string) { checkParam(beatsIndexPattern, 'beatsIndexPattern in getBeats'); const config = req.server.config(); @@ -42,12 +44,12 @@ export async function getStats(req, beatsIndexPattern, clusterUuid) { end, clusterUuid, }), - aggs: beatsUuidsAgg(maxBucketSize), + aggs: beatsUuidsAgg(maxBucketSize!), }, }; const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); - const response = await callWithRequest(req, 'search', params); + const response: BeatsElasticsearchResponse = await callWithRequest(req, 'search', params); - return handleResponse(response, start, end); + return handleResponse(response); } diff --git a/x-pack/plugins/monitoring/server/lib/beats/index.js b/x-pack/plugins/monitoring/server/lib/beats/index.ts similarity index 100% rename from x-pack/plugins/monitoring/server/lib/beats/index.js rename to x-pack/plugins/monitoring/server/lib/beats/index.ts diff --git a/x-pack/plugins/monitoring/server/lib/beats/types.ts b/x-pack/plugins/monitoring/server/lib/beats/types.ts new file mode 100644 index 0000000000000..516a4ba587524 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/beats/types.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ElasticsearchResponse } from '../../../common/types/es'; +import type { Aggregation } from '../../types'; + +export type BucketCount = Array< + T & { + count: number; + } +>; + +export interface BeatsElasticsearchResponse extends ElasticsearchResponse { + aggregations?: { + types?: Aggregation; + active_counts?: Aggregation; + versions?: Aggregation; + total: { + value: number; + }; + max_events_total: { + value: number; + }; + min_events_total: { + value: number; + }; + max_bytes_sent_total: { + value: number; + }; + min_bytes_sent_total: { + value: number; + }; + }; +} diff --git a/x-pack/plugins/monitoring/server/lib/calculate_auto.test.js b/x-pack/plugins/monitoring/server/lib/calculate_auto.test.js index 93a1d1a40cedc..934781f7786a9 100644 --- a/x-pack/plugins/monitoring/server/lib/calculate_auto.test.js +++ b/x-pack/plugins/monitoring/server/lib/calculate_auto.test.js @@ -5,7 +5,7 @@ * 2.0. */ -import { calculateAuto } from './calculate_auto.js'; +import { calculateAuto } from './calculate_auto'; import _ from 'lodash'; import moment from 'moment'; diff --git a/x-pack/plugins/monitoring/server/lib/calculate_auto.js b/x-pack/plugins/monitoring/server/lib/calculate_auto.ts similarity index 75% rename from x-pack/plugins/monitoring/server/lib/calculate_auto.js rename to x-pack/plugins/monitoring/server/lib/calculate_auto.ts index a6ffda39c79f8..817f49d1a5868 100644 --- a/x-pack/plugins/monitoring/server/lib/calculate_auto.js +++ b/x-pack/plugins/monitoring/server/lib/calculate_auto.ts @@ -5,7 +5,9 @@ * 2.0. */ -import moment from 'moment'; +import moment, { Duration } from 'moment'; + +type RoundingRule = [number | Duration, Duration]; const d = moment.duration; const roundingRules = [ @@ -25,18 +27,22 @@ const roundingRules = [ [d(3, 'week'), d(1, 'week')], [d(1, 'year'), d(1, 'month')], [Infinity, d(1, 'year')], -]; +] as RoundingRule[]; -function find(rules, check) { - function pick(buckets, duration) { - const target = duration / buckets; +function find( + rules: RoundingRule[], + check: (b: number | Duration, i: Duration, t: number) => Duration | void +) { + function pick(buckets?: number, duration?: Duration): Duration { + if (!buckets || !duration) return moment.duration(0); + const target = duration.asMilliseconds() / buckets; let lastResp; for (let i = 0; i < rules.length; i++) { const rule = rules[i]; const resp = check(rule[0], rule[1], target); - if (resp == null) { + if (resp === null || resp === undefined) { if (lastResp) { return lastResp; } @@ -51,11 +57,9 @@ function find(rules, check) { return moment.duration(ms, 'ms'); } - return function (buckets, duration) { + return function (buckets: number, duration: Duration) { const interval = pick(buckets, duration); - if (interval) { - return moment.duration(interval._data); - } + return interval; }; } diff --git a/x-pack/plugins/monitoring/server/lib/calculate_availability.js b/x-pack/plugins/monitoring/server/lib/calculate_availability.ts similarity index 90% rename from x-pack/plugins/monitoring/server/lib/calculate_availability.js rename to x-pack/plugins/monitoring/server/lib/calculate_availability.ts index 81b8273990793..96a89ee62664b 100644 --- a/x-pack/plugins/monitoring/server/lib/calculate_availability.js +++ b/x-pack/plugins/monitoring/server/lib/calculate_availability.ts @@ -10,7 +10,7 @@ import moment from 'moment'; * Return `true` if timestamp of last update is younger than 10 minutes ago * If older than, it indicates cluster/instance is offline */ -export function calculateAvailability(timestamp) { +export function calculateAvailability(timestamp: string) { const lastUpdate = moment(timestamp); // converts to local time return lastUpdate.isAfter(moment().subtract(10, 'minutes')); // compares with local time } diff --git a/x-pack/plugins/monitoring/server/lib/calculate_overall_status.js b/x-pack/plugins/monitoring/server/lib/calculate_overall_status.ts similarity index 89% rename from x-pack/plugins/monitoring/server/lib/calculate_overall_status.js rename to x-pack/plugins/monitoring/server/lib/calculate_overall_status.ts index 7a0edbdb49e62..78cbb9935045f 100644 --- a/x-pack/plugins/monitoring/server/lib/calculate_overall_status.js +++ b/x-pack/plugins/monitoring/server/lib/calculate_overall_status.ts @@ -9,7 +9,7 @@ * A reduce that takes statuses from different products in a cluster and boil * it down into a single status */ -export function calculateOverallStatus(set) { +export function calculateOverallStatus(set: Array) { return set.reduce((result, current) => { if (!current) { return result; diff --git a/x-pack/plugins/monitoring/server/lib/calculate_rate.js b/x-pack/plugins/monitoring/server/lib/calculate_rate.ts similarity index 81% rename from x-pack/plugins/monitoring/server/lib/calculate_rate.js rename to x-pack/plugins/monitoring/server/lib/calculate_rate.ts index 04d43ca935b9d..3e9bdbb5e20f4 100644 --- a/x-pack/plugins/monitoring/server/lib/calculate_rate.js +++ b/x-pack/plugins/monitoring/server/lib/calculate_rate.ts @@ -20,6 +20,16 @@ import moment from 'moment'; * 4. From that subtract the earliest timestamp from the time picker * This gives you the denominator in millis. Divide it by 1000 to convert to seconds */ + +interface CalculateRateProps { + hitTimestamp: string | null; + earliestHitTimestamp: string | null; + latestTotal?: string | number | null; + earliestTotal?: string | number | null; + timeWindowMin: number; + timeWindowMax: number; +} + export function calculateRate({ hitTimestamp = null, earliestHitTimestamp = null, @@ -27,7 +37,7 @@ export function calculateRate({ earliestTotal = null, timeWindowMin, timeWindowMax, -} = {}) { +}: CalculateRateProps) { const nullResult = { rate: null, isEstimate: false, @@ -58,9 +68,9 @@ export function calculateRate({ let rate = null; let isEstimate = false; if (millisDelta !== 0) { - const totalDelta = latestTotal - earliestTotal; + const totalDelta = Number(latestTotal) - Number(earliestTotal); if (totalDelta < 0) { - rate = latestTotal / (millisDelta / 1000); // a restart caused an unwanted negative rate + rate = Number(latestTotal) / (millisDelta / 1000); // a restart caused an unwanted negative rate isEstimate = true; } else { rate = totalDelta / (millisDelta / 1000); diff --git a/x-pack/plugins/monitoring/server/lib/calculate_timeseries_interval.js b/x-pack/plugins/monitoring/server/lib/calculate_timeseries_interval.ts similarity index 67% rename from x-pack/plugins/monitoring/server/lib/calculate_timeseries_interval.js rename to x-pack/plugins/monitoring/server/lib/calculate_timeseries_interval.ts index c304e75accd34..b4ac193283849 100644 --- a/x-pack/plugins/monitoring/server/lib/calculate_timeseries_interval.js +++ b/x-pack/plugins/monitoring/server/lib/calculate_timeseries_interval.ts @@ -9,11 +9,14 @@ import moment from 'moment'; import { calculateAuto } from './calculate_auto'; export function calculateTimeseriesInterval( - lowerBoundInMsSinceEpoch, - upperBoundInMsSinceEpoch, - minIntervalSeconds + lowerBoundInMsSinceEpoch: number, + upperBoundInMsSinceEpoch: number, + minIntervalSeconds: number ) { const duration = moment.duration(upperBoundInMsSinceEpoch - lowerBoundInMsSinceEpoch, 'ms'); - return Math.max(minIntervalSeconds, calculateAuto(100, duration).asSeconds()); + return Math.max( + !isNaN(minIntervalSeconds) ? minIntervalSeconds : 0, + calculateAuto(100, duration).asSeconds() + ); } diff --git a/x-pack/plugins/monitoring/server/lib/ccs_utils.js b/x-pack/plugins/monitoring/server/lib/ccs_utils.ts similarity index 88% rename from x-pack/plugins/monitoring/server/lib/ccs_utils.js rename to x-pack/plugins/monitoring/server/lib/ccs_utils.ts index 08ed8d5a7b35a..1d899456913b9 100644 --- a/x-pack/plugins/monitoring/server/lib/ccs_utils.js +++ b/x-pack/plugins/monitoring/server/lib/ccs_utils.ts @@ -6,8 +6,18 @@ */ import { isFunction, get } from 'lodash'; +import type { MonitoringConfig } from '../config'; -export function appendMetricbeatIndex(config, indexPattern, ccs, bypass = false) { +type Config = Partial & { + get?: (key: string) => any; +}; + +export function appendMetricbeatIndex( + config: Config, + indexPattern: string, + ccs?: string, + bypass: boolean = false +) { if (bypass) { return indexPattern; } @@ -39,7 +49,12 @@ export function appendMetricbeatIndex(config, indexPattern, ccs, bypass = false) * @param {String} ccs The optional cluster-prefix to prepend. * @return {String} The index pattern with the {@code cluster} prefix appropriately prepended. */ -export function prefixIndexPattern(config, indexPattern, ccs, monitoringIndicesOnly = false) { +export function prefixIndexPattern( + config: Config, + indexPattern: string, + ccs?: string, + monitoringIndicesOnly: boolean = false +) { let ccsEnabled = false; // TODO: NP // This function is called with both NP config and LP config @@ -102,7 +117,7 @@ export function prefixIndexPattern(config, indexPattern, ccs, monitoringIndicesO * @param {String} indexName The index's name, possibly including the cross-cluster prefix * @return {String} {@code null} if none. Otherwise the cluster prefix. */ -export function parseCrossClusterPrefix(indexName) { +export function parseCrossClusterPrefix(indexName: string) { const colonIndex = indexName.indexOf(':'); if (colonIndex === -1) { diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_stats.js b/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_stats.ts similarity index 91% rename from x-pack/plugins/monitoring/server/lib/cluster/get_cluster_stats.js rename to x-pack/plugins/monitoring/server/lib/cluster/get_cluster_stats.ts index 4dece136158ac..c671765a44548 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_stats.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_cluster_stats.ts @@ -6,8 +6,9 @@ */ import { badRequest, notFound } from '@hapi/boom'; -import { getClustersStats } from './get_clusters_stats'; import { i18n } from '@kbn/i18n'; +import { LegacyRequest } from '../../types'; +import { getClustersStats } from './get_clusters_stats'; /** * This will fetch the cluster stats and cluster state as a single object for the cluster specified by the {@code req}. @@ -17,7 +18,7 @@ import { i18n } from '@kbn/i18n'; * @param {String} clusterUuid The requested cluster's UUID * @return {Promise} The object cluster response. */ -export function getClusterStats(req, esIndexPattern, clusterUuid) { +export function getClusterStats(req: LegacyRequest, esIndexPattern: string, clusterUuid: string) { if (!clusterUuid) { throw badRequest( i18n.translate('xpack.monitoring.clusterStats.uuidNotSpecifiedErrorMessage', { diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.ts similarity index 90% rename from x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js rename to x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.ts index 9ef309cee7312..ab421312244f7 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.ts @@ -6,8 +6,9 @@ */ import { notFound } from '@hapi/boom'; -import { set } from '@elastic/safer-lodash-set'; import { get } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { i18n } from '@kbn/i18n'; import { getClustersStats } from './get_clusters_stats'; import { flagSupportedClusters } from './flag_supported_clusters'; import { getMlJobsForCluster } from '../elasticsearch'; @@ -15,7 +16,7 @@ import { getKibanasForClusters } from '../kibana'; import { getLogstashForClusters } from '../logstash'; import { getLogstashPipelineIds } from '../logstash/get_pipeline_ids'; import { getBeatsForClusters } from '../beats'; -import { getClustersSummary } from './get_clusters_summary'; +import { getClustersSummary, EnhancedClusters } from './get_clusters_summary'; import { STANDALONE_CLUSTER_CLUSTER_UUID, CODE_PATH_ML, @@ -26,21 +27,27 @@ import { CODE_PATH_BEATS, CODE_PATH_APM, } from '../../../common/constants'; + import { getApmsForClusters } from '../apm/get_apms_for_clusters'; -import { i18n } from '@kbn/i18n'; import { checkCcrEnabled } from '../elasticsearch/ccr'; import { fetchStatus } from '../alerts/fetch_status'; import { getStandaloneClusterDefinition, hasStandaloneClusters } from '../standalone_clusters'; import { getLogTypes } from '../logs'; import { isInCodePath } from './is_in_code_path'; +import { LegacyRequest, Cluster } from '../../types'; /** * Get all clusters or the cluster associated with {@code clusterUuid} when it is defined. */ export async function getClustersFromRequest( - req, - indexPatterns, - { clusterUuid, start, end, codePaths } = {} + req: LegacyRequest, + indexPatterns: { [x: string]: string }, + { + clusterUuid, + start, + end, + codePaths, + }: { clusterUuid: string; start: number; end: number; codePaths: string[] } ) { const { esIndexPattern, @@ -54,7 +61,7 @@ export async function getClustersFromRequest( const config = req.server.config(); const isStandaloneCluster = clusterUuid === STANDALONE_CLUSTER_CLUSTER_UUID; - let clusters = []; + let clusters: Cluster[] = []; if (isStandaloneCluster) { clusters.push(getStandaloneClusterDefinition()); @@ -120,7 +127,6 @@ export async function getClustersFromRequest( const rulesClient = req.getRulesClient(); const alertStatus = await fetchStatus( rulesClient, - req.server.plugins.monitoring.info, undefined, clusters.map((cluster) => get(cluster, 'elasticsearch.cluster.id', cluster.cluster_uuid)) ); @@ -139,16 +145,16 @@ export async function getClustersFromRequest( list: Object.keys(alertStatus).reduce((accum, alertName) => { const value = alertStatus[alertName]; if (value.states && value.states.length) { - accum[alertName] = { + Reflect.set(accum, alertName, { ...value, states: value.states.filter( (state) => state.state.cluster.clusterUuid === get(cluster, 'elasticsearch.cluster.id', cluster.cluster_uuid) ), - }; + }); } else { - accum[alertName] = value; + Reflect.set(accum, alertName, value); } return accum; }, {}), @@ -197,7 +203,7 @@ export async function getClustersFromRequest( ); // withhold LS overview stats until there is at least 1 pipeline if (logstash.clusterUuid === clusterUuid && !pipelines.length) { - logstash.stats = {}; + Reflect.set(logstash, 'stats', {}); } set(clusters[clusterIndex], 'logstash', logstash.stats); }); @@ -225,18 +231,18 @@ export async function getClustersFromRequest( get(cluster, 'elasticsearch.cluster.id', cluster.cluster_uuid) === apm.clusterUuid ); if (clusterIndex >= 0) { - const { stats, config } = apm; - clusters[clusterIndex].apm = { + const { stats, config: apmConfig } = apm; + Reflect.set(clusters[clusterIndex], 'apm', { ...stats, - config, - }; + config: apmConfig, + }); } }); // check ccr configuration const isCcrEnabled = await checkCcrEnabled(req, esIndexPattern); - const kibanaUuid = config.get('server.uuid'); + const kibanaUuid = config.get('server.uuid')!; - return getClustersSummary(req.server, clusters, kibanaUuid, isCcrEnabled); + return getClustersSummary(req.server, clusters as EnhancedClusters[], kibanaUuid, isCcrEnabled); } diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_summary.ts b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_summary.ts index 955c4d3d3b625..e29c1ad5292f9 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_summary.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_summary.ts @@ -16,7 +16,7 @@ import { calculateOverallStatus } from '../calculate_overall_status'; // @ts-ignore import { MonitoringLicenseError } from '../errors/custom_errors'; -type EnhancedClusters = ElasticsearchModifiedSource & { +export type EnhancedClusters = ElasticsearchModifiedSource & { license: ElasticsearchLegacySource['license']; [key: string]: any; }; diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.js b/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts similarity index 88% rename from x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.js rename to x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts index 52f2bb7b19736..d908d6180772e 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { LegacyServer } from '../../types'; import { prefixIndexPattern } from '../ccs_utils'; import { INDEX_PATTERN_ELASTICSEARCH, @@ -14,7 +15,11 @@ import { INDEX_ALERTS, } from '../../../common/constants'; -export function getIndexPatterns(server, additionalPatterns = {}, ccs = '*') { +export function getIndexPatterns( + server: LegacyServer, + additionalPatterns: Record = {}, + ccs: string = '*' +) { const config = server.config(); const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs); const kbnIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_KIBANA, ccs); diff --git a/x-pack/plugins/monitoring/server/lib/cluster/is_in_code_path.js b/x-pack/plugins/monitoring/server/lib/cluster/is_in_code_path.ts similarity index 86% rename from x-pack/plugins/monitoring/server/lib/cluster/is_in_code_path.js rename to x-pack/plugins/monitoring/server/lib/cluster/is_in_code_path.ts index 2b59fb3c460ea..2f795e5997f05 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/is_in_code_path.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/is_in_code_path.ts @@ -7,7 +7,7 @@ import { CODE_PATH_ALL } from '../../../common/constants'; -export function isInCodePath(codePaths, codePathsToTest) { +export function isInCodePath(codePaths: string[], codePathsToTest: string[]) { if (codePaths.includes(CODE_PATH_ALL)) { return true; } diff --git a/x-pack/plugins/monitoring/server/lib/create_query.test.js b/x-pack/plugins/monitoring/server/lib/create_query.test.js index a1f25d8937b49..60fa6faa79e44 100644 --- a/x-pack/plugins/monitoring/server/lib/create_query.test.js +++ b/x-pack/plugins/monitoring/server/lib/create_query.test.js @@ -8,7 +8,7 @@ import { set } from '@elastic/safer-lodash-set'; import { MissingRequiredError } from './error_missing_required'; import { ElasticsearchMetric } from './metrics'; -import { createQuery } from './create_query.js'; +import { createQuery } from './create_query'; let metric; diff --git a/x-pack/plugins/monitoring/server/lib/create_query.js b/x-pack/plugins/monitoring/server/lib/create_query.ts similarity index 72% rename from x-pack/plugins/monitoring/server/lib/create_query.js rename to x-pack/plugins/monitoring/server/lib/create_query.ts index c9a5ffeca3cee..83817280730f2 100644 --- a/x-pack/plugins/monitoring/server/lib/create_query.js +++ b/x-pack/plugins/monitoring/server/lib/create_query.ts @@ -5,23 +5,37 @@ * 2.0. */ -import { defaults, get } from 'lodash'; -import { MissingRequiredError } from './error_missing_required'; +import { defaults } from 'lodash'; import moment from 'moment'; +import { MissingRequiredError } from './error_missing_required'; import { standaloneClusterFilter } from './standalone_clusters'; import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../common/constants'; -export function createTimeFilter(options) { +export interface TimerangeFilter { + range: { + [x: string]: { + format: 'epoch_millis'; + gte?: number; + lte?: number; + }; + }; +} + +export function createTimeFilter(options: { + start?: number; + end?: number; + metric?: { timestampField: string }; +}) { const { start, end } = options; if (!start && !end) { return null; } - const timestampField = get(options, 'metric.timestampField'); + const timestampField = options.metric?.timestampField; if (!timestampField) { throw new MissingRequiredError('metric.timestampField'); } - const timeRangeFilter = { + const timeRangeFilter: TimerangeFilter = { range: { [timestampField]: { format: 'epoch_millis', @@ -50,9 +64,17 @@ export function createTimeFilter(options) { * @param {Date} options.end - numeric timestamp (optional) * @param {Metric} options.metric - Metric instance or metric fields object @see ElasticsearchMetric.getMetricFields */ -export function createQuery(options) { - options = defaults(options, { filters: [] }); - const { type, types, clusterUuid, uuid, filters } = options; +export function createQuery(options: { + type?: string; + types?: string[]; + filters?: any[]; + clusterUuid: string; + uuid?: string; + start?: number; + end?: number; + metric?: { uuidField?: string; timestampField: string }; +}) { + const { type, types, clusterUuid, uuid, filters } = defaults(options, { filters: [] }); const isFromStandaloneCluster = clusterUuid === STANDALONE_CLUSTER_CLUSTER_UUID; @@ -63,8 +85,8 @@ export function createQuery(options) { typeFilter = { bool: { should: [ - ...types.map((type) => ({ term: { type } })), - ...types.map((type) => ({ term: { 'metricset.name': type } })), + ...types.map((t) => ({ term: { type: t } })), + ...types.map((t) => ({ term: { 'metricset.name': t } })), ], }, }; @@ -78,23 +100,26 @@ export function createQuery(options) { let uuidFilter; // options.uuid can be null, for example getting all the clusters if (uuid) { - const uuidField = get(options, 'metric.uuidField'); + const uuidField = options.metric?.uuidField; if (!uuidField) { throw new MissingRequiredError('options.uuid given but options.metric.uuidField is false'); } uuidFilter = { term: { [uuidField]: uuid } }; } - const timestampField = get(options, 'metric.timestampField'); + const timestampField = options.metric?.timestampField; if (!timestampField) { throw new MissingRequiredError('metric.timestampField'); } const timeRangeFilter = createTimeFilter(options); - const combinedFilters = [typeFilter, clusterUuidFilter, uuidFilter, ...filters]; - if (timeRangeFilter) { - combinedFilters.push(timeRangeFilter); - } + const combinedFilters = [ + typeFilter, + clusterUuidFilter, + uuidFilter, + timeRangeFilter ?? undefined, + ...filters, + ]; if (isFromStandaloneCluster) { combinedFilters.push(standaloneClusterFilter); diff --git a/x-pack/plugins/monitoring/server/lib/details/get_metrics.js b/x-pack/plugins/monitoring/server/lib/details/get_metrics.ts similarity index 75% rename from x-pack/plugins/monitoring/server/lib/details/get_metrics.js rename to x-pack/plugins/monitoring/server/lib/details/get_metrics.ts index da0c2e0b3fcdb..83bb18169ae1e 100644 --- a/x-pack/plugins/monitoring/server/lib/details/get_metrics.js +++ b/x-pack/plugins/monitoring/server/lib/details/get_metrics.ts @@ -6,21 +6,23 @@ */ import moment from 'moment'; -import { isPlainObject } from 'lodash'; import Bluebird from 'bluebird'; import { checkParam } from '../error_missing_required'; import { getSeries } from './get_series'; import { calculateTimeseriesInterval } from '../calculate_timeseries_interval'; import { getTimezone } from '../get_timezone'; +import { LegacyRequest } from '../../types'; + +type Metric = string | { keys: string | string[]; name: string }; export async function getMetrics( - req, - indexPattern, - metricSet = [], - filters = [], + req: LegacyRequest, + indexPattern: string, + metricSet: Metric[] = [], + filters: Array> = [], metricOptions = {}, - numOfBuckets = 0, - groupBy = null + numOfBuckets: number = 0, + groupBy: string | Record | null = null ) { checkParam(indexPattern, 'indexPattern in details/getMetrics'); checkParam(metricSet, 'metricSet in details/getMetrics'); @@ -29,7 +31,7 @@ export async function getMetrics( // TODO: Pass in req parameters as explicit function parameters let min = moment.utc(req.payload.timeRange.min).valueOf(); const max = moment.utc(req.payload.timeRange.max).valueOf(); - const minIntervalSeconds = config.get('monitoring.ui.min_interval_seconds'); + const minIntervalSeconds = Number(config.get('monitoring.ui.min_interval_seconds')); const bucketSize = calculateTimeseriesInterval(min, max, minIntervalSeconds); const timezone = await getTimezone(req); @@ -38,11 +40,11 @@ export async function getMetrics( min = max - numOfBuckets * bucketSize * 1000; } - return Bluebird.map(metricSet, (metric) => { + return Bluebird.map(metricSet, (metric: Metric) => { // metric names match the literal metric name, but they can be supplied in groups or individually let metricNames; - if (isPlainObject(metric)) { + if (typeof metric !== 'string') { metricNames = metric.keys; } else { metricNames = [metric]; @@ -57,10 +59,10 @@ export async function getMetrics( }); }); }).then((rows) => { - const data = {}; + const data: Record = {}; metricSet.forEach((key, index) => { // keyName must match the value stored in the html template - const keyName = isPlainObject(key) ? key.name : key; + const keyName = typeof key === 'string' ? key : key.name; data[keyName] = rows[index]; }); diff --git a/x-pack/plugins/monitoring/server/lib/details/get_series.js b/x-pack/plugins/monitoring/server/lib/details/get_series.ts similarity index 74% rename from x-pack/plugins/monitoring/server/lib/details/get_series.js rename to x-pack/plugins/monitoring/server/lib/details/get_series.ts index d06ff950449dc..906c2df29fee0 100644 --- a/x-pack/plugins/monitoring/server/lib/details/get_series.js +++ b/x-pack/plugins/monitoring/server/lib/details/get_series.ts @@ -7,13 +7,36 @@ import { get } from 'lodash'; import moment from 'moment'; +import { ElasticsearchResponse } from '../../../common/types/es'; +import { LegacyRequest, Bucket } from '../../types'; import { checkParam } from '../error_missing_required'; import { metrics } from '../metrics'; -import { createQuery } from '../create_query.js'; +import { createQuery } from '../create_query'; import { formatTimestampToDuration } from '../../../common'; import { NORMALIZED_DERIVATIVE_UNIT, CALCULATE_DURATION_UNTIL } from '../../../common/constants'; import { formatUTCTimestampForTimezone } from '../format_timezone'; +type SeriesBucket = Bucket & { metric_mb_deriv?: { normalized_value: number } }; + +interface Metric { + app: string; + derivative: boolean; + mbField?: string; + aggs: any; + getDateHistogramSubAggs?: Function; + dateHistogramSubAggs?: any; + metricAgg: string; + field: string; + timestampField: string; + calculation: ( + b: SeriesBucket, + key: string, + metric: Metric, + defaultSizeInSeconds: number + ) => number | null; + serialize: () => string; +} + /** * Derivative metrics for the first two agg buckets are unusable. For the first bucket, there * simply is no derivative metric (as calculating a derivative requires two adjacent buckets). For @@ -27,12 +50,12 @@ import { formatUTCTimestampForTimezone } from '../format_timezone'; * @param {int} minInMsSinceEpoch Lower bound of timepicker range, in ms-since-epoch * @param {int} bucketSizeInSeconds Size of a single date_histogram bucket, in seconds */ -function offsetMinForDerivativeMetric(minInMsSinceEpoch, bucketSizeInSeconds) { +function offsetMinForDerivativeMetric(minInMsSinceEpoch: number, bucketSizeInSeconds: number) { return minInMsSinceEpoch - 2 * bucketSizeInSeconds * 1000; } // Use the metric object as the source of truth on where to find the UUID -function getUuid(req, metric) { +function getUuid(req: LegacyRequest, metric: Metric) { if (metric.app === 'kibana') { return req.params.kibanaUuid; } else if (metric.app === 'logstash') { @@ -42,12 +65,11 @@ function getUuid(req, metric) { } } -function defaultCalculation(bucket, key) { - const mbKey = `metric_mb_deriv.normalized_value`; - const legacyValue = get(bucket, key, null); - const mbValue = get(bucket, mbKey, null); +function defaultCalculation(bucket: SeriesBucket, key: string) { + const legacyValue: number = get(bucket, key, null); + const mbValue = bucket.metric_mb_deriv?.normalized_value ?? null; let value; - if (!isNaN(mbValue) && mbValue > 0) { + if (mbValue !== null && !isNaN(mbValue) && mbValue > 0) { value = mbValue; } else { value = legacyValue; @@ -60,7 +82,7 @@ function defaultCalculation(bucket, key) { return value; } -function createMetricAggs(metric) { +function createMetricAggs(metric: Metric) { if (metric.derivative) { const mbDerivative = metric.mbField ? { @@ -90,18 +112,20 @@ function createMetricAggs(metric) { } async function fetchSeries( - req, - indexPattern, - metric, - metricOptions, - groupBy, - min, - max, - bucketSize, - filters + req: LegacyRequest, + indexPattern: string, + metric: Metric, + metricOptions: any, + groupBy: string | Record | null, + min: string | number, + max: string | number, + bucketSize: number, + filters: Array> ) { // if we're using a derivative metric, offset the min (also @see comment on offsetMinForDerivativeMetric function) - const adjustedMin = metric.derivative ? offsetMinForDerivativeMetric(min, bucketSize) : min; + const adjustedMin = metric.derivative + ? offsetMinForDerivativeMetric(Number(min), bucketSize) + : Number(min); let dateHistogramSubAggs = null; if (metric.getDateHistogramSubAggs) { @@ -118,15 +142,15 @@ async function fetchSeries( ...createMetricAggs(metric), }; if (metric.mbField) { - dateHistogramSubAggs.metric_mb = { + Reflect.set(dateHistogramSubAggs, 'metric_mb', { [metric.metricAgg]: { field: metric.mbField, }, - }; + }); } } - let aggs = { + let aggs: any = { check: { date_histogram: { field: metric.timestampField, @@ -154,7 +178,7 @@ async function fetchSeries( body: { query: createQuery({ start: adjustedMin, - end: max, + end: Number(max), metric, clusterUuid: req.params.clusterUuid, // TODO: Pass in the UUID as an explicit function parameter @@ -165,10 +189,6 @@ async function fetchSeries( }, }; - if (metric.debug) { - console.log('metric.debug', JSON.stringify(params)); - } - const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); return await callWithRequest(req, 'search', params); } @@ -180,11 +200,11 @@ async function fetchSeries( * @param {String} min Max timestamp for results to exist within. * @return {Number} Index position to use for the first bucket. {@code buckets.length} if none should be used. */ -function findFirstUsableBucketIndex(buckets, min) { +function findFirstUsableBucketIndex(buckets: SeriesBucket[], min: string | number) { const minInMillis = moment.utc(min).valueOf(); for (let i = 0; i < buckets.length; ++i) { - const bucketTime = get(buckets, [i, 'key']); + const bucketTime = buckets[i].key; const bucketTimeInMillis = moment.utc(bucketTime).valueOf(); // if the bucket start time, without knowing the bucket size, is before the filter time, then it's inherently a partial bucket @@ -208,11 +228,16 @@ function findFirstUsableBucketIndex(buckets, min) { * @param {Number} bucketSizeInMillis Size of a bucket in milliseconds. Set to 0 to allow partial trailing buckets. * @return {Number} Index position to use for the last bucket. {@code -1} if none should be used. */ -function findLastUsableBucketIndex(buckets, max, firstUsableBucketIndex, bucketSizeInMillis = 0) { +function findLastUsableBucketIndex( + buckets: SeriesBucket[], + max: string | number, + firstUsableBucketIndex: number, + bucketSizeInMillis: number = 0 +) { const maxInMillis = moment.utc(max).valueOf(); for (let i = buckets.length - 1; i > firstUsableBucketIndex - 1; --i) { - const bucketTime = get(buckets, [i, 'key']); + const bucketTime = buckets[i].key; const bucketTimeInMillis = moment.utc(bucketTime).valueOf() + bucketSizeInMillis; if (bucketTimeInMillis <= maxInMillis) { @@ -224,41 +249,25 @@ function findLastUsableBucketIndex(buckets, max, firstUsableBucketIndex, bucketS return -1; } -const formatBucketSize = (bucketSizeInSeconds) => { +const formatBucketSize = (bucketSizeInSeconds: number) => { const now = moment(); const timestamp = moment(now).add(bucketSizeInSeconds, 'seconds'); // clone the `now` object return formatTimestampToDuration(timestamp, CALCULATE_DURATION_UNTIL, now); }; -function isObject(value) { - return typeof value === 'object' && !!value && !Array.isArray(value); -} - -function countBuckets(data, count = 0) { - if (data && data.buckets) { - count += data.buckets.length; - for (const bucket of data.buckets) { - for (const key of Object.keys(bucket)) { - if (isObject(bucket[key])) { - count = countBuckets(bucket[key], count); - } - } - } - } else if (data) { - for (const key of Object.keys(data)) { - if (isObject(data[key])) { - count = countBuckets(data[key], count); - } - } - } - return count; -} - -function handleSeries(metric, groupBy, min, max, bucketSizeInSeconds, timezone, response) { +function handleSeries( + metric: Metric, + groupBy: string | Record | null, + min: string | number, + max: string | number, + bucketSizeInSeconds: number, + timezone: string, + response: ElasticsearchResponse +) { const { derivative, calculation: customCalculation } = metric; - function getAggregatedData(buckets) { + function getAggregatedData(buckets: SeriesBucket[]) { const firstUsableBucketIndex = findFirstUsableBucketIndex(buckets, min); const lastUsableBucketIndex = findLastUsableBucketIndex( buckets, @@ -266,20 +275,7 @@ function handleSeries(metric, groupBy, min, max, bucketSizeInSeconds, timezone, firstUsableBucketIndex, bucketSizeInSeconds * 1000 ); - let data = []; - - if (metric.debug) { - console.log( - `metric.debug field=${metric.field} bucketsCreated: ${countBuckets( - get(response, 'aggregations.check') - )}` - ); - console.log(`metric.debug`, { - bucketsLength: buckets.length, - firstUsableBucketIndex, - lastUsableBucketIndex, - }); - } + let data: Array<[string | number, number | null]> = []; if (firstUsableBucketIndex <= lastUsableBucketIndex) { // map buckets to values for charts @@ -306,15 +302,17 @@ function handleSeries(metric, groupBy, min, max, bucketSizeInSeconds, timezone, } if (groupBy) { - return get(response, 'aggregations.groupBy.buckets', []).map((bucket) => { - return { - groupedBy: bucket.key, - ...getAggregatedData(get(bucket, 'check.buckets', [])), - }; - }); + return (response?.aggregations?.groupBy?.buckets ?? []).map( + (bucket: Bucket & { check: { buckets: SeriesBucket[] } }) => { + return { + groupedBy: bucket.key, + ...getAggregatedData(bucket.check.buckets ?? []), + }; + } + ); } - return getAggregatedData(get(response, 'aggregations.check.buckets', [])); + return getAggregatedData(response.aggregations?.check?.buckets ?? []); } /** @@ -329,13 +327,18 @@ function handleSeries(metric, groupBy, min, max, bucketSizeInSeconds, timezone, * @return {Promise} The object response containing the {@code timeRange}, {@code metric}, and {@code data}. */ export async function getSeries( - req, - indexPattern, - metricName, - metricOptions, - filters, - groupBy, - { min, max, bucketSize, timezone } + req: LegacyRequest, + indexPattern: string, + metricName: string, + metricOptions: Record, + filters: Array>, + groupBy: string | Record | null, + { + min, + max, + bucketSize, + timezone, + }: { min: string | number; max: string | number; bucketSize: number; timezone: string } ) { checkParam(indexPattern, 'indexPattern in details/getSeries'); diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.ts index 482cbd3601993..17dc48d0b237e 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/ccr.ts @@ -50,5 +50,5 @@ export async function checkCcrEnabled(req: LegacyRequest, esIndexPattern: string const mbCcr = response.hits?.hits[0]?._source?.elasticsearch?.cluster?.stats?.stack?.xpack?.ccr; const isEnabled = legacyCcr?.enabled ?? mbCcr?.enabled; const isAvailable = legacyCcr?.available ?? mbCcr?.available; - return isEnabled && isAvailable; + return Boolean(isEnabled && isAvailable); } diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/convert_metric_names.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/convert_metric_names.ts similarity index 78% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/convert_metric_names.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/convert_metric_names.ts index cd3390416ccc3..244f2a14a2ecc 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/convert_metric_names.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/convert_metric_names.ts @@ -30,13 +30,13 @@ const CONVERTED_TOKEN = `odh_`; * @param string prefix - This is the aggregation name prefix where the rest of the name will be the type of aggregation * @param object metricObj The metric aggregation itself */ -export function convertMetricNames(prefix, metricObj) { +export function convertMetricNames(prefix: string, metricObj: Record) { return Object.entries(metricObj).reduce((newObj, [key, value]) => { const newValue = cloneDeep(value); if (key.includes('_deriv') && newValue.derivative) { newValue.derivative.buckets_path = `${CONVERTED_TOKEN}${prefix}__${newValue.derivative.buckets_path}`; } - newObj[`${CONVERTED_TOKEN}${prefix}__${key}`] = newValue; + Reflect.set(newObj, `${CONVERTED_TOKEN}${prefix}__${key}`, newValue); return newObj; }, {}); } @@ -50,32 +50,36 @@ export function convertMetricNames(prefix, metricObj) { * * @param object byDateBucketResponse - The response object from the single `date_histogram` bucket */ -export function uncovertMetricNames(byDateBucketResponse) { - const unconverted = {}; + +type MetricNameBucket = { key: string; key_as_string: string; doc_count: number } & Record< + string, + any +>; +export function uncovertMetricNames(byDateBucketResponse: { buckets: MetricNameBucket[] }) { + const unconverted: Record = {}; for (const metricName of LISTING_METRICS_NAMES) { unconverted[metricName] = { buckets: byDateBucketResponse.buckets.map((bucket) => { const { - // eslint-disable-next-line camelcase + // eslint-disable-next-line @typescript-eslint/naming-convention key_as_string, - // eslint-disable-next-line camelcase key, - // eslint-disable-next-line camelcase + // eslint-disable-next-line @typescript-eslint/naming-convention doc_count, ...rest } = bucket; - const metrics = Object.entries(rest).reduce((accum, [key, value]) => { - if (key.startsWith(`${CONVERTED_TOKEN}${metricName}`)) { - const name = key.split('__')[1]; + const metrics = Object.entries(rest).reduce((accum, [k, value]) => { + if (k.startsWith(`${CONVERTED_TOKEN}${metricName}`)) { + const name = k.split('__')[1]; accum[name] = value; } return accum; - }, {}); + }, {} as Record); return { - key_as_string /* eslint-disable-line camelcase */, + key_as_string, key, - doc_count /* eslint-disable-line camelcase */, + doc_count, ...metrics, }; }), diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/index.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/index.ts similarity index 100% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/index.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/index.ts diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.ts index 0a9993769a6e5..a43feb8fc84a3 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.ts @@ -35,10 +35,11 @@ export function handleResponse( hit.inner_hits?.earliest?.hits?.hits[0]?._source.elasticsearch?.index; const rateOptions = { - hitTimestamp: hit._source.timestamp ?? hit._source['@timestamp'], + hitTimestamp: hit._source.timestamp ?? hit._source['@timestamp'] ?? null, earliestHitTimestamp: hit.inner_hits?.earliest?.hits?.hits[0]?._source.timestamp ?? - hit.inner_hits?.earliest?.hits?.hits[0]?._source['@timestamp'], + hit.inner_hits?.earliest?.hits?.hits[0]?._source['@timestamp'] ?? + null, timeWindowMin: min, timeWindowMax: max, }; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/index.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/index.ts similarity index 100% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/indices/index.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/indices/index.ts diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/calculate_node_type.test.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/calculate_node_type.test.js index 1e056c19ea9ee..d249bc82b9387 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/calculate_node_type.test.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/calculate_node_type.test.js @@ -6,7 +6,7 @@ */ import { set } from '@elastic/safer-lodash-set'; -import { calculateNodeType } from './calculate_node_type.js'; +import { calculateNodeType } from './calculate_node_type'; const masterNodeId = 'def456'; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/calculate_node_type.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/calculate_node_type.ts similarity index 69% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/calculate_node_type.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/calculate_node_type.ts index 4b2bcb4cda432..8fc6084b114df 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/calculate_node_type.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/calculate_node_type.ts @@ -12,16 +12,22 @@ * - client only node: --node.data=false --node.master=false * https://www.elastic.co/guide/en/elasticsearch/reference/2.x/modules-node.html */ -import { includes, isUndefined } from 'lodash'; +import { isUndefined } from 'lodash'; +import { ElasticsearchLegacySource } from '../../../../common/types/es'; -export function calculateNodeType(node, masterNodeId) { +export type Node = ElasticsearchLegacySource['source_node'] & { + attributes?: Record; + node_ids?: Array; +}; + +export function calculateNodeType(node: Node, masterNodeId?: string | boolean) { const attrs = node.attributes || {}; - function mightBe(attr) { + function mightBe(attr?: string) { return attr === 'true' || isUndefined(attr); } - function isNot(attr) { + function isNot(attr?: string) { return attr === 'false'; } @@ -30,7 +36,7 @@ export function calculateNodeType(node, masterNodeId) { if (uuid !== undefined && uuid === masterNodeId) { return 'master'; } - if (includes(node.node_ids, masterNodeId)) { + if (node.node_ids?.includes(masterNodeId)) { return 'master'; } if (isNot(attrs.data) && isNot(attrs.master)) { diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_default_node_from_id.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_default_node_from_id.ts similarity index 76% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_default_node_from_id.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_default_node_from_id.ts index a0d4602340419..047bcf554ddf0 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_default_node_from_id.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_default_node_from_id.ts @@ -10,7 +10,7 @@ * If node information can't be retrieved, we call this function * that provides some usable defaults */ -export function getDefaultNodeFromId(nodeId) { +export function getDefaultNodeFromId(nodeId: string) { return { id: nodeId, name: nodeId, @@ -20,3 +20,7 @@ export function getDefaultNodeFromId(nodeId) { attributes: {}, }; } + +export function isDefaultNode(node: any): node is ReturnType { + return !node.uuid; +} diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.ts index 79c44286717b4..aed6b40675e45 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; // @ts-ignore import { checkParam } from '../../error_missing_required'; @@ -14,7 +13,7 @@ import { createQuery } from '../../create_query'; // @ts-ignore import { ElasticsearchMetric } from '../../metrics'; // @ts-ignore -import { getDefaultNodeFromId } from './get_default_node_from_id'; +import { getDefaultNodeFromId, isDefaultNode } from './get_default_node_from_id'; // @ts-ignore import { calculateNodeType } from './calculate_node_type'; // @ts-ignore @@ -23,7 +22,6 @@ import { ElasticsearchSource, ElasticsearchResponse, ElasticsearchLegacySource, - ElasticsearchMetricbeatNode, } from '../../../../common/types/es'; import { LegacyRequest } from '../../../types'; @@ -35,9 +33,9 @@ export function handleResponse( return (response: ElasticsearchResponse) => { let nodeSummary = {}; const nodeStatsHits = response.hits?.hits ?? []; - const nodes: Array< - ElasticsearchLegacySource['source_node'] | ElasticsearchMetricbeatNode - > = nodeStatsHits.map((hit) => hit._source.elasticsearch?.node || hit._source.source_node); // using [0] value because query results are sorted desc per timestamp + const nodes: Array = nodeStatsHits.map( + (hit) => hit._source.elasticsearch?.node || hit._source.source_node + ); // using [0] value because query results are sorted desc per timestamp const node = nodes[0] || getDefaultNodeFromId(nodeUuid); const sourceStats = response.hits?.hits[0]?._source.elasticsearch?.node?.stats || @@ -46,7 +44,7 @@ export function handleResponse( clusterState && clusterState.nodes ? clusterState.nodes[nodeUuid] : undefined; const stats = { resolver: nodeUuid, - node_ids: nodes.map((_node) => node.id || node.uuid), + node_ids: nodes.map((_node) => (isDefaultNode(node) ? node.id : node.id || node.uuid)), attributes: node.attributes, transport_address: response.hits?.hits[0]?._source.service?.address || node.transport_address, name: node.name, @@ -54,8 +52,8 @@ export function handleResponse( }; if (clusterNode) { - const _shardStats = get(shardStats, ['nodes', nodeUuid], {}); - const calculatedNodeType = calculateNodeType(stats, get(clusterState, 'master_node')); // set type for labeling / iconography + const _shardStats = shardStats.nodes[nodeUuid] ?? {}; + const calculatedNodeType = calculateNodeType(stats, clusterState?.master_node); // set type for labeling / iconography const { nodeType, nodeTypeLabel, nodeTypeClass } = getNodeTypeClassLabel( node, calculatedNodeType diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_type_class_label.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_type_class_label.ts similarity index 69% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_type_class_label.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_type_class_label.ts index fb7429d9d79b6..c0a4f4ff2a48c 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_type_class_label.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_type_class_label.ts @@ -6,6 +6,10 @@ */ import { nodeTypeLabel, nodeTypeClass } from './lookups'; +import { + ElasticsearchLegacySource, + ElasticsearchMetricbeatNode, +} from '../../../../common/types/es'; /* * Note: currently only `node` and `master` are supported due to @@ -13,8 +17,11 @@ import { nodeTypeLabel, nodeTypeClass } from './lookups'; * @param {Object} node - a node object from getNodes / getNodeSummary * @param {Object} type - the node type calculated from `calculateNodeType` */ -export function getNodeTypeClassLabel(node, type) { - const nodeType = node.master ? 'master' : type; +export function getNodeTypeClassLabel( + node: ElasticsearchLegacySource['source_node'] | ElasticsearchMetricbeatNode, + type: keyof typeof nodeTypeLabel +) { + const nodeType = node && 'master' in node ? 'master' : type; const returnObj = { nodeType, nodeTypeLabel: nodeTypeLabel[nodeType], diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_live_nodes.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_live_nodes.ts similarity index 84% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_live_nodes.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_live_nodes.ts index 3ae1d87427d2b..7bcba822845c3 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_live_nodes.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_live_nodes.ts @@ -5,7 +5,9 @@ * 2.0. */ -export async function getLivesNodes(req) { +import { LegacyRequest } from '../../../../types'; + +export async function getLivesNodes(req: LegacyRequest) { const params = { path: '/_nodes', method: 'GET', diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_metric_aggs.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_metric_aggs.ts similarity index 96% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_metric_aggs.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_metric_aggs.ts index c34458c30469d..ea2940ee1589c 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_metric_aggs.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_metric_aggs.ts @@ -18,7 +18,7 @@ import { convertMetricNames } from '../../convert_metric_names'; * @param {Number} bucketSize: Bucket size in seconds for date histogram interval * @return {Object} Aggregation DSL */ -export function getMetricAggs(listingMetrics) { +export function getMetricAggs(listingMetrics: string[]) { let aggItems = {}; listingMetrics.forEach((metricName) => { diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.ts similarity index 86% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.ts index 87781417a07e5..03524eebd12e8 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.ts @@ -9,8 +9,14 @@ import moment from 'moment'; import { get } from 'lodash'; import { ElasticsearchMetric } from '../../../metrics'; import { createQuery } from '../../../create_query'; +import { LegacyRequest, Bucket } from '../../../../types'; -export async function getNodeIds(req, indexPattern, { clusterUuid }, size) { +export async function getNodeIds( + req: LegacyRequest, + indexPattern: string, + { clusterUuid }: { clusterUuid: string }, + size: number +) { const start = moment.utc(req.payload.timeRange.min).valueOf(); const end = moment.utc(req.payload.timeRange.max).valueOf(); @@ -55,5 +61,7 @@ export async function getNodeIds(req, indexPattern, { clusterUuid }, size) { const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); const response = await callWithRequest(req, 'search', params); - return get(response, 'aggregations.composite_data.buckets', []).map((bucket) => bucket.key); + return get(response, 'aggregations.composite_data.buckets', []).map( + (bucket: Bucket) => bucket.key + ); } diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.ts index 442a2b1b9b9c9..0b5c0337e6c47 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.ts @@ -6,18 +6,12 @@ */ import moment from 'moment'; -// @ts-ignore import { checkParam } from '../../../error_missing_required'; -// @ts-ignore import { createQuery } from '../../../create_query'; -// @ts-ignore import { calculateAuto } from '../../../calculate_auto'; -// @ts-ignore import { ElasticsearchMetric } from '../../../metrics'; -// @ts-ignore import { getMetricAggs } from './get_metric_aggs'; import { handleResponse } from './handle_response'; -// @ts-ignore import { LISTING_METRICS_NAMES, LISTING_METRICS_PATHS } from './nodes_listing_metrics'; import { LegacyRequest } from '../../../../types'; import { ElasticsearchModifiedSource } from '../../../../../common/types/es'; @@ -103,7 +97,7 @@ export async function getNodes( min_doc_count: 0, fixed_interval: bucketSize + 's', }, - aggs: getMetricAggs(LISTING_METRICS_NAMES, bucketSize), + aggs: getMetricAggs(LISTING_METRICS_NAMES), }, }, }, diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_paginated_nodes.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_paginated_nodes.ts similarity index 74% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_paginated_nodes.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_paginated_nodes.ts index 485378fd01de0..118140fe3f9cd 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_paginated_nodes.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_paginated_nodes.ts @@ -5,12 +5,15 @@ * 2.0. */ -import { get, isUndefined } from 'lodash'; +import { isUndefined } from 'lodash'; import { getNodeIds } from './get_node_ids'; +// @ts-ignore import { filter } from '../../../pagination/filter'; import { sortNodes } from './sort_nodes'; +// @ts-ignore import { paginate } from '../../../pagination/paginate'; import { getMetrics } from '../../../details/get_metrics'; +import { LegacyRequest } from '../../../../types'; /** * This function performs an optimization around the node listing tables in the UI. To avoid @@ -28,25 +31,41 @@ import { getMetrics } from '../../../details/get_metrics'; * @param {*} sort - ({ field, direction }) * @param {*} queryText - Text that will be used to filter out pipelines */ + +interface Node { + name: string; + uuid: string; + isOnline: boolean; + shardCount: number; +} + export async function getPaginatedNodes( - req, - esIndexPattern, - { clusterUuid }, - metricSet, - pagination, - sort, - queryText, - { clusterStats, nodesShardCount } + req: LegacyRequest, + esIndexPattern: string, + { clusterUuid }: { clusterUuid: string }, + metricSet: string[], + pagination: { index: number; size: number }, + sort: { field: string; direction: 'asc' | 'desc' }, + queryText: string, + { + clusterStats, + nodesShardCount, + }: { + clusterStats: { + cluster_state: { nodes: Record }; + }; + nodesShardCount: { nodes: Record }; + } ) { const config = req.server.config(); - const size = config.get('monitoring.ui.max_bucket_size'); - const nodes = await getNodeIds(req, esIndexPattern, { clusterUuid }, size); + const size = Number(config.get('monitoring.ui.max_bucket_size')); + const nodes: Node[] = await getNodeIds(req, esIndexPattern, { clusterUuid }, size); // Add `isOnline` and shards from the cluster state and shard stats - const clusterState = get(clusterStats, 'cluster_state', { nodes: {} }); + const clusterState = clusterStats?.cluster_state ?? { nodes: {} }; for (const node of nodes) { - node.isOnline = !isUndefined(get(clusterState, ['nodes', node.uuid])); - node.shardCount = get(nodesShardCount, `nodes[${node.uuid}].shardCount`, 0); + node.isOnline = !isUndefined(clusterState?.nodes[node.uuid]); + node.shardCount = nodesShardCount?.nodes[node.uuid]?.shardCount ?? 0; } // `metricSet` defines a list of metrics that are sortable in the UI @@ -82,7 +101,7 @@ export async function getPaginatedNodes( const metricList = metricSeriesData[metricName]; for (const metricItem of metricList[0]) { - const node = nodes.find((node) => node.uuid === metricItem.groupedBy); + const node = nodes.find((n) => n.uuid === metricItem.groupedBy); if (!node) { continue; } @@ -91,7 +110,7 @@ export async function getPaginatedNodes( if (dataSeries && dataSeries.length) { const lastItem = dataSeries[dataSeries.length - 1]; if (lastItem.length && lastItem.length === 2) { - node[metricName] = lastItem[1]; + Reflect.set(node, metricName, lastItem[1]); } } } diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/handle_response.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/handle_response.ts index 5beabe70f0771..9fc06f1f9654f 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/handle_response.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/handle_response.ts @@ -7,9 +7,7 @@ import { get } from 'lodash'; import { mapNodesInfo } from './map_nodes_info'; -// @ts-ignore import { mapNodesMetrics } from './map_nodes_metrics'; -// @ts-ignore import { uncovertMetricNames } from '../../convert_metric_names'; import { ElasticsearchResponse, ElasticsearchModifiedSource } from '../../../../../common/types/es'; @@ -26,7 +24,7 @@ export function handleResponse( clusterStats: ElasticsearchModifiedSource | undefined, nodesShardCount: { nodes: { [nodeId: string]: { shardCount: number } } } | undefined, pageOfNodes: Array<{ uuid: string }>, - timeOptions = {} + timeOptions: { min?: number; max?: number; bucketSize?: number } = {} ) { if (!get(response, 'hits.hits')) { return []; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/index.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/index.ts similarity index 100% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/index.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/index.ts diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_info.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_info.ts index 745556f5d2c88..aaa2092e08c4f 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_info.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_info.ts @@ -6,9 +6,7 @@ */ import { isUndefined } from 'lodash'; -// @ts-ignore import { calculateNodeType } from '../calculate_node_type'; -// @ts-ignore import { getNodeTypeClassLabel } from '../get_node_type_class_label'; import { ElasticsearchResponseHit, @@ -31,6 +29,7 @@ export function mapNodesInfo( return nodeHits.reduce((prev, node) => { const sourceNode = node._source.source_node || node._source.elasticsearch?.node; + if (!sourceNode) return prev; const calculatedNodeType = calculateNodeType(sourceNode, clusterState?.master_node); const { nodeType, nodeTypeLabel, nodeTypeClass } = getNodeTypeClassLabel( diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_metrics.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_metrics.ts similarity index 75% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_metrics.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_metrics.ts index bee11e0e10494..3426c7ac42643 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_metrics.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_metrics.ts @@ -8,8 +8,16 @@ import { get, map, min, max, last } from 'lodash'; import { filterPartialBuckets } from '../../../filter_partial_buckets'; import { metrics } from '../../../metrics'; +import { Bucket } from '../../../../types'; -function calcSlope(data) { +type MetricBucket = Bucket & { metric_deriv?: { value: number; normalized_value: number } }; +interface TimeOptions { + min?: number; + max?: number; + bucketSize?: number; +} + +function calcSlope(data: Array<{ x: number; y: number }>) { const length = data.length; const xSum = data.reduce((prev, curr) => prev + curr.x, 0); const ySum = data.reduce((prev, curr) => prev + curr.y, 0); @@ -27,12 +35,15 @@ function calcSlope(data) { return null; // convert possible NaN to `null` for JSON-friendliness } -const mapBuckets = (bucket, metric) => { +const mapBuckets = ( + bucket: MetricBucket, + metric: { derivative: boolean; calculation: (b: Bucket) => number | null } +) => { const x = bucket.key; if (metric.calculation) { return { - x: bucket.key, + x: Number(bucket.key), y: metric.calculation(bucket), }; } @@ -60,12 +71,16 @@ const mapBuckets = (bucket, metric) => { return { x, y: null }; }; -function reduceMetric(metricName, metricBuckets, { min: startTime, max: endTime, bucketSize }) { +function reduceMetric( + metricName: string, + metricBuckets: MetricBucket[], + { min: startTime, max: endTime, bucketSize }: TimeOptions +) { if (startTime === undefined || endTime === undefined || startTime >= endTime) { return null; } - const partialBucketFilter = filterPartialBuckets(startTime, endTime, bucketSize, { + const partialBucketFilter = filterPartialBuckets(startTime, endTime, bucketSize!, { ignoreEarly: true, }); const metric = metrics[metricName]; @@ -85,7 +100,7 @@ function reduceMetric(metricName, metricBuckets, { min: startTime, max: endTime, const minVal = min(map(mappedData, 'y')); const maxVal = max(map(mappedData, 'y')); const lastVal = last(map(mappedData, 'y')); - const slope = calcSlope(mappedData) > 0 ? 1 : -1; // no need for the entire precision, it's just an up/down arrow + const slope = Number(calcSlope(mappedData as Array<{ x: number; y: number }>)) > 0 ? 1 : -1; // no need for the entire precision, it's just an up/down arrow return { metric: metric.serialize(), @@ -93,14 +108,14 @@ function reduceMetric(metricName, metricBuckets, { min: startTime, max: endTime, }; } -function reduceAllMetrics(metricSet, timeOptions) { - const metrics = {}; +function reduceAllMetrics(metricSet: string[], timeOptions: TimeOptions) { + const reducedMetrics: Record = {}; Object.keys(metricSet).forEach((metricName) => { const metricBuckets = get(metricSet, [metricName, 'buckets']); - metrics[metricName] = reduceMetric(metricName, metricBuckets, timeOptions); // append summarized metric data + reducedMetrics[metricName] = reduceMetric(metricName, metricBuckets, timeOptions); // append summarized metric data }); - return metrics; + return reducedMetrics; } /* @@ -112,8 +127,12 @@ function reduceAllMetrics(metricSet, timeOptions) { * @param {Object} timeOptions: min, max, and bucketSize needed for date histogram creation * @return {Object} summarized metric data about each node keyed by nodeId */ -export function mapNodesMetrics(metricsForNodes, nodesInfo, timeOptions) { - const metricRows = {}; +export function mapNodesMetrics( + metricsForNodes: Record, + nodesInfo: Record, + timeOptions: TimeOptions +) { + const metricRows: Record = {}; Object.keys(metricsForNodes).forEach((nodeId) => { if (nodesInfo[nodeId].isOnline) { // only do the work of mapping metrics if the node is online diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/nodes_listing_metrics.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/nodes_listing_metrics.ts similarity index 100% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/nodes_listing_metrics.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/nodes_listing_metrics.ts diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/sort_nodes.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/sort_nodes.ts similarity index 76% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/sort_nodes.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/sort_nodes.ts index 3a1f9e264a2ee..33d29f2a05998 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/sort_nodes.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/sort_nodes.ts @@ -7,7 +7,9 @@ import { orderBy } from 'lodash'; -export function sortNodes(nodes, sort) { +type Node = Record; + +export function sortNodes(nodes: Node[], sort?: { field: string; direction: 'asc' | 'desc' }) { if (!sort || !sort.field) { return nodes; } diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/index.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/index.ts similarity index 88% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/index.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/index.ts index 99ada270ac77e..8f474c0284844 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/index.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/index.ts @@ -7,6 +7,6 @@ export { getNodes } from './get_nodes'; export { getNodeSummary } from './get_node_summary'; -export { calculateNodeType } from './calculate_node_type'; +export { calculateNodeType, Node } from './calculate_node_type'; export { getNodeTypeClassLabel } from './get_node_type_class_label'; export { getDefaultNodeFromId } from './get_default_node_from_id'; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/lookups.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/lookups.ts similarity index 100% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/lookups.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/lookups.ts diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/types.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/types.ts new file mode 100644 index 0000000000000..57c0e643888e8 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/types.ts @@ -0,0 +1,13 @@ +/* + * 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 { ElasticsearchLegacySource } from '../../../../common/types/es'; + +export type Node = ElasticsearchLegacySource['source_node'] & { + attributes?: Record; + node_ids: Array; +}; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/calculate_shard_stat_indices_totals.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/calculate_shard_stat_indices_totals.ts similarity index 83% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/shards/calculate_shard_stat_indices_totals.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/shards/calculate_shard_stat_indices_totals.ts index 1b0c4a13cebb7..96e7b5491c4d4 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/calculate_shard_stat_indices_totals.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/calculate_shard_stat_indices_totals.ts @@ -8,7 +8,16 @@ /* * Calculate totals from mapped indices data */ -export function calculateIndicesTotals(indices) { +export function calculateIndicesTotals( + indices: Record< + string, + { + primary: number; + replica: number; + unassigned: { primary: number; replica: number }; + } + > +) { // create datasets for each index const metrics = Object.keys(indices).map((i) => { const index = indices[i]; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stat_aggs.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stat_aggs.ts similarity index 87% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stat_aggs.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stat_aggs.ts index 8bb79c0b0a70c..cf11c939fb230 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stat_aggs.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_shard_stat_aggs.ts @@ -9,7 +9,14 @@ * @param {Object} config - Kibana config service * @param {Boolean} includeNodes - whether to add the aggs for node shards */ -export function getShardAggs(config, includeNodes, includeIndices) { + +import { LegacyServer } from '../../../types'; + +export function getShardAggs( + config: ReturnType, + includeNodes: boolean, + includeIndices: boolean +) { const maxBucketSize = config.get('monitoring.ui.max_bucket_size'); const aggSize = 10; const indicesAgg = { diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_unassigned_shards.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_unassigned_shards.ts similarity index 67% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_unassigned_shards.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_unassigned_shards.ts index 86e747f415058..12735406ff901 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_unassigned_shards.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/get_unassigned_shards.ts @@ -5,18 +5,18 @@ * 2.0. */ -import { get } from 'lodash'; - // Methods for calculating metrics for // - Number of Primary Shards // - Number of Replica Shards // - Unassigned Primary Shards // - Unassigned Replica Shards -export function getUnassignedShards(indexShardStats) { +export function getUnassignedShards(indexShardStats: { + unassigned: { primary: number; replica: number }; +}) { let unassignedShards = 0; - unassignedShards += get(indexShardStats, 'unassigned.primary'); - unassignedShards += get(indexShardStats, 'unassigned.replica'); + unassignedShards += indexShardStats.unassigned.primary; + unassignedShards += indexShardStats.unassigned.replica; return unassignedShards; } diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/index.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/index.ts similarity index 100% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/shards/index.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/shards/index.ts diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/normalize_shard_objects.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/normalize_shard_objects.ts similarity index 71% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/shards/normalize_shard_objects.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/shards/normalize_shard_objects.ts index c0bb9836238db..511935f615cd8 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/normalize_shard_objects.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/normalize_shard_objects.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { get, partition } from 'lodash'; -import { calculateNodeType } from '../nodes'; +import { partition } from 'lodash'; +import { calculateNodeType, Node } from '../nodes'; /* * Reducer function for a set of nodes to key the array by nodeId, summarize @@ -14,8 +14,28 @@ import { calculateNodeType } from '../nodes'; * @param masterNode = nodeId of master node * @return reducer function for set of nodes */ -export function normalizeNodeShards(masterNode) { - return (nodes, node) => { + +type NodeShard = Node & { + key: string; + node_ids: { buckets: Array<{ key: string }> }; + node_names: { buckets: Array<{ key: string }> }; + index_count: { value: number }; + doc_count: number; +}; + +interface ShardBucket { + key: string; + primary: { + buckets: Array<{ + key: string; + key_as_string: string; + doc_count: number; + }>; + }; +} + +export function normalizeNodeShards(masterNode: string) { + return (nodes: NodeShard[], node: NodeShard) => { if (node.key && node.node_ids) { const nodeIds = node.node_ids.buckets.map((b) => b.key); const _node = { @@ -27,8 +47,8 @@ export function normalizeNodeShards(masterNode) { ...nodes, [node.key]: { shardCount: node.doc_count, - indexCount: get(node, 'index_count.value'), - name: get(node, 'node_names.buckets[0].key'), + indexCount: node.index_count.value, + name: node.node_names.buckets[0].key, node_ids: nodeIds, type: calculateNodeType(_node, masterNode), // put the "star" icon on the node link in the shard allocator }, @@ -38,12 +58,12 @@ export function normalizeNodeShards(masterNode) { }; } -const countShards = (shardBuckets) => { +const countShards = (shardBuckets: ShardBucket[]) => { let primaryShards = 0; let replicaShards = 0; shardBuckets.forEach((shard) => { - const primaryMap = get(shard, 'primary.buckets', []); + const primaryMap = shard.primary.buckets ?? []; const primaryBucket = primaryMap.find((b) => b.key_as_string === 'true'); if (primaryBucket !== undefined) { @@ -62,13 +82,18 @@ const countShards = (shardBuckets) => { }; }; +interface Index { + key: string; + states?: { buckets?: ShardBucket[] }; +} + /* * Reducer function for a set of indices to key the array by index name, and * summarize the shard data. * @return reducer function for set of indices */ -export function normalizeIndexShards(indices, index) { - const stateBuckets = get(index, 'states.buckets', []); +export function normalizeIndexShards(indices: Index[], index: Index) { + const stateBuckets = index.states?.buckets ?? []; const [assignedShardBuckets, unassignedShardBuckets] = partition(stateBuckets, (b) => { return b.key === 'STARTED' || b.key === 'RELOCATING'; }); diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_monitoring_auth.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_monitoring_auth.ts similarity index 93% rename from x-pack/plugins/monitoring/server/lib/elasticsearch/verify_monitoring_auth.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch/verify_monitoring_auth.ts index e4cee4d4455ca..7673f1b7ff052 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_monitoring_auth.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_monitoring_auth.ts @@ -8,6 +8,7 @@ import { get } from 'lodash'; import Boom from '@hapi/boom'; import { INDEX_PATTERN } from '../../../common/constants'; +import { LegacyRequest } from '../../types'; /* * Check the currently logged-in user's privileges for "read" privileges on the @@ -16,7 +17,7 @@ import { INDEX_PATTERN } from '../../../common/constants'; * * @param req {Object} the server route handler request object */ -export async function verifyMonitoringAuth(req) { +export async function verifyMonitoringAuth(req: LegacyRequest) { const xpackInfo = get(req.server.plugins.monitoring, 'info'); if (xpackInfo) { @@ -37,7 +38,7 @@ export async function verifyMonitoringAuth(req) { * @param req {Object} the server route handler request object * @return {Promise} That either resolves with no response (void) or an exception. */ -async function verifyHasPrivileges(req) { +async function verifyHasPrivileges(req: LegacyRequest) { const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); let response; diff --git a/x-pack/plugins/monitoring/server/lib/error_missing_required.js b/x-pack/plugins/monitoring/server/lib/error_missing_required.ts similarity index 66% rename from x-pack/plugins/monitoring/server/lib/error_missing_required.js rename to x-pack/plugins/monitoring/server/lib/error_missing_required.ts index b63fecdd56c6d..130e3d8a8e303 100644 --- a/x-pack/plugins/monitoring/server/lib/error_missing_required.js +++ b/x-pack/plugins/monitoring/server/lib/error_missing_required.ts @@ -10,7 +10,7 @@ * @param param - anything * @param context {String} calling context used in the error message */ -export function checkParam(param, context) { +export function checkParam(param: any, context: string) { if (!param) { throw new MissingRequiredError(context); } @@ -21,10 +21,12 @@ export function checkParam(param, context) { * - verification in unit tests * see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error */ -export function MissingRequiredError(param) { - this.name = 'MissingRequiredError'; - this.message = `Missing required parameter or field: ${param}`; - this.stack = new Error().stack; + +export class MissingRequiredError extends Error { + constructor(param: string) { + super(); + this.name = 'MissingRequiredError'; + this.message = `Missing required parameter or field: ${param}`; + this.stack = new Error().stack; + } } -MissingRequiredError.prototype = Object.create(Error.prototype); -MissingRequiredError.prototype.constructor = MissingRequiredError; diff --git a/x-pack/plugins/monitoring/server/lib/filter_partial_buckets.js b/x-pack/plugins/monitoring/server/lib/filter_partial_buckets.ts similarity index 70% rename from x-pack/plugins/monitoring/server/lib/filter_partial_buckets.js rename to x-pack/plugins/monitoring/server/lib/filter_partial_buckets.ts index c39cfc4ea4394..35d079fa323cf 100644 --- a/x-pack/plugins/monitoring/server/lib/filter_partial_buckets.js +++ b/x-pack/plugins/monitoring/server/lib/filter_partial_buckets.ts @@ -7,22 +7,31 @@ import moment from 'moment'; +interface Bucket { + key: string; +} + /* calling .subtract or .add on a moment object mutates the object * so this function shortcuts creating a fresh object */ -function getTime(bucket) { +function getTime(bucket: Bucket) { return moment.utc(bucket.key); } /* find the milliseconds of difference between 2 moment objects */ -function getDelta(t1, t2) { +function getDelta(t1: number, t2: number) { return moment.duration(t1 - t2).asMilliseconds(); } -export function filterPartialBuckets(min, max, bucketSize, options = {}) { - return (bucket) => { +export function filterPartialBuckets( + min: number, + max: number, + bucketSize: number, + options: { ignoreEarly?: boolean } = {} +) { + return (bucket: Bucket) => { const bucketTime = getTime(bucket); // timestamp is too late to be complete - if (getDelta(max, bucketTime.add(bucketSize, 'seconds')) < 0) { + if (getDelta(max, bucketTime.add(bucketSize, 'seconds').valueOf()) < 0) { return false; } @@ -32,7 +41,7 @@ export function filterPartialBuckets(min, max, bucketSize, options = {}) { * ignoreEarly */ if (options.ignoreEarly !== true) { // timestamp is too early to be complete - if (getDelta(bucketTime.subtract(bucketSize, 'seconds'), min) < 0) { + if (getDelta(bucketTime.subtract(bucketSize, 'seconds').valueOf(), min) < 0) { return false; } } diff --git a/x-pack/plugins/monitoring/server/lib/format_timezone.js b/x-pack/plugins/monitoring/server/lib/format_timezone.ts similarity index 82% rename from x-pack/plugins/monitoring/server/lib/format_timezone.js rename to x-pack/plugins/monitoring/server/lib/format_timezone.ts index 72ee3aed4b512..afcc58550905e 100644 --- a/x-pack/plugins/monitoring/server/lib/format_timezone.js +++ b/x-pack/plugins/monitoring/server/lib/format_timezone.ts @@ -16,11 +16,11 @@ import moment from 'moment'; * @param {*} utcTimestamp UTC timestamp * @param {*} timezone The timezone to convert into */ -export const formatUTCTimestampForTimezone = (utcTimestamp, timezone) => { +export const formatUTCTimestampForTimezone = (utcTimestamp: string | number, timezone: string) => { if (timezone === 'Browser') { return utcTimestamp; } const offsetInMinutes = moment.tz(timezone).utcOffset(); - const offsetTimestamp = utcTimestamp + offsetInMinutes * 1 * 60 * 1000; + const offsetTimestamp = Number(utcTimestamp) + offsetInMinutes * 1 * 60 * 1000; return offsetTimestamp; }; diff --git a/x-pack/plugins/monitoring/server/lib/get_timezone.js b/x-pack/plugins/monitoring/server/lib/get_timezone.ts similarity index 76% rename from x-pack/plugins/monitoring/server/lib/get_timezone.js rename to x-pack/plugins/monitoring/server/lib/get_timezone.ts index 76f3dddfa315c..307ec4477bbaf 100644 --- a/x-pack/plugins/monitoring/server/lib/get_timezone.js +++ b/x-pack/plugins/monitoring/server/lib/get_timezone.ts @@ -5,6 +5,8 @@ * 2.0. */ -export async function getTimezone(req) { +import { LegacyRequest } from '../types'; + +export async function getTimezone(req: LegacyRequest) { return await req.getUiSettingsService().get('dateFormat:tz'); } diff --git a/x-pack/plugins/monitoring/server/lib/helpers.js b/x-pack/plugins/monitoring/server/lib/helpers.ts similarity index 79% rename from x-pack/plugins/monitoring/server/lib/helpers.js rename to x-pack/plugins/monitoring/server/lib/helpers.ts index d5ceb2128c282..a904f8cfa497c 100644 --- a/x-pack/plugins/monitoring/server/lib/helpers.js +++ b/x-pack/plugins/monitoring/server/lib/helpers.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { ElasticsearchResponse, ElasticsearchResponseHit } from '../../common/types/es'; + export const response = { hits: { hits: [ @@ -83,20 +85,22 @@ export const response = { }, }; -export const defaultResponseSort = (handleResponse) => { - const responseMulti = { hits: { hits: [] } }; +export const defaultResponseSort = ( + handleResponse: (r: ElasticsearchResponse, n1: number, n2: number) => any +) => { + const responseMulti = { hits: { hits: [] as ElasticsearchResponseHit[] } }; const hit = response.hits.hits[0]; const version = ['6.6.2', '7.0.0-rc1', '6.7.1']; for (let i = 0, l = version.length; i < l; ++i) { // Deep clone the object to preserve the original - const newBeat = JSON.parse(JSON.stringify({ ...hit })); + const newBeat: ElasticsearchResponseHit = JSON.parse(JSON.stringify({ ...hit })); const { beats_stats: beatsStats } = newBeat._source; - beatsStats.timestamp = `2019-01-0${i + 1}T05:00:00.000Z`; - beatsStats.beat.version = version[i]; - beatsStats.beat.uuid = `${i}${beatsStats.beat.uuid}`; + beatsStats!.timestamp = `2019-01-0${i + 1}T05:00:00.000Z`; + beatsStats!.beat!.version = version[i]; + beatsStats!.beat!.uuid = `${i}${beatsStats!.beat!.uuid}`; responseMulti.hits.hits.push(newBeat); } - return { beats: handleResponse(responseMulti, 0, 0), version }; + return { beats: handleResponse(responseMulti as ElasticsearchResponse, 0, 0), version }; }; diff --git a/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.ts b/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.ts index 15cc9904dd060..2fa0b02e0fb1a 100644 --- a/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.ts +++ b/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.ts @@ -7,7 +7,7 @@ import { merge } from 'lodash'; // @ts-ignore -import { checkParam } from '../error_missing_required'; +import { checkParam, MissingRequiredError } from '../error_missing_required'; // @ts-ignore import { calculateAvailability } from '../calculate_availability'; import { LegacyRequest } from '../../types'; @@ -17,10 +17,13 @@ export function handleResponse(resp: ElasticsearchResponse) { const legacySource = resp.hits?.hits[0]?._source.kibana_stats; const mbSource = resp.hits?.hits[0]?._source.kibana?.stats; const kibana = resp.hits?.hits[0]?._source.kibana?.kibana ?? legacySource?.kibana; + const availabilityTimestamp = + resp.hits?.hits[0]?._source['@timestamp'] ?? legacySource?.timestamp; + if (!availabilityTimestamp) { + throw new MissingRequiredError('timestamp'); + } return merge(kibana, { - availability: calculateAvailability( - resp.hits?.hits[0]?._source['@timestamp'] ?? legacySource?.timestamp - ), + availability: calculateAvailability(availabilityTimestamp), os_memory_free: mbSource?.os?.memory?.free_in_bytes ?? legacySource?.os?.memory?.free_in_bytes, uptime: mbSource?.process?.uptime?.ms ?? legacySource?.process?.uptime_in_millis, }); diff --git a/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas_for_clusters.js b/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas_for_clusters.ts similarity index 86% rename from x-pack/plugins/monitoring/server/lib/kibana/get_kibanas_for_clusters.js rename to x-pack/plugins/monitoring/server/lib/kibana/get_kibanas_for_clusters.ts index 141596ffd2f6f..4e806c07ee660 100644 --- a/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas_for_clusters.js +++ b/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas_for_clusters.ts @@ -6,9 +6,10 @@ */ import Bluebird from 'bluebird'; -import { chain, find, get } from 'lodash'; +import { chain, find } from 'lodash'; +import { LegacyRequest, Cluster, Bucket } from '../../types'; import { checkParam } from '../error_missing_required'; -import { createQuery } from '../create_query.js'; +import { createQuery } from '../create_query'; import { KibanaClusterMetric } from '../metrics'; /* @@ -24,7 +25,11 @@ import { KibanaClusterMetric } from '../metrics'; * - number of instances * - combined health */ -export function getKibanasForClusters(req, kbnIndexPattern, clusters) { +export function getKibanasForClusters( + req: LegacyRequest, + kbnIndexPattern: string, + clusters: Cluster[] +) { checkParam(kbnIndexPattern, 'kbnIndexPattern in kibana/getKibanasForClusters'); const config = req.server.config(); @@ -32,7 +37,7 @@ export function getKibanasForClusters(req, kbnIndexPattern, clusters) { const end = req.payload.timeRange.max; return Bluebird.map(clusters, (cluster) => { - const clusterUuid = get(cluster, 'elasticsearch.cluster.id', cluster.cluster_uuid); + const clusterUuid = cluster.elasticsearch?.cluster?.id ?? cluster.cluster_uuid; const metric = KibanaClusterMetric.getMetricFields(); const params = { index: kbnIndexPattern, @@ -162,9 +167,9 @@ export function getKibanasForClusters(req, kbnIndexPattern, clusters) { const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); return callWithRequest(req, 'search', params).then((result) => { - const aggregations = get(result, 'aggregations', {}); - const kibanaUuids = get(aggregations, 'kibana_uuids.buckets', []); - const statusBuckets = get(aggregations, 'status.buckets', []); + const aggregations = result.aggregations ?? {}; + const kibanaUuids = aggregations.kibana_uuids?.buckets ?? []; + const statusBuckets = aggregations.status?.buckets ?? []; // everything is initialized such that it won't impact any rollup let status = null; @@ -185,19 +190,19 @@ export function getKibanasForClusters(req, kbnIndexPattern, clusters) { statusBuckets, (bucket) => bucket.max_timestamp.value === latestTimestamp ); - status = get(latestBucket, 'key'); + status = latestBucket.key; - requestsTotal = get(aggregations, 'requests_total.value'); - connections = get(aggregations, 'concurrent_connections.value'); - responseTime = get(aggregations, 'response_time_max.value'); - memorySize = get(aggregations, 'memory_rss.value'); // resident set size - memoryLimit = get(aggregations, 'memory_heap_size_limit.value'); // max old space + requestsTotal = aggregations.requests_total?.value; + connections = aggregations.concurrent_connections?.value; + responseTime = aggregations.response_time_max?.value; + memorySize = aggregations.memory_rss?.value; + memoryLimit = aggregations.memory_heap_size_limit?.value; } return { clusterUuid, stats: { - uuids: get(aggregations, 'kibana_uuids.buckets', []).map(({ key }) => key), + uuids: kibanaUuids.map(({ key }: Bucket) => key), status, requests_total: requestsTotal, concurrent_connections: connections, diff --git a/x-pack/plugins/monitoring/server/lib/kibana/index.js b/x-pack/plugins/monitoring/server/lib/kibana/index.ts similarity index 100% rename from x-pack/plugins/monitoring/server/lib/kibana/index.js rename to x-pack/plugins/monitoring/server/lib/kibana/index.ts diff --git a/x-pack/plugins/monitoring/server/lib/logs/detect_reason.js b/x-pack/plugins/monitoring/server/lib/logs/detect_reason.ts similarity index 75% rename from x-pack/plugins/monitoring/server/lib/logs/detect_reason.js rename to x-pack/plugins/monitoring/server/lib/logs/detect_reason.ts index a739a8d062c3e..216d29d841a86 100644 --- a/x-pack/plugins/monitoring/server/lib/logs/detect_reason.js +++ b/x-pack/plugins/monitoring/server/lib/logs/detect_reason.ts @@ -5,13 +5,21 @@ * 2.0. */ +import { LegacyRequest } from '../../types'; import { createTimeFilter } from '../create_query'; -import { get } from 'lodash'; + +interface Opts { + start: number; + end: number; + clusterUuid?: string; + nodeUuid?: string; + indexUuid?: string; +} async function doesFilebeatIndexExist( - req, - filebeatIndexPattern, - { start, end, clusterUuid, nodeUuid, indexUuid } + req: LegacyRequest, + filebeatIndexPattern: string, + { start, end, clusterUuid, nodeUuid, indexUuid }: Opts ) { const metric = { timestampField: '@timestamp' }; const filter = [createTimeFilter({ start, end, metric })]; @@ -122,18 +130,18 @@ async function doesFilebeatIndexExist( } = await callWithRequest(req, 'msearch', { body }); return { - indexPatternExists: get(indexPatternExistsResponse, 'hits.total.value', 0) > 0, + indexPatternExists: (indexPatternExistsResponse?.hits?.total.value ?? 0) > 0, indexPatternInTimeRangeExists: - get(indexPatternExistsInTimeRangeResponse, 'hits.total.value', 0) > 0, - typeExistsAtAnyTime: get(typeExistsAtAnyTimeResponse, 'hits.total.value', 0) > 0, - typeExists: get(typeExistsResponse, 'hits.total.value', 0) > 0, - usingStructuredLogs: get(usingStructuredLogsResponse, 'hits.total.value', 0) > 0, - clusterExists: clusterUuid ? get(clusterExistsResponse, 'hits.total.value', 0) > 0 : null, - nodeExists: nodeUuid ? get(nodeExistsResponse, 'hits.total.value', 0) > 0 : null, - indexExists: indexUuid ? get(indexExistsResponse, 'hits.total.value', 0) > 0 : null, + (indexPatternExistsInTimeRangeResponse?.hits?.total.value ?? 0) > 0, + typeExistsAtAnyTime: (typeExistsAtAnyTimeResponse?.hits?.total.value ?? 0) > 0, + typeExists: (typeExistsResponse?.hits?.total.value ?? 0) > 0, + usingStructuredLogs: (usingStructuredLogsResponse?.hits?.total.value ?? 0) > 0, + clusterExists: clusterUuid ? (clusterExistsResponse?.hits?.total.value ?? 0) > 0 : null, + nodeExists: nodeUuid ? (nodeExistsResponse?.hits?.total.value ?? 0) > 0 : null, + indexExists: indexUuid ? (indexExistsResponse?.hits?.total.value ?? 0) > 0 : null, }; } -export async function detectReason(req, filebeatIndexPattern, opts) { +export async function detectReason(req: LegacyRequest, filebeatIndexPattern: string, opts: Opts) { return await doesFilebeatIndexExist(req, filebeatIndexPattern, opts); } diff --git a/x-pack/plugins/monitoring/server/lib/logs/detect_reason_from_exception.js b/x-pack/plugins/monitoring/server/lib/logs/detect_reason_from_exception.ts similarity index 86% rename from x-pack/plugins/monitoring/server/lib/logs/detect_reason_from_exception.js rename to x-pack/plugins/monitoring/server/lib/logs/detect_reason_from_exception.ts index bd4776409299e..6d6ebf9b8dad2 100644 --- a/x-pack/plugins/monitoring/server/lib/logs/detect_reason_from_exception.js +++ b/x-pack/plugins/monitoring/server/lib/logs/detect_reason_from_exception.ts @@ -5,7 +5,7 @@ * 2.0. */ -export function detectReasonFromException(exception) { +export function detectReasonFromException(exception: Error & { status: number }) { const reason = { correctIndexName: true }; if (exception) { diff --git a/x-pack/plugins/monitoring/server/lib/logs/get_log_types.ts b/x-pack/plugins/monitoring/server/lib/logs/get_log_types.ts index bbb48c43033da..2346fdd6f4531 100644 --- a/x-pack/plugins/monitoring/server/lib/logs/get_log_types.ts +++ b/x-pack/plugins/monitoring/server/lib/logs/get_log_types.ts @@ -5,13 +5,9 @@ * 2.0. */ -// @ts-ignore import { checkParam } from '../error_missing_required'; -// @ts-ignore -import { createTimeFilter } from '../create_query'; -// @ts-ignore +import { createTimeFilter, TimerangeFilter } from '../create_query'; import { detectReason } from './detect_reason'; -// @ts-ignore import { detectReasonFromException } from './detect_reason_from_exception'; import { LegacyRequest } from '../../types'; import { FilebeatResponse } from '../../../common/types/filebeat'; @@ -25,7 +21,7 @@ async function handleResponse( response: FilebeatResponse, req: LegacyRequest, filebeatIndexPattern: string, - opts: { clusterUuid: string; nodeUuid: string; indexUuid: string; start: number; end: number } + opts: { clusterUuid?: string; nodeUuid?: string; indexUuid?: string; start: number; end: number } ) { const result: { enabled: boolean; types: LogType[]; reason?: any } = { enabled: false, @@ -62,12 +58,12 @@ export async function getLogTypes( indexUuid, start, end, - }: { clusterUuid: string; nodeUuid: string; indexUuid: string; start: number; end: number } + }: { clusterUuid?: string; nodeUuid?: string; indexUuid?: string; start: number; end: number } ) { checkParam(filebeatIndexPattern, 'filebeatIndexPattern in logs/getLogTypes'); const metric = { timestampField: '@timestamp' }; - const filter = [ + const filter: Array<{ term: { [x: string]: string } } | TimerangeFilter | null> = [ { term: { 'service.type': 'elasticsearch' } }, createTimeFilter({ start, end, metric }), ]; diff --git a/x-pack/plugins/monitoring/server/lib/logs/get_logs.ts b/x-pack/plugins/monitoring/server/lib/logs/get_logs.ts index 4c21422a5d0cf..5226c04377ffe 100644 --- a/x-pack/plugins/monitoring/server/lib/logs/get_logs.ts +++ b/x-pack/plugins/monitoring/server/lib/logs/get_logs.ts @@ -22,7 +22,7 @@ import { LegacyRequest } from '../../types'; import { FilebeatResponse } from '../../../common/types/filebeat'; interface Log { - timestamp?: string; + timestamp?: string | number; component?: string; node?: string; index?: string; @@ -83,7 +83,7 @@ export async function getLogs( checkParam(filebeatIndexPattern, 'filebeatIndexPattern in logs/getLogs'); const metric = { timestampField: '@timestamp' }; - const filter = [ + const filter: any[] = [ { term: { 'service.type': 'elasticsearch' } }, createTimeFilter({ start, end, metric }), ]; diff --git a/x-pack/plugins/monitoring/server/lib/logs/index.js b/x-pack/plugins/monitoring/server/lib/logs/index.ts similarity index 100% rename from x-pack/plugins/monitoring/server/lib/logs/index.js rename to x-pack/plugins/monitoring/server/lib/logs/index.ts diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.js b/x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.ts similarity index 94% rename from x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.js rename to x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.ts index 17f76834b333a..c0c29756818ee 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.ts @@ -7,14 +7,15 @@ import Bluebird from 'bluebird'; import { get } from 'lodash'; +import { LegacyRequest, Cluster, Bucket } from '../../types'; +import { LOGSTASH } from '../../../common/constants'; import { checkParam } from '../error_missing_required'; -import { createQuery } from '../create_query.js'; +import { createQuery } from '../create_query'; import { LogstashClusterMetric } from '../metrics'; -import { LOGSTASH } from '../../../common/constants'; const { MEMORY, PERSISTED } = LOGSTASH.QUEUE_TYPES; -const getQueueTypes = (queueBuckets) => { +const getQueueTypes = (queueBuckets: Array) => { const memory = queueBuckets.find((bucket) => bucket.key === MEMORY); const persisted = queueBuckets.find((bucket) => bucket.key === PERSISTED); return { @@ -36,7 +37,11 @@ const getQueueTypes = (queueBuckets) => { * - number of instances * - combined health */ -export function getLogstashForClusters(req, lsIndexPattern, clusters) { +export function getLogstashForClusters( + req: LegacyRequest, + lsIndexPattern: string, + clusters: Cluster[] +) { checkParam(lsIndexPattern, 'lsIndexPattern in logstash/getLogstashForClusters'); const start = req.payload.timeRange.min; @@ -226,7 +231,7 @@ export function getLogstashForClusters(req, lsIndexPattern, clusters) { let types = get(aggregations, 'pipelines_nested_mb.queue_types.buckets', []); if (!types || types.length === 0) { - types = get(aggregations, 'pipelines_nested.queue_types.buckets', []); + types = aggregations.pipelines_nested?.queue_types.buckets ?? []; } return { @@ -242,7 +247,7 @@ export function getLogstashForClusters(req, lsIndexPattern, clusters) { get(aggregations, 'pipelines_nested_mb.pipelines.value') || get(aggregations, 'pipelines_nested.pipelines.value', 0), queue_types: getQueueTypes(types), - versions: logstashVersions.map((versionBucket) => versionBucket.key), + versions: logstashVersions.map((versionBucket: Bucket) => versionBucket.key), }, }; }); diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.ts index d047729a0b3c2..276b8b119bba3 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_node_info.ts @@ -7,7 +7,7 @@ import { merge } from 'lodash'; // @ts-ignore -import { checkParam } from '../error_missing_required'; +import { checkParam, MissingRequiredError } from '../error_missing_required'; // @ts-ignore import { calculateAvailability } from '../calculate_availability'; import { LegacyRequest } from '../../types'; @@ -20,8 +20,12 @@ export function handleResponse(resp: ElasticsearchResponse) { const legacyStats = resp.hits?.hits[0]?._source?.logstash_stats; const mbStats = resp.hits?.hits[0]?._source?.logstash?.node?.stats; const logstash = mbStats?.logstash ?? legacyStats?.logstash; + const availabilityTimestamp = mbStats?.timestamp ?? legacyStats?.timestamp; + if (!availabilityTimestamp) { + throw new MissingRequiredError('timestamp'); + } const info = merge(logstash, { - availability: calculateAvailability(mbStats?.timestamp ?? legacyStats?.timestamp), + availability: calculateAvailability(availabilityTimestamp), events: mbStats?.events ?? legacyStats?.events, reloads: mbStats?.reloads ?? legacyStats?.reloads, queue_type: mbStats?.queue?.type ?? legacyStats?.queue?.type, diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline.ts index b17f7d27c6c9b..d8bfd91a4aec8 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline.ts @@ -132,9 +132,9 @@ export async function getPipeline( // Determine metrics' timeseries interval based on version's timespan const minIntervalSeconds = config.get('monitoring.ui.min_interval_seconds'); const timeseriesInterval = calculateTimeseriesInterval( - version.firstSeen, - version.lastSeen, - minIntervalSeconds + Number(version.firstSeen), + Number(version.lastSeen), + Number(minIntervalSeconds) ); const [stateDocument, statsAggregation] = await Promise.all([ diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_ids.js b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_ids.ts similarity index 90% rename from x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_ids.js rename to x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_ids.ts index 2846a968bfed6..1a5595d45ffbb 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_ids.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_ids.ts @@ -7,14 +7,15 @@ import moment from 'moment'; import { get } from 'lodash'; +import { LegacyRequest, Bucket } from '../../types'; import { createQuery } from '../create_query'; import { LogstashMetric } from '../metrics'; export async function getLogstashPipelineIds( - req, - logstashIndexPattern, - { clusterUuid, logstashUuid }, - size + req: LegacyRequest, + logstashIndexPattern: string, + { clusterUuid, logstashUuid }: { clusterUuid: string; logstashUuid?: string }, + size: number ) { const start = moment.utc(req.payload.timeRange.min).valueOf(); const end = moment.utc(req.payload.timeRange.max).valueOf(); @@ -100,14 +101,14 @@ export async function getLogstashPipelineIds( if (!buckets || buckets.length === 0) { buckets = get(response, 'aggregations.nest.id.buckets', []); } - return buckets.map((bucket) => { + return buckets.map((bucket: Bucket) => { let nodeBuckets = get(bucket, 'unnest_mb.nodes.buckets', []); if (!nodeBuckets || nodeBuckets.length === 0) { nodeBuckets = get(bucket, 'unnest.nodes.buckets', []); } return { id: bucket.key, - nodeIds: nodeBuckets.map((item) => item.key), + nodeIds: nodeBuckets.map((item: Bucket) => item.key), }; }); } diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex.ts b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex.ts index 3d657e8344e3c..e41eea0bce64a 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex.ts +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_pipeline_vertex.ts @@ -150,9 +150,9 @@ export async function getPipelineVertex( // Determine metrics' timeseries interval based on version's timespan const minIntervalSeconds = config.get('monitoring.ui.min_interval_seconds'); const timeseriesInterval = calculateTimeseriesInterval( - version.firstSeen, - version.lastSeen, - minIntervalSeconds + Number(version.firstSeen), + Number(version.lastSeen), + Number(minIntervalSeconds) ); const [stateDocument, statsAggregation] = await Promise.all([ diff --git a/x-pack/plugins/monitoring/server/lib/logstash/index.js b/x-pack/plugins/monitoring/server/lib/logstash/index.ts similarity index 100% rename from x-pack/plugins/monitoring/server/lib/logstash/index.js rename to x-pack/plugins/monitoring/server/lib/logstash/index.ts diff --git a/x-pack/plugins/monitoring/server/lib/metrics/apm/classes.js b/x-pack/plugins/monitoring/server/lib/metrics/apm/classes.ts similarity index 91% rename from x-pack/plugins/monitoring/server/lib/metrics/apm/classes.js rename to x-pack/plugins/monitoring/server/lib/metrics/apm/classes.ts index 638d14594699f..4a97e7823999b 100644 --- a/x-pack/plugins/monitoring/server/lib/metrics/apm/classes.js +++ b/x-pack/plugins/monitoring/server/lib/metrics/apm/classes.ts @@ -5,12 +5,16 @@ * 2.0. */ +/* eslint-disable max-classes-per-file */ + +import { i18n } from '@kbn/i18n'; +// @ts-ignore import { ClusterMetric, Metric } from '../classes'; import { SMALL_FLOAT, LARGE_FLOAT } from '../../../../common/formatting'; -import { i18n } from '@kbn/i18n'; import { NORMALIZED_DERIVATIVE_UNIT } from '../../../../common/constants'; export class ApmClusterMetric extends ClusterMetric { + // @ts-ignore constructor(opts) { super({ ...opts, @@ -28,6 +32,7 @@ export class ApmClusterMetric extends ClusterMetric { } export class ApmMetric extends Metric { + // @ts-ignore constructor(opts) { super({ ...opts, @@ -44,7 +49,10 @@ export class ApmMetric extends Metric { } } +export type ApmMetricFields = ReturnType; + export class ApmCpuUtilizationMetric extends ApmMetric { + // @ts-ignore constructor(opts) { super({ ...opts, @@ -57,6 +65,7 @@ export class ApmCpuUtilizationMetric extends ApmMetric { /* * Convert a counter of milliseconds of utilization time into a percentage of the bucket size */ + // @ts-ignore this.calculation = ({ metric_deriv: metricDeriv } = {}, _key, _metric, bucketSizeInSeconds) => { if (metricDeriv) { const { value: metricDerivValue } = metricDeriv; @@ -72,6 +81,7 @@ export class ApmCpuUtilizationMetric extends ApmMetric { } export class ApmEventsRateClusterMetric extends ApmClusterMetric { + // @ts-ignore constructor(opts) { super({ ...opts, @@ -83,6 +93,7 @@ export class ApmEventsRateClusterMetric extends ApmClusterMetric { }), }); + // @ts-ignore this.aggs = { beats_uuids: { terms: { @@ -92,6 +103,7 @@ export class ApmEventsRateClusterMetric extends ApmClusterMetric { aggs: { event_rate_per_beat: { max: { + // @ts-ignore field: this.field, }, }, diff --git a/x-pack/plugins/monitoring/server/lib/metrics/beats/classes.js b/x-pack/plugins/monitoring/server/lib/metrics/beats/classes.ts similarity index 91% rename from x-pack/plugins/monitoring/server/lib/metrics/beats/classes.js rename to x-pack/plugins/monitoring/server/lib/metrics/beats/classes.ts index 4af71d2fd4bd7..78e731cee8881 100644 --- a/x-pack/plugins/monitoring/server/lib/metrics/beats/classes.js +++ b/x-pack/plugins/monitoring/server/lib/metrics/beats/classes.ts @@ -5,16 +5,20 @@ * 2.0. */ +/* eslint-disable max-classes-per-file */ + +import { i18n } from '@kbn/i18n'; +// @ts-ignore import { ClusterMetric, Metric } from '../classes'; import { SMALL_FLOAT, LARGE_FLOAT, LARGE_BYTES } from '../../../../common/formatting'; import { NORMALIZED_DERIVATIVE_UNIT } from '../../../../common/constants'; -import { i18n } from '@kbn/i18n'; const perSecondUnitLabel = i18n.translate('xpack.monitoring.metrics.beats.perSecondUnitLabel', { defaultMessage: '/s', }); export class BeatsClusterMetric extends ClusterMetric { + // @ts-ignore constructor(opts) { super({ ...opts, @@ -32,6 +36,7 @@ export class BeatsClusterMetric extends ClusterMetric { } export class BeatsEventsRateClusterMetric extends BeatsClusterMetric { + // @ts-ignore constructor(opts) { super({ ...opts, @@ -40,6 +45,7 @@ export class BeatsEventsRateClusterMetric extends BeatsClusterMetric { metricAgg: 'max', units: perSecondUnitLabel, }); + // @ts-ignore this.aggs = { beats_uuids: { @@ -50,6 +56,7 @@ export class BeatsEventsRateClusterMetric extends BeatsClusterMetric { aggs: { event_rate_per_beat: { max: { + // @ts-ignore field: this.field, }, }, @@ -73,6 +80,7 @@ export class BeatsEventsRateClusterMetric extends BeatsClusterMetric { } export class BeatsMetric extends Metric { + // @ts-ignore constructor(opts) { super({ ...opts, @@ -89,7 +97,10 @@ export class BeatsMetric extends Metric { } } +export type BeatsMetricFields = ReturnType; + export class BeatsByteRateClusterMetric extends BeatsEventsRateClusterMetric { + // @ts-ignore constructor(opts) { super({ ...opts, @@ -99,6 +110,7 @@ export class BeatsByteRateClusterMetric extends BeatsEventsRateClusterMetric { } export class BeatsEventsRateMetric extends BeatsMetric { + // @ts-ignore constructor(opts) { super({ ...opts, @@ -111,6 +123,7 @@ export class BeatsEventsRateMetric extends BeatsMetric { } export class BeatsByteRateMetric extends BeatsMetric { + // @ts-ignore constructor(opts) { super({ ...opts, @@ -123,6 +136,7 @@ export class BeatsByteRateMetric extends BeatsMetric { } export class BeatsCpuUtilizationMetric extends BeatsMetric { + // @ts-ignore constructor(opts) { super({ ...opts, @@ -135,6 +149,7 @@ export class BeatsCpuUtilizationMetric extends BeatsMetric { /* * Convert a counter of milliseconds of utilization time into a percentage of the bucket size */ + // @ts-ignore this.calculation = ({ metric_deriv: metricDeriv } = {}, _key, _metric, bucketSizeInSeconds) => { if (metricDeriv) { const { value } = metricDeriv; diff --git a/x-pack/plugins/monitoring/server/lib/metrics/index.js b/x-pack/plugins/monitoring/server/lib/metrics/index.ts similarity index 69% rename from x-pack/plugins/monitoring/server/lib/metrics/index.js rename to x-pack/plugins/monitoring/server/lib/metrics/index.ts index c2b24f653bc9d..ba43a8c316d3b 100644 --- a/x-pack/plugins/monitoring/server/lib/metrics/index.js +++ b/x-pack/plugins/monitoring/server/lib/metrics/index.ts @@ -5,9 +5,13 @@ * 2.0. */ +// @ts-ignore export { ElasticsearchMetric } from './elasticsearch/classes'; +// @ts-ignore export { KibanaClusterMetric, KibanaMetric } from './kibana/classes'; -export { ApmMetric, ApmClusterMetric } from './apm/classes'; +export { ApmMetric, ApmClusterMetric, ApmMetricFields } from './apm/classes'; +// @ts-ignore export { LogstashClusterMetric, LogstashMetric } from './logstash/classes'; -export { BeatsClusterMetric, BeatsMetric } from './beats/classes'; +export { BeatsClusterMetric, BeatsMetric, BeatsMetricFields } from './beats/classes'; +// @ts-ignore export { metrics } from './metrics'; diff --git a/x-pack/plugins/monitoring/server/lib/normalize_version_string.js b/x-pack/plugins/monitoring/server/lib/normalize_version_string.ts similarity index 90% rename from x-pack/plugins/monitoring/server/lib/normalize_version_string.js rename to x-pack/plugins/monitoring/server/lib/normalize_version_string.ts index 359db54ae9661..5d49304414846 100644 --- a/x-pack/plugins/monitoring/server/lib/normalize_version_string.js +++ b/x-pack/plugins/monitoring/server/lib/normalize_version_string.ts @@ -6,7 +6,7 @@ */ import { escape } from 'lodash'; -export function normalizeVersionString(string) { +export function normalizeVersionString(string: string) { if (string) { // get just the number.number.number portion (filter out '-snapshot') const matches = string.match(/^\d+\.\d+.\d+/); diff --git a/x-pack/plugins/monitoring/server/lib/standalone_clusters/index.js b/x-pack/plugins/monitoring/server/lib/standalone_clusters/index.ts similarity index 92% rename from x-pack/plugins/monitoring/server/lib/standalone_clusters/index.js rename to x-pack/plugins/monitoring/server/lib/standalone_clusters/index.ts index f15d3819fb03e..edd18710b940b 100644 --- a/x-pack/plugins/monitoring/server/lib/standalone_clusters/index.js +++ b/x-pack/plugins/monitoring/server/lib/standalone_clusters/index.ts @@ -5,6 +5,9 @@ * 2.0. */ +// @ts-ignore export { hasStandaloneClusters } from './has_standalone_clusters'; +// @ts-ignore export { getStandaloneClusterDefinition } from './get_standalone_cluster_definition'; +// @ts-ignore export { standaloneClusterFilter } from './standalone_cluster_query_filter'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts index f77630e5d61a5..27b21c342f037 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts @@ -41,7 +41,6 @@ export function alertStatusRoute(server: any, npRoute: RouteDependencies) { const status = await fetchStatus( rulesClient, - npRoute.licenseService, alertTypeIds, [clusterUuid], filters as CommonAlertFilter[] diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.js b/x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.ts similarity index 89% rename from x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.js rename to x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.ts index e6de6ca984cb1..2629d763d656d 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.ts @@ -7,10 +7,12 @@ import { schema } from '@kbn/config-schema'; import { getClustersFromRequest } from '../../../../lib/cluster/get_clusters_from_request'; +// @ts-ignore import { handleError } from '../../../../lib/errors'; import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns'; +import { LegacyRequest, LegacyServer } from '../../../../types'; -export function clusterRoute(server) { +export function clusterRoute(server: LegacyServer) { /* * Cluster Overview */ @@ -32,11 +34,11 @@ export function clusterRoute(server) { }), }, }, - handler: async (req) => { + handler: async (req: LegacyRequest) => { const config = server.config(); const indexPatterns = getIndexPatterns(server, { - filebeatIndexPattern: config.get('monitoring.ui.logs.index'), + filebeatIndexPattern: config.get('monitoring.ui.logs.index')!, }); const options = { clusterUuid: req.params.clusterUuid, diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts index c2bad7b905c5b..d05d60866d119 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts @@ -17,7 +17,7 @@ import { import { prefixIndexPattern } from '../../../../../lib/ccs_utils'; // @ts-ignore import { handleError } from '../../../../../lib/errors'; -import { RouteDependencies } from '../../../../../types'; +import { RouteDependencies, LegacyServer } from '../../../../../types'; const queryBody = { size: 0, @@ -70,10 +70,7 @@ const checkLatestMonitoringIsLegacy = async (context: RequestHandlerContext, ind return counts; }; -export function internalMonitoringCheckRoute( - server: { config: () => unknown }, - npRoute: RouteDependencies -) { +export function internalMonitoringCheckRoute(server: LegacyServer, npRoute: RouteDependencies) { npRoute.router.post( { path: '/api/monitoring/v1/elasticsearch_settings/check/internal_monitoring', diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index 07e56f0c00232..a057b72d89b29 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -30,6 +30,7 @@ import { LicensingPluginStart } from '../../licensing/server'; import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; import { EncryptedSavedObjectsPluginSetup } from '../../encrypted_saved_objects/server'; import { CloudSetup } from '../../cloud/server'; +import { ElasticsearchModifiedSource } from '../common/types/es'; export interface MonitoringLicenseService { refresh: () => Promise; @@ -146,3 +147,20 @@ export interface LegacyServer { }; }; } + +export type Cluster = ElasticsearchModifiedSource & { + ml?: { jobs: any }; + logs?: any; + alerts?: any; +}; + +export interface Bucket { + key: string; + uuids: { + buckets: unknown[]; + }; +} + +export interface Aggregation { + buckets: Bucket[]; +} diff --git a/x-pack/plugins/observability/kibana.json b/x-pack/plugins/observability/kibana.json index f4b3754e4253e..b794f91231505 100644 --- a/x-pack/plugins/observability/kibana.json +++ b/x-pack/plugins/observability/kibana.json @@ -1,18 +1,13 @@ { "id": "observability", + "owner": { + "name": "Observability UI", + "gitHubTeam": "observability-ui" + }, "version": "8.0.0", "kibanaVersion": "kibana", - "configPath": [ - "xpack", - "observability" - ], - "optionalPlugins": [ - "home", - "discover", - "lens", - "licensing", - "usageCollection" - ], + "configPath": ["xpack", "observability"], + "optionalPlugins": ["home", "discover", "lens", "licensing", "usageCollection"], "requiredPlugins": [ "alerting", "cases", @@ -24,9 +19,5 @@ ], "ui": true, "server": true, - "requiredBundles": [ - "data", - "kibanaReact", - "kibanaUtils" - ] + "requiredBundles": ["data", "kibanaReact", "kibanaUtils"] } diff --git a/x-pack/plugins/observability/public/components/app/resources/index.test.tsx b/x-pack/plugins/observability/public/components/app/resources/index.test.tsx index 7a1b84c04c035..e5a8bd3d097fc 100644 --- a/x-pack/plugins/observability/public/components/app/resources/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/resources/index.test.tsx @@ -13,7 +13,8 @@ describe('Resources', () => { it('renders resources with all elements', () => { const { getByText } = render(); expect(getByText('Documentation')).toBeInTheDocument(); - expect(getByText('Discuss forum')).toBeInTheDocument(); - expect(getByText('Observability fundamentals')).toBeInTheDocument(); + expect(getByText('Discuss Forum')).toBeInTheDocument(); + expect(getByText('Quick Start Videos')).toBeInTheDocument(); + expect(getByText('Free Observability Course')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/observability/public/components/app/resources/index.tsx b/x-pack/plugins/observability/public/components/app/resources/index.tsx index 1a0c473734993..dfb50c9ad825f 100644 --- a/x-pack/plugins/observability/public/components/app/resources/index.tsx +++ b/x-pack/plugins/observability/public/components/app/resources/index.tsx @@ -20,14 +20,21 @@ const resources = [ { iconType: 'editorComment', label: i18n.translate('xpack.observability.resources.forum', { - defaultMessage: 'Discuss forum', + defaultMessage: 'Discuss Forum', }), href: 'https://ela.st/observability-discuss', }, + { + iconType: 'play', + label: i18n.translate('xpack.observability.resources.quick_start', { + defaultMessage: 'Quick Start Videos', + }), + href: 'https://ela.st/observability-quick-starts', + }, { iconType: 'training', label: i18n.translate('xpack.observability.resources.training', { - defaultMessage: 'Observability fundamentals', + defaultMessage: 'Free Observability Course', }), href: 'https://ela.st/observability-training', }, diff --git a/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx b/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx index 82392b5c23bf9..9e7b96b02206f 100644 --- a/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx +++ b/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx @@ -28,7 +28,7 @@ export function buildFilterLabel({ const filter = value instanceof Array && value.length > 1 ? esFilters.buildPhrasesFilter(indexField, value, indexPattern) - : esFilters.buildPhraseFilter(indexField, value, indexPattern); + : esFilters.buildPhraseFilter(indexField, value as string, indexPattern); filter.meta.type = value instanceof Array && value.length > 1 ? 'phrases' : 'phrase'; diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx index bd6844915459c..77c2b5cbca0cf 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx @@ -120,7 +120,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { const leadingControlColumns = [ { id: 'expand', - width: 20, + width: 40, headerCellRender: () => { return ( @@ -149,7 +149,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { }, { id: 'view_in_app', - width: 20, + width: 40, headerCellRender: () => null, rowCellRender: ({ data }: ActionProps) => { const dataFieldEs = data.reduce((acc, d) => ({ ...acc, [d.field]: d.value }), {}); diff --git a/x-pack/plugins/reporting/common/constants.ts b/x-pack/plugins/reporting/common/constants.ts index c9a763fae52fe..be543b3908b68 100644 --- a/x-pack/plugins/reporting/common/constants.ts +++ b/x-pack/plugins/reporting/common/constants.ts @@ -7,8 +7,6 @@ export const PLUGIN_ID = 'reporting'; -export const BROWSER_TYPE = 'chromium'; - export const JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY = 'xpack.reporting.jobCompletionNotifications'; @@ -93,13 +91,10 @@ export const API_DIAGNOSE_URL = `${API_BASE_URL}/diagnose`; export const API_GET_ILM_POLICY_STATUS = `${API_BASE_URL}/ilm_policy_status`; export const API_MIGRATE_ILM_POLICY_URL = `${API_BASE_URL}/deprecations/migrate_ilm_policy`; +export const API_BASE_URL_V1 = '/api/reporting/v1'; // export const ILM_POLICY_NAME = 'kibana-reporting'; -// hacky endpoint: download CSV without queueing a report -export const API_BASE_URL_V1 = '/api/reporting/v1'; // -export const API_GENERATE_IMMEDIATE = `${API_BASE_URL_V1}/generate/immediate/csv_searchsource`; - // Management UI route export const REPORTING_MANAGEMENT_HOME = '/app/management/insightsAndAlerting/reporting'; @@ -109,6 +104,18 @@ export enum JOB_STATUSES { PROCESSING = 'processing', COMPLETED = 'completed', FAILED = 'failed', - CANCELLED = 'cancelled', WARNINGS = 'completed_with_warnings', } + +// Test Subjects +export const REPORT_TABLE_ID = 'reportJobListing'; +export const REPORT_TABLE_ROW_ID = 'reportJobRow'; + +// Job params require a `version` field as of 7.15.0. For older jobs set with +// automation that have no version value in the job params, we assume the +// intended version is 7.14.0 +export const UNVERSIONED_VERSION = '7.14.0'; + +// hacky endpoint: download CSV without queueing a report +// FIXME: find a way to make these endpoints "generic" instead of hardcoded, as are the queued report export types +export const API_GENERATE_IMMEDIATE = `${API_BASE_URL_V1}/generate/immediate/csv_searchsource`; diff --git a/x-pack/plugins/reporting/common/types.ts b/x-pack/plugins/reporting/common/types.ts index 308245a696d92..f3a0e9192cf7d 100644 --- a/x-pack/plugins/reporting/common/types.ts +++ b/x-pack/plugins/reporting/common/types.ts @@ -50,7 +50,6 @@ export interface TaskRunResult { size: number; csv_contains_formulas?: boolean; max_size_reached?: boolean; - needs_sorting?: boolean; warnings?: string[]; } @@ -98,10 +97,11 @@ export interface ReportDocument extends ReportDocumentHead { } export interface BaseParams { - browserTimezone?: string; // browserTimezone is optional: it is not in old POST URLs that were generated prior to being added to this interface layout?: LayoutParams; objectType: string; title: string; + browserTimezone: string; // to format dates in the user's time zone + version: string; // to handle any state migrations } export type JobId = string; @@ -120,8 +120,10 @@ export type JobStatus = | 'processing' // Report job has been claimed and is executing | 'failed'; // Report was not successful, and all retries are done. Nothing to download. +// payload for retrieving the error message of a failed job export interface JobContent { - content: string; + content: TaskRunResult['content']; + content_type: false; } /* diff --git a/x-pack/plugins/reporting/public/lib/__snapshots__/stream_handler.test.ts.snap b/x-pack/plugins/reporting/public/lib/__snapshots__/stream_handler.test.ts.snap index f1d9d747a7236..3d49e8e695f9b 100644 --- a/x-pack/plugins/reporting/public/lib/__snapshots__/stream_handler.test.ts.snap +++ b/x-pack/plugins/reporting/public/lib/__snapshots__/stream_handler.test.ts.snap @@ -86,7 +86,7 @@ Array [ size="m" title="The reporting job failed" > - this is the completed report data + this is the failed report error

diff --git a/x-pack/plugins/reporting/public/lib/job.ts b/x-pack/plugins/reporting/public/lib/job.ts deleted file mode 100644 index c882e8b92986b..0000000000000 --- a/x-pack/plugins/reporting/public/lib/job.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { JobId, ReportApiJSON, ReportSource, TaskRunResult } from '../../common/types'; - -type ReportPayload = ReportSource['payload']; - -/* - * This class represents a report job for the UI - * It can be instantiated with ReportApiJSON: the response data format for the report job APIs - */ -export class Job { - public id: JobId; - public index: string; - - public objectType: ReportPayload['objectType']; - public title: ReportPayload['title']; - public isDeprecated: ReportPayload['isDeprecated']; - public browserTimezone?: ReportPayload['browserTimezone']; - public layout: ReportPayload['layout']; - - public jobtype: ReportSource['jobtype']; - public created_by: ReportSource['created_by']; - public created_at: ReportSource['created_at']; - public started_at: ReportSource['started_at']; - public completed_at: ReportSource['completed_at']; - public status: ReportSource['status']; - public attempts: ReportSource['attempts']; - public max_attempts: ReportSource['max_attempts']; - - public timeout: ReportSource['timeout']; - public kibana_name: ReportSource['kibana_name']; - public kibana_id: ReportSource['kibana_id']; - public browser_type: ReportSource['browser_type']; - - public size?: TaskRunResult['size']; - public content_type?: TaskRunResult['content_type']; - public csv_contains_formulas?: TaskRunResult['csv_contains_formulas']; - public max_size_reached?: TaskRunResult['max_size_reached']; - public warnings?: TaskRunResult['warnings']; - - constructor(report: ReportApiJSON) { - this.id = report.id; - this.index = report.index; - - this.jobtype = report.jobtype; - this.objectType = report.payload.objectType; - this.title = report.payload.title; - this.layout = report.payload.layout; - this.created_by = report.created_by; - this.created_at = report.created_at; - this.started_at = report.started_at; - this.completed_at = report.completed_at; - this.status = report.status; - this.attempts = report.attempts; - this.max_attempts = report.max_attempts; - - this.timeout = report.timeout; - this.kibana_name = report.kibana_name; - this.kibana_id = report.kibana_id; - this.browser_type = report.browser_type; - this.browserTimezone = report.payload.browserTimezone; - this.size = report.output?.size; - this.content_type = report.output?.content_type; - - this.isDeprecated = report.payload.isDeprecated || false; - this.csv_contains_formulas = report.output?.csv_contains_formulas; - this.max_size_reached = report.output?.max_size_reached; - this.warnings = report.output?.warnings; - } -} diff --git a/x-pack/plugins/reporting/public/lib/job.tsx b/x-pack/plugins/reporting/public/lib/job.tsx new file mode 100644 index 0000000000000..96967dc9226c9 --- /dev/null +++ b/x-pack/plugins/reporting/public/lib/job.tsx @@ -0,0 +1,269 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiText, EuiTextColor } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import moment from 'moment'; +import React from 'react'; +import { JOB_STATUSES } from '../../common/constants'; +import { JobId, ReportApiJSON, ReportSource, TaskRunResult } from '../../common/types'; + +const { COMPLETED, FAILED, PENDING, PROCESSING, WARNINGS } = JOB_STATUSES; + +type ReportPayload = ReportSource['payload']; + +/* + * This class represents a report job for the UI + * It can be instantiated with ReportApiJSON: the response data format for the report job APIs + */ +export class Job { + public id: JobId; + public index: string; + + public objectType: ReportPayload['objectType']; + public title: ReportPayload['title']; + public isDeprecated: ReportPayload['isDeprecated']; + public browserTimezone?: ReportPayload['browserTimezone']; + public layout: ReportPayload['layout']; + + public jobtype: ReportSource['jobtype']; + public created_by: ReportSource['created_by']; + public created_at: ReportSource['created_at']; + public started_at: ReportSource['started_at']; + public completed_at: ReportSource['completed_at']; + public status: JOB_STATUSES; // FIXME: can not use ReportSource['status'] due to type mismatch + public attempts: ReportSource['attempts']; + public max_attempts: ReportSource['max_attempts']; + + public timeout: ReportSource['timeout']; + public kibana_name: ReportSource['kibana_name']; + public kibana_id: ReportSource['kibana_id']; + public browser_type: ReportSource['browser_type']; + + public size?: TaskRunResult['size']; + public content_type?: TaskRunResult['content_type']; + public csv_contains_formulas?: TaskRunResult['csv_contains_formulas']; + public max_size_reached?: TaskRunResult['max_size_reached']; + public warnings?: TaskRunResult['warnings']; + + constructor(report: ReportApiJSON) { + this.id = report.id; + this.index = report.index; + + this.jobtype = report.jobtype; + this.objectType = report.payload.objectType; + this.title = report.payload.title; + this.layout = report.payload.layout; + this.created_by = report.created_by; + this.created_at = report.created_at; + this.started_at = report.started_at; + this.completed_at = report.completed_at; + this.status = report.status as JOB_STATUSES; + this.attempts = report.attempts; + this.max_attempts = report.max_attempts; + + this.timeout = report.timeout; + this.kibana_name = report.kibana_name; + this.kibana_id = report.kibana_id; + this.browser_type = report.browser_type; + this.browserTimezone = report.payload.browserTimezone; + this.size = report.output?.size; + this.content_type = report.output?.content_type; + + this.isDeprecated = report.payload.isDeprecated || false; + this.csv_contains_formulas = report.output?.csv_contains_formulas; + this.max_size_reached = report.output?.max_size_reached; + this.warnings = report.output?.warnings; + } + + getStatusMessage() { + const status = this.status; + let smallMessage; + if (status === PENDING) { + smallMessage = i18n.translate('xpack.reporting.jobStatusDetail.pendingStatusReachedText', { + defaultMessage: 'Waiting for job to be processed.', + }); + } else if (status === PROCESSING) { + smallMessage = i18n.translate('xpack.reporting.jobStatusDetail.attemptXofY', { + defaultMessage: 'Attempt {attempts} of {max_attempts}.', + values: { attempts: this.attempts, max_attempts: this.max_attempts }, + }); + } else if (this.getWarnings()) { + smallMessage = i18n.translate('xpack.reporting.jobStatusDetail.warningsText', { + defaultMessage: 'See report info for warnings.', + }); + } else if (this.getError()) { + smallMessage = i18n.translate('xpack.reporting.jobStatusDetail.errorText', { + defaultMessage: 'See report info for error details.', + }); + } + + if (smallMessage) { + return ( + + {smallMessage} + + ); + } + + return null; + } + + getStatus() { + const statusLabel = jobStatusLabelsMap.get(this.status) as string; + const statusTimestamp = this.getStatusTimestamp(); + + if (statusTimestamp) { + return ( + {this.formatDate(statusTimestamp)} + ), + }} + /> + ); + } + + return statusLabel; + } + + getStatusLabel() { + return ( + <> + {this.getStatus()} + {this.getStatusMessage()} + + ); + } + + getCreatedAtLabel() { + if (this.created_by) { + return ( + <> +

{this.formatDate(this.created_at)}
+ {this.created_by} + + ); + } + return this.formatDate(this.created_at); + } + + /* + * We use `output.warnings` to show the error of a failed report job, + * and to show warnings of a job that completed with warnings. + */ + + // There is no error unless the status is 'failed' + getError() { + if (this.status === FAILED) { + return this.warnings; + } + } + + getWarnings() { + if (this.status !== FAILED) { + const warnings: string[] = []; + if (this.isDeprecated) { + warnings.push( + i18n.translate('xpack.reporting.jobWarning.exportTypeDeprecated', { + defaultMessage: + 'This is a deprecated export type. Automation of this report will need to be re-created for compatibility with future versions of Kibana.', + }) + ); + } + if (this.csv_contains_formulas) { + warnings.push( + i18n.translate('xpack.reporting.jobWarning.csvContainsFormulas', { + defaultMessage: + 'Your CSV contains characters which spreadsheet applications can interpret as formulas.', + }) + ); + } + if (this.max_size_reached) { + warnings.push( + i18n.translate('xpack.reporting.jobWarning.maxSizeReachedTooltip', { + defaultMessage: 'Max size reached, contains partial data.', + }) + ); + } + + if (this.warnings?.length) { + warnings.push(...this.warnings); + } + + if (warnings.length) { + return ( +
    + {warnings.map((w, i) => { + return
  • {w}
  • ; + })} +
+ ); + } + } + } + + private formatDate(timestamp: string) { + try { + return moment(timestamp).format('YYYY-MM-DD @ hh:mm A'); + } catch (error) { + // ignore parse error and display unformatted value + return timestamp; + } + } + + private getStatusTimestamp() { + const status = this.status; + if (status === PROCESSING && this.started_at) { + return this.started_at; + } + + if (this.completed_at && ([COMPLETED, FAILED, WARNINGS] as string[]).includes(status)) { + return this.completed_at; + } + + return this.created_at; + } +} + +const jobStatusLabelsMap = new Map([ + [ + PENDING, + i18n.translate('xpack.reporting.jobStatuses.pendingText', { + defaultMessage: 'Pending', + }), + ], + [ + PROCESSING, + i18n.translate('xpack.reporting.jobStatuses.processingText', { + defaultMessage: 'Processing', + }), + ], + [ + COMPLETED, + i18n.translate('xpack.reporting.jobStatuses.completedText', { + defaultMessage: 'Completed', // NOTE: a job is `completed` not `completed_with_warings` if it has reached max size or possibly contains csv characters + }), + ], + [ + WARNINGS, + i18n.translate('xpack.reporting.jobStatuses.warningText', { + defaultMessage: 'Completed', + }), + ], + [ + FAILED, + i18n.translate('xpack.reporting.jobStatuses.failedText', { + defaultMessage: 'Failed', + }), + ], +]); diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts index 90411884332c8..5c618ba8261fa 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts @@ -5,24 +5,35 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; import { stringify } from 'query-string'; -import rison from 'rison-node'; -import { HttpSetup } from 'src/core/public'; +import rison, { RisonObject } from 'rison-node'; +import { HttpSetup, IUiSettingsClient } from 'src/core/public'; import { API_BASE_GENERATE, API_BASE_URL, + API_GENERATE_IMMEDIATE, API_LIST_URL, API_MIGRATE_ILM_POLICY_URL, REPORTING_MANAGEMENT_HOME, } from '../../../common/constants'; -import { DownloadReportFn, JobId, ManagementLinkFn, ReportApiJSON } from '../../../common/types'; +import { + BaseParams, + DownloadReportFn, + JobId, + ManagementLinkFn, + ReportApiJSON, +} from '../../../common/types'; import { add } from '../../notifier/job_completion_notifications'; import { Job } from '../job'; -export interface JobContent { - content: string; - content_type: boolean; -} +/* + * For convenience, apps do not have to provide the browserTimezone and Kibana version. + * Those fields are added in this client as part of the service. + * TODO: export a type like this to other plugins: https://github.com/elastic/kibana/issues/107085 + */ +type AppParams = Omit; export interface DiagnoseResponse { help: string[]; @@ -30,14 +41,10 @@ export interface DiagnoseResponse { logs: string; } -interface JobParams { - [paramName: string]: any; -} - interface IReportingAPI { // Helpers getReportURL(jobId: string): string; - getReportingJobPath(exportType: string, jobParams: JobParams): string; // Return a URL to queue a job, with the job params encoded in the query string of the URL. Used for copying POST URL + getReportingJobPath(exportType: string, jobParams: BaseParams & T): string; // Return a URL to queue a job, with the job params encoded in the query string of the URL. Used for copying POST URL createReportingJob(exportType: string, jobParams: any): Promise; // Sends a request to queue a job, with the job params in the POST body getServerBasePath(): string; // Provides the raw server basePath to allow it to be stripped out from relativeUrls in job params @@ -46,7 +53,7 @@ interface IReportingAPI { deleteReport(jobId: string): Promise; list(page: number, jobIds: string[]): Promise; // gets the first 10 report of the page total(): Promise; - getError(jobId: string): Promise; + getError(jobId: string): Promise; getInfo(jobId: string): Promise; findForJobIds(jobIds: string[]): Promise; @@ -61,11 +68,11 @@ interface IReportingAPI { } export class ReportingAPIClient implements IReportingAPI { - private http: HttpSetup; - - constructor(http: HttpSetup) { - this.http = http; - } + constructor( + private http: HttpSetup, + private uiSettings: IUiSettingsClient, + private kibanaVersion: string + ) {} public getReportURL(jobId: string) { const apiBaseUrl = this.http.basePath.prepend(API_LIST_URL); @@ -108,8 +115,16 @@ export class ReportingAPIClient implements IReportingAPI { } public async getError(jobId: string) { - return await this.http.get(`${API_LIST_URL}/output/${jobId}`, { - asSystemRequest: true, + const job = await this.getInfo(jobId); + + if (job.warnings?.[0]) { + // the error message of a failed report is a singular string in the warnings array + return job.warnings[0]; + } + + return i18n.translate('xpack.reporting.apiClient.unknownError', { + defaultMessage: `Report job {job} failed: Unknown error.`, + values: { job: jobId }, }); } @@ -128,13 +143,15 @@ export class ReportingAPIClient implements IReportingAPI { return reports.map((report) => new Job(report)); } - public getReportingJobPath(exportType: string, jobParams: JobParams) { - const params = stringify({ jobParams: rison.encode(jobParams) }); + public getReportingJobPath(exportType: string, jobParams: BaseParams) { + const risonObject: RisonObject = jobParams as Record; + const params = stringify({ jobParams: rison.encode(risonObject) }); return `${this.http.basePath.prepend(API_BASE_GENERATE)}/${exportType}?${params}`; } - public async createReportingJob(exportType: string, jobParams: any) { - const jobParamsRison = rison.encode(jobParams); + public async createReportingJob(exportType: string, jobParams: BaseParams) { + const risonObject: RisonObject = jobParams as Record; + const jobParamsRison = rison.encode(risonObject); const resp: { job: ReportApiJSON } = await this.http.post( `${API_BASE_GENERATE}/${exportType}`, { @@ -150,6 +167,27 @@ export class ReportingAPIClient implements IReportingAPI { return new Job(resp.job); } + public async createImmediateReport(baseParams: BaseParams) { + const { objectType: _objectType, ...params } = baseParams; // objectType is not needed for immediate download api + return this.http.post(`${API_GENERATE_IMMEDIATE}`, { body: JSON.stringify(params) }); + } + + public getDecoratedJobParams(baseParams: T): BaseParams { + // If the TZ is set to the default "Browser", it will not be useful for + // server-side export. We need to derive the timezone and pass it as a param + // to the export API. + const browserTimezone: string = + this.uiSettings.get('dateFormat:tz') === 'Browser' + ? moment.tz.guess() + : this.uiSettings.get('dateFormat:tz'); + + return { + browserTimezone, + version: this.kibanaVersion, + ...baseParams, + }; + } + public getManagementLink: ManagementLinkFn = () => this.http.basePath.prepend(REPORTING_MANAGEMENT_HOME); diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts index 58fde5cbd83ad..518c8ef11857a 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts @@ -26,12 +26,12 @@ const mockJobsFound: Job[] = [ { id: 'job-source-mock3', status: 'pending', output: { csv_contains_formulas: false, max_size_reached: false }, payload: { title: 'specimen' } }, ].map((j) => new Job(j as ReportApiJSON)); // prettier-ignore -const jobQueueClientMock = new ReportingAPIClient(coreMock.createSetup().http); -jobQueueClientMock.findForJobIds = async (jobIds: string[]) => mockJobsFound; +const coreSetup = coreMock.createSetup(); +const jobQueueClientMock = new ReportingAPIClient(coreSetup.http, coreSetup.uiSettings, '7.15.0'); +jobQueueClientMock.findForJobIds = async () => mockJobsFound; jobQueueClientMock.getInfo = () => Promise.resolve(({ content: 'this is the completed report data' } as unknown) as Job); -jobQueueClientMock.getError = () => - Promise.resolve({ content: 'this is the completed report data' }); +jobQueueClientMock.getError = () => Promise.resolve('this is the failed report error'); jobQueueClientMock.getManagementLink = () => '/#management'; jobQueueClientMock.getDownloadLink = () => '/reporting/download/job-123'; diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.ts b/x-pack/plugins/reporting/public/lib/stream_handler.ts index 8e41d34d054ec..304b4fb73374d 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.ts @@ -74,9 +74,9 @@ export class ReportingNotifierStreamHandler { // no download link available for (const job of failedJobs) { - const { content } = await this.apiClient.getError(job.id); + const errorText = await this.apiClient.getError(job.id); this.notifications.toasts.addDanger( - getFailureToast(content, job, this.apiClient.getManagementLink) + getFailureToast(errorText, job, this.apiClient.getManagementLink) ); } return { completed: completedJobs, failed: failedJobs }; diff --git a/x-pack/plugins/reporting/public/management/__snapshots__/report_info_button.test.tsx.snap b/x-pack/plugins/reporting/public/management/__snapshots__/report_info_button.test.tsx.snap index 3417aa59f9d72..4ab50750bbc52 100644 --- a/x-pack/plugins/reporting/public/management/__snapshots__/report_info_button.test.tsx.snap +++ b/x-pack/plugins/reporting/public/management/__snapshots__/report_info_button.test.tsx.snap @@ -176,7 +176,7 @@ Array [ className="euiTitle euiTitle--medium" id="flyoutTitle" > - Job Info + Report info
@@ -226,7 +226,7 @@ Array [ className="euiTitle euiTitle--medium" id="flyoutTitle" > - Job Info + Report info diff --git a/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap b/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap index d0ed2d737b584..d78a09bc6af52 100644 --- a/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap +++ b/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap @@ -340,12 +340,14 @@ exports[`ReportListing Report job listing with some items 1`] = ` >
@@ -521,15 +524,47 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
+ 2020-04-14 @ 05:01 PM + , + } + } > - Pending - waiting for job to be processed + Pending at + + 2020-04-14 @ 05:01 PM + + +
+ + + Waiting for job to be processed. + + +
+
@@ -648,6 +683,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent__hoverItem" >
@@ -779,25 +815,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` "timeZone": null, } } - license$={ - Object { - "subscribe": [Function], - } - } - navigateToUrl={[MockFunction]} - pollConfig={ - Object { - "jobCompletionNotifier": Object { - "interval": 5000, - "intervalErrorMultiplier": 3, - }, - "jobsRefresh": Object { - "interval": 5000, - "intervalErrorMultiplier": 3, - }, - } - } - record={ + job={ Job { "attempts": 0, "browserTimezone": "America/Phoenix", @@ -831,6 +849,24 @@ exports[`ReportListing Report job listing with some items 1`] = ` "warnings": undefined, } } + license$={ + Object { + "subscribe": [Function], + } + } + navigateToUrl={[MockFunction]} + pollConfig={ + Object { + "jobCompletionNotifier": Object { + "interval": 5000, + "intervalErrorMultiplier": 3, + }, + "jobsRefresh": Object { + "interval": 5000, + "intervalErrorMultiplier": 3, + }, + } + } redirect={[MockFunction]} toasts={ Object { @@ -852,7 +888,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } } /> - - - - - @@ -1652,12 +1351,14 @@ exports[`ReportListing Report job listing with some items 1`] = `
@@ -1833,14 +1535,15 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -1849,13 +1552,30 @@ exports[`ReportListing Report job listing with some items 1`] = ` } } > - Processing (attempt 1 of 1) at + Processing at 2020-04-14 @ 05:01 PM + +
+ + + Attempt 1 of 1. + + +
+
@@ -1974,6 +1694,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent__hoverItem" >
@@ -2105,25 +1826,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` "timeZone": null, } } - license$={ - Object { - "subscribe": [Function], - } - } - navigateToUrl={[MockFunction]} - pollConfig={ - Object { - "jobCompletionNotifier": Object { - "interval": 5000, - "intervalErrorMultiplier": 3, - }, - "jobsRefresh": Object { - "interval": 5000, - "intervalErrorMultiplier": 3, - }, - } - } - record={ + job={ Job { "attempts": 1, "browserTimezone": "America/Phoenix", @@ -2157,10 +1860,28 @@ exports[`ReportListing Report job listing with some items 1`] = ` "warnings": undefined, } } - redirect={[MockFunction]} - toasts={ + license$={ Object { - "add": [MockFunction], + "subscribe": [Function], + } + } + navigateToUrl={[MockFunction]} + pollConfig={ + Object { + "jobCompletionNotifier": Object { + "interval": 5000, + "intervalErrorMultiplier": 3, + }, + "jobsRefresh": Object { + "interval": 5000, + "intervalErrorMultiplier": 3, + }, + } + } + redirect={[MockFunction]} + toasts={ + Object { + "add": [MockFunction], "addDanger": [MockFunction], "addError": [MockFunction], "addInfo": [MockFunction], @@ -2178,7 +1899,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } } /> - - - - - @@ -2978,12 +2362,14 @@ exports[`ReportListing Report job listing with some items 1`] = `
@@ -3159,11 +2546,12 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -3431,25 +2820,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` "timeZone": null, } } - license$={ - Object { - "subscribe": [Function], - } - } - navigateToUrl={[MockFunction]} - pollConfig={ - Object { - "jobCompletionNotifier": Object { - "interval": 5000, - "intervalErrorMultiplier": 3, - }, - "jobsRefresh": Object { - "interval": 5000, - "intervalErrorMultiplier": 3, - }, - } - } - record={ + job={ Job { "attempts": 1, "browserTimezone": "America/Phoenix", @@ -3483,6 +2854,24 @@ exports[`ReportListing Report job listing with some items 1`] = ` "warnings": undefined, } } + license$={ + Object { + "subscribe": [Function], + } + } + navigateToUrl={[MockFunction]} + pollConfig={ + Object { + "jobCompletionNotifier": Object { + "interval": 5000, + "intervalErrorMultiplier": 3, + }, + "jobsRefresh": Object { + "interval": 5000, + "intervalErrorMultiplier": 3, + }, + } + } redirect={[MockFunction]} toasts={ Object { @@ -3551,7 +2940,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` - - - - - @@ -4351,12 +3403,14 @@ exports[`ReportListing Report job listing with some items 1`] = `
@@ -4532,14 +3587,15 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -4548,7 +3604,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } } > - Completed with warnings at + Completed at @@ -4567,13 +3623,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` - - Errors occurred: see job info for details. - + See report info for warnings.
@@ -4700,6 +3750,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent__hoverItem" >
@@ -4831,25 +3882,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` "timeZone": null, } } - license$={ - Object { - "subscribe": [Function], - } - } - navigateToUrl={[MockFunction]} - pollConfig={ - Object { - "jobCompletionNotifier": Object { - "interval": 5000, - "intervalErrorMultiplier": 3, - }, - "jobsRefresh": Object { - "interval": 5000, - "intervalErrorMultiplier": 3, - }, - } - } - record={ + job={ Job { "attempts": 1, "browserTimezone": "America/Phoenix", @@ -4885,6 +3918,24 @@ exports[`ReportListing Report job listing with some items 1`] = ` ], } } + license$={ + Object { + "subscribe": [Function], + } + } + navigateToUrl={[MockFunction]} + pollConfig={ + Object { + "jobCompletionNotifier": Object { + "interval": 5000, + "intervalErrorMultiplier": 3, + }, + "jobsRefresh": Object { + "interval": 5000, + "intervalErrorMultiplier": 3, + }, + } + } redirect={[MockFunction]} toasts={ Object { @@ -4907,7 +3958,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } > @@ -4953,7 +4004,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` - - - - - - - - - - - - - - -
-
- - -
- - - -
- - - - -
- - -
- -
-
- - -
- - - - -
- Report -
-
-
-
- My Canvas Workpad -
- -
- - - canvas workpad - - -
-
-
-
- -
- - -
- Created at -
-
-
-
- 2020-04-14 @ 01:19 PM -
- - elastic - -
-
- -
- - -
- Status -
-
-
- - 2020-04-14 @ 01:19 PM - , - } - } - > - Completed at - - 2020-04-14 @ 01:19 PM - - -
-
- -
- - -
- - -
-
- - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- -
- - - - - - -
- - -
- -
-
- - -
- - - - -
- Report -
-
-
-
- My Canvas Workpad -
- -
- - - canvas workpad - - -
-
-
-
- -
- - -
- Created at -
-
-
-
- 2020-04-14 @ 01:19 PM -
- - elastic - -
-
- -
- - -
- Status -
-
-
- - 2020-04-14 @ 01:19 PM - , - } - } - > - Completed at - - 2020-04-14 @ 01:19 PM - - -
-
- -
- - -
- - -
-
- - - - - - - - - - - - - - @@ -8455,16 +4423,16 @@ exports[`ReportListing Report job listing with some items 1`] = ` >
-
- - - - -
- Created at -
-
-
-
- 2020-04-14 @ 01:17 PM -
- - elastic - -
-
- -
- - -
- Status -
-
-
- - 2020-04-14 @ 01:18 PM - , - } - } - > - Completed at - - 2020-04-14 @ 01:18 PM - - -
-
- -
- - -
- - + My Canvas Workpad +
+
-
- + - - - - - - - - - + +
+ +
+
+ +
+ + +
+ Created at +
+
+
+
+ 2020-04-14 @ 01:19 PM +
+ + elastic + +
+
+ +
+ + +
+ Status +
+
+
+ + 2020-04-14 @ 01:19 PM + , + } + } + > + Completed at + + 2020-04-14 @ 01:19 PM + + +
+
+ +
+ + +
+ + +
+
+ - - + } + > + + + + + + + + @@ -9876,15 +5512,17 @@ exports[`ReportListing Report job listing with some items 1`] = `
- 2020-04-14 @ 01:12 PM + 2020-04-14 @ 01:19 PM
elastic @@ -10030,7 +5669,7 @@ exports[`ReportListing Report job listing with some items 1`] = `
- 2020-04-14 @ 01:13 PM + 2020-04-14 @ 01:19 PM , } } @@ -10077,7 +5717,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` - 2020-04-14 @ 01:13 PM + 2020-04-14 @ 01:19 PM
@@ -10087,7 +5727,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` @@ -10117,12 +5757,12 @@ exports[`ReportListing Report job listing with some items 1`] = ` "attempts": 1, "browserTimezone": "America/Phoenix", "browser_type": "chromium", - "completed_at": "2020-04-14T17:13:03.719Z", + "completed_at": "2020-04-14T17:19:36.822Z", "content_type": "application/pdf", - "created_at": "2020-04-14T17:12:51.985Z", + "created_at": "2020-04-14T17:19:23.578Z", "created_by": "elastic", "csv_contains_formulas": undefined, - "id": "k905zdw11d34cbae0c3y6tzh", + "id": "k9067s1m1d4wcbae0cdnvcms", "index": ".reporting-2020.04.12", "isDeprecated": false, "jobtype": "printable_pdf", @@ -10139,14 +5779,14 @@ exports[`ReportListing Report job listing with some items 1`] = ` "max_size_reached": undefined, "objectType": "canvas workpad", "size": 80262, - "started_at": "2020-04-14T17:12:52.431Z", + "started_at": "2020-04-14T17:19:25.247Z", "status": "completed", "timeout": 300000, "title": "My Canvas Workpad", "warnings": undefined, } } - itemId="k905zdw11d34cbae0c3y6tzh" + itemId="k9067s1m1d4wcbae0cdnvcms" key=".0" >
@@ -10329,127 +5970,920 @@ exports[`ReportListing Report job listing with some items 1`] = ` "timeZone": null, } } + job={ + Job { + "attempts": 1, + "browserTimezone": "America/Phoenix", + "browser_type": "chromium", + "completed_at": "2020-04-14T17:19:36.822Z", + "content_type": "application/pdf", + "created_at": "2020-04-14T17:19:23.578Z", + "created_by": "elastic", + "csv_contains_formulas": undefined, + "id": "k9067s1m1d4wcbae0cdnvcms", + "index": ".reporting-2020.04.12", + "isDeprecated": false, + "jobtype": "printable_pdf", + "kibana_id": "5b2de169-2785-441b-ae8c-186a1936b17d", + "kibana_name": "spicy.local", + "layout": Object { + "dimensions": Object { + "height": 720, + "width": 1080, + }, + "id": "preserve_layout", + }, + "max_attempts": 1, + "max_size_reached": undefined, + "objectType": "canvas workpad", + "size": 80262, + "started_at": "2020-04-14T17:19:25.247Z", + "status": "completed", + "timeout": 300000, + "title": "My Canvas Workpad", + "warnings": undefined, + } + } + license$={ + Object { + "subscribe": [Function], + } + } + navigateToUrl={[MockFunction]} + pollConfig={ + Object { + "jobCompletionNotifier": Object { + "interval": 5000, + "intervalErrorMultiplier": 3, + }, + "jobsRefresh": Object { + "interval": 5000, + "intervalErrorMultiplier": 3, + }, + } + } + redirect={[MockFunction]} + toasts={ + Object { + "add": [MockFunction], + "addDanger": [MockFunction], + "addError": [MockFunction], + "addInfo": [MockFunction], + "addSuccess": [MockFunction], + "addWarning": [MockFunction], + "get$": [MockFunction], + "remove": [MockFunction], + } + } + urlService={ + Object { + "locators": Object { + "get": [Function], + }, + } + } + > + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+ +
+ + + + +
+ + +
+ +
+
+ + +
+ + + + +
+ Report +
+
+
+
+ My Canvas Workpad +
+ +
+ + - - - - - - - - - + +
+
+
+
+ +
+ + +
+ Created at +
+
+
+
+ 2020-04-14 @ 01:17 PM +
+ + elastic + +
+
+ +
+ + +
+ Status +
+
+
+ + 2020-04-14 @ 01:18 PM + , + } + } + > + Completed at + + 2020-04-14 @ 01:18 PM + + +
+
+ +
+ + +
+ + +
+
+ - - + } + > + + + + + + + + @@ -11249,15 +7594,17 @@ exports[`ReportListing Report job listing with some items 1`] = `
- count + My Canvas Workpad
- visualization + canvas workpad
@@ -11362,7 +7709,7 @@ exports[`ReportListing Report job listing with some items 1`] = `
- 2020-04-09 @ 03:09 PM + 2020-04-14 @ 01:12 PM
elastic @@ -11403,7 +7751,7 @@ exports[`ReportListing Report job listing with some items 1`] = `
- 2020-04-09 @ 03:10 PM + 2020-04-14 @ 01:13 PM , } } @@ -11450,7 +7799,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` - 2020-04-09 @ 03:10 PM + 2020-04-14 @ 01:13 PM
@@ -11460,7 +7809,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` @@ -11490,36 +7839,36 @@ exports[`ReportListing Report job listing with some items 1`] = ` "attempts": 1, "browserTimezone": "America/Phoenix", "browser_type": "chromium", - "completed_at": "2020-04-09T19:10:10.049Z", - "content_type": "image/png", - "created_at": "2020-04-09T19:09:52.139Z", + "completed_at": "2020-04-14T17:13:03.719Z", + "content_type": "application/pdf", + "created_at": "2020-04-14T17:12:51.985Z", "created_by": "elastic", "csv_contains_formulas": undefined, - "id": "k8t4ylcb07mi9d006214ifyg", - "index": ".reporting-2020.04.05", + "id": "k905zdw11d34cbae0c3y6tzh", + "index": ".reporting-2020.04.12", "isDeprecated": false, - "jobtype": "PNG", - "kibana_id": "f2e59b4e-f79b-4a48-8a7d-6d50a3c1d914", + "jobtype": "printable_pdf", + "kibana_id": "5b2de169-2785-441b-ae8c-186a1936b17d", "kibana_name": "spicy.local", "layout": Object { "dimensions": Object { - "height": 1575, - "width": 1423, + "height": 720, + "width": 1080, }, - "id": "png", + "id": "preserve_layout", }, "max_attempts": 1, "max_size_reached": undefined, - "objectType": "visualization", - "size": 123456789, - "started_at": "2020-04-09T19:09:54.570Z", + "objectType": "canvas workpad", + "size": 80262, + "started_at": "2020-04-14T17:12:52.431Z", "status": "completed", "timeout": 300000, - "title": "count", + "title": "My Canvas Workpad", "warnings": undefined, } } - itemId="k8t4ylcb07mi9d006214ifyg" + itemId="k905zdw11d34cbae0c3y6tzh" key=".0" >
@@ -11702,127 +8052,920 @@ exports[`ReportListing Report job listing with some items 1`] = ` "timeZone": null, } } + job={ + Job { + "attempts": 1, + "browserTimezone": "America/Phoenix", + "browser_type": "chromium", + "completed_at": "2020-04-14T17:13:03.719Z", + "content_type": "application/pdf", + "created_at": "2020-04-14T17:12:51.985Z", + "created_by": "elastic", + "csv_contains_formulas": undefined, + "id": "k905zdw11d34cbae0c3y6tzh", + "index": ".reporting-2020.04.12", + "isDeprecated": false, + "jobtype": "printable_pdf", + "kibana_id": "5b2de169-2785-441b-ae8c-186a1936b17d", + "kibana_name": "spicy.local", + "layout": Object { + "dimensions": Object { + "height": 720, + "width": 1080, + }, + "id": "preserve_layout", + }, + "max_attempts": 1, + "max_size_reached": undefined, + "objectType": "canvas workpad", + "size": 80262, + "started_at": "2020-04-14T17:12:52.431Z", + "status": "completed", + "timeout": 300000, + "title": "My Canvas Workpad", + "warnings": undefined, + } + } + license$={ + Object { + "subscribe": [Function], + } + } + navigateToUrl={[MockFunction]} + pollConfig={ + Object { + "jobCompletionNotifier": Object { + "interval": 5000, + "intervalErrorMultiplier": 3, + }, + "jobsRefresh": Object { + "interval": 5000, + "intervalErrorMultiplier": 3, + }, + } + } + redirect={[MockFunction]} + toasts={ + Object { + "add": [MockFunction], + "addDanger": [MockFunction], + "addError": [MockFunction], + "addInfo": [MockFunction], + "addSuccess": [MockFunction], + "addWarning": [MockFunction], + "get$": [MockFunction], + "remove": [MockFunction], + } + } + urlService={ + Object { + "locators": Object { + "get": [Function], + }, + } + } + > + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+ + + + + + +
+ + +
+ +
+
+ + +
+ + + + +
+ Report +
+
+
+
+ count +
+ +
+ + - - - - - - - - - + +
+
+
+
+ +
+ + +
+ Created at +
+
+
+
+ 2020-04-09 @ 03:09 PM +
+ + elastic + +
+
+ +
+ + +
+ Status +
+
+
+ + 2020-04-09 @ 03:10 PM + , + } + } + > + Completed at + + 2020-04-09 @ 03:10 PM + + +
+
+ +
+ + +
+ + +
+
+ - - + } + > + + + + + + + + diff --git a/x-pack/plugins/reporting/public/management/index.ts b/x-pack/plugins/reporting/public/management/index.ts index c107993ef3074..4d324135288db 100644 --- a/x-pack/plugins/reporting/public/management/index.ts +++ b/x-pack/plugins/reporting/public/management/index.ts @@ -5,7 +5,25 @@ * 2.0. */ -export { ReportErrorButton } from './report_error_button'; -export { ReportDeleteButton } from './report_delete_button'; -export { ReportDownloadButton } from './report_download_button'; -export { ReportInfoButton } from './report_info_button'; +import { InjectedIntl } from '@kbn/i18n/react'; +import { ApplicationStart, ToastsSetup } from 'src/core/public'; +import { LicensingPluginSetup } from '../../../licensing/public'; +import { UseIlmPolicyStatusReturn } from '../lib/ilm_policy_status_context'; +import { ReportingAPIClient } from '../lib/reporting_api_client'; +import { ClientConfigType } from '../plugin'; +import type { SharePluginSetup } from '../shared_imports'; + +export interface ListingProps { + intl: InjectedIntl; + apiClient: ReportingAPIClient; + capabilities: ApplicationStart['capabilities']; + license$: LicensingPluginSetup['license$']; // FIXME: license$ is deprecated + pollConfig: ClientConfigType['poll']; + redirect: ApplicationStart['navigateToApp']; + navigateToUrl: ApplicationStart['navigateToUrl']; + toasts: ToastsSetup; + urlService: SharePluginSetup['url']; + ilmPolicyContextValue: UseIlmPolicyStatusReturn; +} + +export { ReportListing } from './report_listing'; diff --git a/x-pack/plugins/reporting/public/management/mount_management_section.tsx b/x-pack/plugins/reporting/public/management/mount_management_section.tsx index 20ea2988f3b8b..0f0c06f830205 100644 --- a/x-pack/plugins/reporting/public/management/mount_management_section.tsx +++ b/x-pack/plugins/reporting/public/management/mount_management_section.tsx @@ -16,7 +16,7 @@ import { IlmPolicyStatusContextProvider } from '../lib/ilm_policy_status_context import { ClientConfigType } from '../plugin'; import type { ManagementAppMountParams, SharePluginSetup } from '../shared_imports'; import { KibanaContextProvider } from '../shared_imports'; -import { ReportListing } from './report_listing'; +import { ReportListing } from '.'; export async function mountManagementSection( coreSetup: CoreSetup, diff --git a/x-pack/plugins/reporting/public/management/report_delete_button.tsx b/x-pack/plugins/reporting/public/management/report_delete_button.tsx index 7009a653c1bf6..da1ce9dd9e1cb 100644 --- a/x-pack/plugins/reporting/public/management/report_delete_button.tsx +++ b/x-pack/plugins/reporting/public/management/report_delete_button.tsx @@ -8,7 +8,7 @@ import { EuiButton, EuiConfirmModal } from '@elastic/eui'; import React, { Fragment, PureComponent } from 'react'; import { Job } from '../lib/job'; -import { Props as ListingProps } from './report_listing'; +import { ListingProps } from './'; type DeleteFn = () => Promise; type Props = { jobsToDelete: Job[]; performDelete: DeleteFn } & ListingProps; diff --git a/x-pack/plugins/reporting/public/management/report_diagnostic.tsx b/x-pack/plugins/reporting/public/management/report_diagnostic.tsx index c7f5518ec9c1f..7525cf3ba7303 100644 --- a/x-pack/plugins/reporting/public/management/report_diagnostic.tsx +++ b/x-pack/plugins/reporting/public/management/report_diagnostic.tsx @@ -120,7 +120,7 @@ export const ReportDiagnostic = ({ apiClient }: Props) => { apiClient.verifyConfig(), statuses.configStatus)} iconType={configStatus === 'complete' ? 'check' : undefined} > { apiClient.verifyBrowser(), statuses.chromeStatus)} isLoading={isBusy && chromeStatus === 'incomplete'} iconType={chromeStatus === 'complete' ? 'check' : undefined} > @@ -177,7 +177,7 @@ export const ReportDiagnostic = ({ apiClient }: Props) => { apiClient.verifyScreenCapture(), statuses.screenshotStatus)} isLoading={isBusy && screenshotStatus === 'incomplete'} iconType={screenshotStatus === 'complete' ? 'check' : undefined} > diff --git a/x-pack/plugins/reporting/public/management/report_download_button.tsx b/x-pack/plugins/reporting/public/management/report_download_button.tsx index b421271037722..f21c83fbf42da 100644 --- a/x-pack/plugins/reporting/public/management/report_download_button.tsx +++ b/x-pack/plugins/reporting/public/management/report_download_button.tsx @@ -6,23 +6,28 @@ */ import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { InjectedIntl } from '@kbn/i18n/react'; import React, { FunctionComponent } from 'react'; import { JOB_STATUSES } from '../../common/constants'; import { Job as ListingJob } from '../lib/job'; -import { Props as ListingProps } from './report_listing'; +import { ReportingAPIClient } from '../lib/reporting_api_client'; -type Props = { record: ListingJob } & ListingProps; +interface Props { + intl: InjectedIntl; + apiClient: ReportingAPIClient; + job: ListingJob; +} export const ReportDownloadButton: FunctionComponent = (props: Props) => { - const { record, apiClient, intl } = props; + const { job, apiClient, intl } = props; - if (record.status !== JOB_STATUSES.COMPLETED && record.status !== JOB_STATUSES.WARNINGS) { + if (job.status !== JOB_STATUSES.COMPLETED && job.status !== JOB_STATUSES.WARNINGS) { return null; } const button = ( apiClient.downloadReport(record.id)} + onClick={() => apiClient.downloadReport(job.id)} iconType="importAction" aria-label={intl.formatMessage({ id: 'xpack.reporting.listing.table.downloadReportAriaLabel', @@ -31,28 +36,14 @@ export const ReportDownloadButton: FunctionComponent = (props: Props) => /> ); - if (record.csv_contains_formulas) { + const warnings = job.getWarnings(); + if (warnings) { return ( - {button} - - ); - } - - if (record.max_size_reached) { - return ( - {button} diff --git a/x-pack/plugins/reporting/public/management/report_error_button.tsx b/x-pack/plugins/reporting/public/management/report_error_button.tsx deleted file mode 100644 index ee0c0e162cb7d..0000000000000 --- a/x-pack/plugins/reporting/public/management/report_error_button.tsx +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiButtonIcon, EuiCallOut, EuiPopover } from '@elastic/eui'; -import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import React, { Component } from 'react'; -import { JOB_STATUSES } from '../../common/constants'; -import { Job as ListingJob } from '../lib/job'; -import { JobContent, ReportingAPIClient } from '../lib/reporting_api_client'; - -interface Props { - intl: InjectedIntl; - apiClient: ReportingAPIClient; - record: ListingJob; -} - -interface State { - isLoading: boolean; - isPopoverOpen: boolean; - calloutTitle: string; - error?: string; -} - -class ReportErrorButtonUi extends Component { - private mounted?: boolean; - - constructor(props: Props) { - super(props); - - this.state = { - isLoading: false, - isPopoverOpen: false, - calloutTitle: props.intl.formatMessage({ - id: 'xpack.reporting.errorButton.unableToGenerateReportTitle', - defaultMessage: 'Unable to generate report', - }), - }; - } - - public render() { - const { record, intl } = this.props; - - if (record.status !== JOB_STATUSES.FAILED) { - return null; - } - - const button = ( - - ); - - return ( - - -

{this.state.error}

-
-
- ); - } - - public componentWillUnmount() { - this.mounted = false; - } - - public componentDidMount() { - this.mounted = true; - } - - private togglePopover = () => { - this.setState((prevState) => { - return { isPopoverOpen: !prevState.isPopoverOpen }; - }); - - if (!this.state.error) { - this.loadError(); - } - }; - - private closePopover = () => { - this.setState({ isPopoverOpen: false }); - }; - - private loadError = async () => { - const { record, apiClient, intl } = this.props; - - this.setState({ isLoading: true }); - try { - const reportContent: JobContent = await apiClient.getError(record.id); - if (this.mounted) { - this.setState({ isLoading: false, error: reportContent.content }); - } - } catch (kfetchError) { - if (this.mounted) { - this.setState({ - isLoading: false, - calloutTitle: intl.formatMessage({ - id: 'xpack.reporting.errorButton.unableToFetchReportContentTitle', - defaultMessage: 'Unable to fetch report content', - }), - error: kfetchError.message, - }); - } - } - }; -} - -export const ReportErrorButton = injectI18n(ReportErrorButtonUi); diff --git a/x-pack/plugins/reporting/public/management/report_info_button.test.tsx b/x-pack/plugins/reporting/public/management/report_info_button.test.tsx index 119856042a326..c52027355ac5e 100644 --- a/x-pack/plugins/reporting/public/management/report_info_button.test.tsx +++ b/x-pack/plugins/reporting/public/management/report_info_button.test.tsx @@ -7,24 +7,49 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; +import { coreMock } from '../../../../../src/core/public/mocks'; +import { Job } from '../lib/job'; import { ReportInfoButton } from './report_info_button'; jest.mock('../lib/reporting_api_client'); import { ReportingAPIClient } from '../lib/reporting_api_client'; -const httpSetup = {} as any; -const apiClient = new ReportingAPIClient(httpSetup); +const coreSetup = coreMock.createSetup(); +const apiClient = new ReportingAPIClient(coreSetup.http, coreSetup.uiSettings, '7.15.0'); + +const job = new Job({ + id: 'abc-123', + index: '.reporting-2020.04.12', + migration_version: '7.15.0', + attempts: 0, + browser_type: 'chromium', + created_at: '2020-04-14T21:01:13.064Z', + created_by: 'elastic', + jobtype: 'printable_pdf', + max_attempts: 1, + meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, + payload: { + browserTimezone: 'America/Phoenix', + version: '7.15.0-test', + layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, + objectType: 'canvas workpad', + title: 'My Canvas Workpad', + }, + process_expiration: '1970-01-01T00:00:00.000Z', + status: 'pending', + timeout: 300000, +}); describe('ReportInfoButton', () => { it('handles button click flyout on click', () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); expect(input).toMatchSnapshot(); }); it('opens flyout with info', async () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); input.simulate('click'); @@ -33,7 +58,7 @@ describe('ReportInfoButton', () => { expect(flyout).toMatchSnapshot(); expect(apiClient.getInfo).toHaveBeenCalledTimes(1); - expect(apiClient.getInfo).toHaveBeenCalledWith('abc-456'); + expect(apiClient.getInfo).toHaveBeenCalledWith('abc-123'); }); it('opens flyout with fetch error info', () => { @@ -42,7 +67,7 @@ describe('ReportInfoButton', () => { throw new Error('Could not fetch the job info'); }); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); const input = wrapper.find('[data-test-subj="reportInfoButton"]').hostNodes(); input.simulate('click'); @@ -51,6 +76,6 @@ describe('ReportInfoButton', () => { expect(flyout).toMatchSnapshot(); expect(apiClient.getInfo).toHaveBeenCalledTimes(1); - expect(apiClient.getInfo).toHaveBeenCalledWith('abc-789'); + expect(apiClient.getInfo).toHaveBeenCalledWith('abc-123'); }); }); diff --git a/x-pack/plugins/reporting/public/management/report_info_button.tsx b/x-pack/plugins/reporting/public/management/report_info_button.tsx index 92acaa386bd56..8513558fb89cc 100644 --- a/x-pack/plugins/reporting/public/management/report_info_button.tsx +++ b/x-pack/plugins/reporting/public/management/report_info_button.tsx @@ -22,11 +22,11 @@ import React, { Component } from 'react'; import { USES_HEADLESS_JOB_TYPES } from '../../common/constants'; import { Job } from '../lib/job'; import { ReportingAPIClient } from '../lib/reporting_api_client'; -import { Props as ListingProps } from './report_listing'; +import { ListingProps } from '.'; interface Props extends Pick { - jobId: string; apiClient: ReportingAPIClient; + job: Job; } interface State { @@ -58,7 +58,10 @@ class ReportInfoButtonUi extends Component { this.state = { isLoading: false, isFlyoutVisible: false, - calloutTitle: 'Job Info', + calloutTitle: props.intl.formatMessage({ + id: 'xpack.reporting.listing.table.reportCalloutTitle', + defaultMessage: 'Report info', + }), info: null, error: null, }; @@ -76,58 +79,177 @@ class ReportInfoButtonUi extends Component { return null; } - const jobType = info.jobtype || NA; - const attempts = info.attempts ? info.attempts.toString() : NA; - const maxAttempts = info.max_attempts ? info.max_attempts.toString() : NA; const timeout = info.timeout ? info.timeout.toString() : NA; - const warnings = info.warnings?.join(',') ?? null; const jobInfo = [ - { title: 'Title', description: info.title || NA }, - { title: 'Created By', description: info.created_by || NA }, - { title: 'Created At', description: info.created_at || NA }, - { title: 'Timezone', description: info.browserTimezone || NA }, - { title: 'Status', description: info.status || NA }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.titleInfo', + defaultMessage: 'Title', + }), + description: info.title || NA, + }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.createdAtInfo', + defaultMessage: 'Created At', + }), + description: info.getCreatedAtLabel(), + }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.statusInfo', + defaultMessage: 'Status', + }), + description: info.getStatus(), + }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.tzInfo', + defaultMessage: 'Timezone', + }), + description: info.browserTimezone || NA, + }, ]; const processingInfo = [ - { title: 'Started At', description: info.started_at || NA }, - { title: 'Completed At', description: info.completed_at || NA }, { - title: 'Processed By', + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.startedAtInfo', + defaultMessage: 'Started At', + }), + description: info.started_at || NA, + }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.completedAtInfo', + defaultMessage: 'Completed At', + }), + description: info.completed_at || NA, + }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.processedByInfo', + defaultMessage: 'Processed By', + }), description: info.kibana_name && info.kibana_id ? `${info.kibana_name} (${info.kibana_id})` : NA, }, - { title: 'Content Type', description: info.content_type || NA }, - { title: 'Size in Bytes', description: info.size?.toString() || NA }, - { title: 'Attempts', description: attempts }, - { title: 'Max Attempts', description: maxAttempts }, - { title: 'Timeout', description: timeout }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.contentTypeInfo', + defaultMessage: 'Content Type', + }), + description: info.content_type || NA, + }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.sizeInfo', + defaultMessage: 'Size in Bytes', + }), + description: info.size?.toString() || NA, + }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.attemptsInfo', + defaultMessage: 'Attempts', + }), + description: info.attempts.toString(), + }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.maxAttemptsInfo', + defaultMessage: 'Max Attempts', + }), + description: info.max_attempts?.toString() || NA, + }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.timeoutInfo', + defaultMessage: 'Timeout', + }), + description: timeout, + }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.exportTypeInfo', + defaultMessage: 'Export Type', + }), + description: info.isDeprecated + ? this.props.intl.formatMessage( + { + id: 'xpack.reporting.listing.table.reportCalloutExportTypeDeprecated', + defaultMessage: '{jobtype} (DEPRECATED)', + }, + { jobtype: info.jobtype } + ) + : info.jobtype, + }, + + // TODO when https://github.com/elastic/kibana/pull/106137 is merged, add kibana version field ]; const jobScreenshot = [ - { title: 'Dimensions', description: getDimensions(info) }, - { title: 'Layout', description: info.layout?.id || UNKNOWN }, - { title: 'Browser Type', description: info.browser_type || NA }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.dimensionsInfo', + defaultMessage: 'Dimensions', + }), + description: getDimensions(info), + }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.layoutInfo', + defaultMessage: 'Layout', + }), + description: info.layout?.id || UNKNOWN, + }, + { + title: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.infoPanel.browserTypeInfo', + defaultMessage: 'Browser Type', + }), + description: info.browser_type || NA, + }, + ]; + + const warnings = info.getWarnings(); + const warningsInfo = warnings && [ + { + title: Warnings, + description: {warnings}, + }, ]; - const warningInfo = warnings && [{ title: 'Errors', description: warnings }]; + const errored = info.getError(); + const errorInfo = errored && [ + { + title: Error, + description: {errored}, + }, + ]; return ( <> - {USES_HEADLESS_JOB_TYPES.includes(jobType) ? ( + {USES_HEADLESS_JOB_TYPES.includes(info.jobtype) ? ( <> ) : null} - {warningInfo ? ( + {warningsInfo ? ( + <> + + + + ) : null} + {errorInfo ? ( <> - + ) : null} @@ -143,6 +265,7 @@ class ReportInfoButtonUi extends Component { } public render() { + const job = this.props.job; let flyout; if (this.state.isFlyoutVisible) { @@ -168,21 +291,44 @@ class ReportInfoButtonUi extends Component { ); } + let message = this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.table.reportInfoButtonTooltip', + defaultMessage: 'See report info', + }); + if (job.getError()) { + message = this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.table.reportInfoAndErrorButtonTooltip', + defaultMessage: 'See report info and error message', + }); + } else if (job.getWarnings()) { + message = this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.table.reportInfoAndWarningsButtonTooltip', + defaultMessage: 'See report info and warnings', + }); + } + + let buttonIconType = 'iInCircle'; + let buttonColor: 'primary' | 'danger' | 'warning' = 'primary'; + if (job.getWarnings() || job.getError()) { + buttonIconType = 'alert'; + buttonColor = 'danger'; + } + if (job.getWarnings()) { + buttonColor = 'warning'; + } + return ( <> - + {flyout} @@ -193,7 +339,7 @@ class ReportInfoButtonUi extends Component { private loadInfo = async () => { this.setState({ isLoading: true }); try { - const info = await this.props.apiClient.getInfo(this.props.jobId); + const info = await this.props.apiClient.getInfo(this.props.job.id); if (this.mounted) { this.setState({ isLoading: false, info }); } @@ -201,7 +347,10 @@ class ReportInfoButtonUi extends Component { if (this.mounted) { this.setState({ isLoading: false, - calloutTitle: 'Unable to fetch report info', + calloutTitle: this.props.intl.formatMessage({ + id: 'xpack.reporting.listing.table.reportInfoUnableToFetch', + defaultMessage: 'Unable to fetch report info', + }), info: null, error: err, }); diff --git a/x-pack/plugins/reporting/public/management/report_listing.test.tsx b/x-pack/plugins/reporting/public/management/report_listing.test.tsx index b2eb6f0029580..dd8b60801066f 100644 --- a/x-pack/plugins/reporting/public/management/report_listing.test.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing.test.tsx @@ -23,7 +23,7 @@ import { IlmPolicyStatusContextProvider } from '../lib/ilm_policy_status_context import { Job } from '../lib/job'; import { InternalApiClientClientProvider, ReportingAPIClient } from '../lib/reporting_api_client'; import { KibanaContextProvider } from '../shared_imports'; -import { Props, ReportListing } from './report_listing'; +import { ListingProps as Props, ReportListing } from '.'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { return { @@ -32,15 +32,15 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { }); const mockJobs: ReportApiJSON[] = [ - { id: 'k90e51pk1ieucbae0c3t8wo2', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 0, browser_type: 'chromium', created_at: '2020-04-14T21:01:13.064Z', created_by: 'elastic', jobtype: 'printable_pdf', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '1970-01-01T00:00:00.000Z', status: 'pending', timeout: 300000 }, // prettier-ignore - { id: 'k90e51pk1ieucbae0c3t8wo1', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', created_at: '2020-04-14T21:01:13.064Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T21:06:14.526Z', started_at: '2020-04-14T21:01:14.526Z', status: 'processing', timeout: 300000 }, - { id: 'k90cmthd1gv8cbae0c2le8bo', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T20:19:14.748Z', created_at: '2020-04-14T20:19:02.977Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T20:24:04.073Z', started_at: '2020-04-14T20:19:04.073Z', status: 'completed', timeout: 300000 }, - { id: 'k906958e1d4wcbae0c9hip1a', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:21:08.223Z', created_at: '2020-04-14T17:20:27.326Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 49468, warnings: [ 'An error occurred when trying to read the page for visualization panel info. You may need to increase \'xpack.reporting.capture.timeouts.waitForElements\'. TimeoutError: waiting for selector "[data-shared-item],[data-shared-items-count]" failed: timeout 30000ms exceeded', ] }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T17:25:29.444Z', started_at: '2020-04-14T17:20:29.444Z', status: 'completed_with_warnings', timeout: 300000 }, - { id: 'k9067y2a1d4wcbae0cad38n0', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:19:53.244Z', created_at: '2020-04-14T17:19:31.379Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T17:24:39.883Z', started_at: '2020-04-14T17:19:39.883Z', status: 'completed', timeout: 300000 }, - { id: 'k9067s1m1d4wcbae0cdnvcms', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:19:36.822Z', created_at: '2020-04-14T17:19:23.578Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T17:24:25.247Z', started_at: '2020-04-14T17:19:25.247Z', status: 'completed', timeout: 300000 }, - { id: 'k9065q3s1d4wcbae0c00fxlh', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:18:03.910Z', created_at: '2020-04-14T17:17:47.752Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T17:22:50.379Z', started_at: '2020-04-14T17:17:50.379Z', status: 'completed', timeout: 300000 }, - { id: 'k905zdw11d34cbae0c3y6tzh', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:13:03.719Z', created_at: '2020-04-14T17:12:51.985Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T17:17:52.431Z', started_at: '2020-04-14T17:12:52.431Z', status: 'completed', timeout: 300000 }, - { id: 'k8t4ylcb07mi9d006214ifyg', index: '.reporting-2020.04.05', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-09T19:10:10.049Z', created_at: '2020-04-09T19:09:52.139Z', created_by: 'elastic', jobtype: 'PNG', kibana_id: 'f2e59b4e-f79b-4a48-8a7d-6d50a3c1d914', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'png', objectType: 'visualization' }, output: { content_type: 'image/png', size: 123456789 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 1575, width: 1423 }, id: 'png' }, objectType: 'visualization', title: 'count' }, process_expiration: '2020-04-09T19:14:54.570Z', started_at: '2020-04-09T19:09:54.570Z', status: 'completed', timeout: 300000 }, + { id: 'k90e51pk1ieucbae0c3t8wo2', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 0, browser_type: 'chromium', created_at: '2020-04-14T21:01:13.064Z', created_by: 'elastic', jobtype: 'printable_pdf', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad', version: '7.14.0' }, process_expiration: '1970-01-01T00:00:00.000Z', status: 'pending', timeout: 300000}, // prettier-ignore + { id: 'k90e51pk1ieucbae0c3t8wo1', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', created_at: '2020-04-14T21:01:13.064Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad', version: '7.14.0' }, process_expiration: '2020-04-14T21:06:14.526Z', started_at: '2020-04-14T21:01:14.526Z', status: 'processing', timeout: 300000 }, + { id: 'k90cmthd1gv8cbae0c2le8bo', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T20:19:14.748Z', created_at: '2020-04-14T20:19:02.977Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad', version: '7.14.0' }, process_expiration: '2020-04-14T20:24:04.073Z', started_at: '2020-04-14T20:19:04.073Z', status: 'completed', timeout: 300000 }, + { id: 'k906958e1d4wcbae0c9hip1a', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:21:08.223Z', created_at: '2020-04-14T17:20:27.326Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 49468, warnings: [ 'An error occurred when trying to read the page for visualization panel info. You may need to increase \'xpack.reporting.capture.timeouts.waitForElements\'. TimeoutError: waiting for selector "[data-shared-item],[data-shared-items-count]" failed: timeout 30000ms exceeded' ] }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad', version: '7.14.0' }, process_expiration: '2020-04-14T17:25:29.444Z', started_at: '2020-04-14T17:20:29.444Z', status: 'completed_with_warnings', timeout: 300000 }, + { id: 'k9067y2a1d4wcbae0cad38n0', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:19:53.244Z', created_at: '2020-04-14T17:19:31.379Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad', version: '7.14.0' }, process_expiration: '2020-04-14T17:24:39.883Z', started_at: '2020-04-14T17:19:39.883Z', status: 'completed', timeout: 300000 }, + { id: 'k9067s1m1d4wcbae0cdnvcms', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:19:36.822Z', created_at: '2020-04-14T17:19:23.578Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad', version: '7.14.0' }, process_expiration: '2020-04-14T17:24:25.247Z', started_at: '2020-04-14T17:19:25.247Z', status: 'completed', timeout: 300000 }, + { id: 'k9065q3s1d4wcbae0c00fxlh', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:18:03.910Z', created_at: '2020-04-14T17:17:47.752Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad', version: '7.14.0' }, process_expiration: '2020-04-14T17:22:50.379Z', started_at: '2020-04-14T17:17:50.379Z', status: 'completed', timeout: 300000 }, + { id: 'k905zdw11d34cbae0c3y6tzh', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:13:03.719Z', created_at: '2020-04-14T17:12:51.985Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad', version: '7.14.0' }, process_expiration: '2020-04-14T17:17:52.431Z', started_at: '2020-04-14T17:12:52.431Z', status: 'completed', timeout: 300000 }, + { id: 'k8t4ylcb07mi9d006214ifyg', index: '.reporting-2020.04.05', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-09T19:10:10.049Z', created_at: '2020-04-09T19:09:52.139Z', created_by: 'elastic', jobtype: 'PNG', kibana_id: 'f2e59b4e-f79b-4a48-8a7d-6d50a3c1d914', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'png', objectType: 'visualization' }, output: { content_type: 'image/png', size: 123456789 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 1575, width: 1423 }, id: 'png' }, objectType: 'visualization', title: 'count', version: '7.14.0' }, process_expiration: '2020-04-09T19:14:54.570Z', started_at: '2020-04-09T19:09:54.570Z', status: 'completed', timeout: 300000 }, ]; // prettier-ignore const reportingAPIClient = { diff --git a/x-pack/plugins/reporting/public/management/report_listing.tsx b/x-pack/plugins/reporting/public/management/report_listing.tsx index 9ba0026999137..4e183380a6b41 100644 --- a/x-pack/plugins/reporting/public/management/report_listing.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing.tsx @@ -16,39 +16,25 @@ import { EuiTextColor, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import moment from 'moment'; +import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; import { Component, default as React, Fragment } from 'react'; import { Subscription } from 'rxjs'; -import { ApplicationStart, ToastsSetup } from 'src/core/public'; -import { ILicense, LicensingPluginSetup } from '../../../licensing/public'; -import { JOB_STATUSES as JobStatuses } from '../../common/constants'; +import { ILicense } from '../../../licensing/public'; +import { REPORT_TABLE_ID, REPORT_TABLE_ROW_ID } from '../../common/constants'; import { Poller } from '../../common/poller'; import { durationToNumber } from '../../common/schema_utils'; -import { useIlmPolicyStatus, UseIlmPolicyStatusReturn } from '../lib/ilm_policy_status_context'; +import { useIlmPolicyStatus } from '../lib/ilm_policy_status_context'; import { Job } from '../lib/job'; import { checkLicense } from '../lib/license_check'; -import { ReportingAPIClient, useInternalApiClient } from '../lib/reporting_api_client'; -import { ClientConfigType } from '../plugin'; -import type { SharePluginSetup } from '../shared_imports'; +import { useInternalApiClient } from '../lib/reporting_api_client'; import { useKibana } from '../shared_imports'; -import { ReportDeleteButton, ReportDownloadButton, ReportErrorButton, ReportInfoButton } from './'; import { IlmPolicyLink } from './ilm_policy_link'; import { MigrateIlmPolicyCallOut } from './migrate_ilm_policy_callout'; +import { ReportDeleteButton } from './report_delete_button'; import { ReportDiagnostic } from './report_diagnostic'; - -export interface Props { - intl: InjectedIntl; - apiClient: ReportingAPIClient; - capabilities: ApplicationStart['capabilities']; - license$: LicensingPluginSetup['license$']; - pollConfig: ClientConfigType['poll']; - redirect: ApplicationStart['navigateToApp']; - navigateToUrl: ApplicationStart['navigateToUrl']; - toasts: ToastsSetup; - urlService: SharePluginSetup['url']; - ilmPolicyContextValue: UseIlmPolicyStatusReturn; -} +import { ReportDownloadButton } from './report_download_button'; +import { ReportInfoButton } from './report_info_button'; +import { ListingProps as Props } from './'; interface State { page: number; @@ -61,45 +47,6 @@ interface State { badLicenseMessage: string; } -const jobStatusLabelsMap = new Map([ - [ - JobStatuses.PENDING, - i18n.translate('xpack.reporting.jobStatuses.pendingText', { - defaultMessage: 'Pending', - }), - ], - [ - JobStatuses.PROCESSING, - i18n.translate('xpack.reporting.jobStatuses.processingText', { - defaultMessage: 'Processing', - }), - ], - [ - JobStatuses.COMPLETED, - i18n.translate('xpack.reporting.jobStatuses.completedText', { - defaultMessage: 'Completed', - }), - ], - [ - JobStatuses.WARNINGS, - i18n.translate('xpack.reporting.jobStatuses.warningText', { - defaultMessage: 'Completed with warnings', - }), - ], - [ - JobStatuses.FAILED, - i18n.translate('xpack.reporting.jobStatuses.failedText', { - defaultMessage: 'Failed', - }), - ], - [ - JobStatuses.CANCELLED, - i18n.translate('xpack.reporting.jobStatuses.cancelledText', { - defaultMessage: 'Cancelled', - }), - ], -]); - class ReportListingUi extends Component { private isInitialJobsFetch: boolean; private licenseSubscription?: Subscription; @@ -212,9 +159,9 @@ class ReportListingUi extends Component { this.setState((current) => ({ ...current, selectedJobs: jobs })); }; - private removeRecord = (record: Job) => { + private removeJob = (job: Job) => { const { jobs } = this.state; - const filtered = jobs.filter((j) => j.id !== record.id); + const filtered = jobs.filter((j) => j.id !== job.id); this.setState((current) => ({ ...current, jobs: filtered })); }; @@ -223,17 +170,17 @@ class ReportListingUi extends Component { if (selectedJobs.length === 0) return undefined; const performDelete = async () => { - for (const record of selectedJobs) { + for (const job of selectedJobs) { try { - await this.props.apiClient.deleteReport(record.id); - this.removeRecord(record); + await this.props.apiClient.deleteReport(job.id); + this.removeJob(job); this.props.toasts.addSuccess( this.props.intl.formatMessage( { id: 'xpack.reporting.listing.table.deleteConfim', defaultMessage: `The {reportTitle} report was deleted`, }, - { reportTitle: record.title } + { reportTitle: job.title } ) ); } catch (error) { @@ -316,15 +263,6 @@ class ReportListingUi extends Component { return this.state.showLinks && this.state.enableLinks; }; - private formatDate(timestamp: string) { - try { - return moment(timestamp).format('YYYY-MM-DD @ hh:mm A'); - } catch (error) { - // ignore parse error and display unformatted value - return timestamp; - } - } - private renderTable() { const { intl } = this.props; @@ -335,12 +273,12 @@ class ReportListingUi extends Component { id: 'xpack.reporting.listing.tableColumns.reportTitle', defaultMessage: 'Report', }), - render: (objectTitle: string, record: Job) => { + render: (objectTitle: string, job: Job) => { return (
{objectTitle}
- {record.objectType} + {job.objectType}
); @@ -352,17 +290,9 @@ class ReportListingUi extends Component { id: 'xpack.reporting.listing.tableColumns.createdAtTitle', defaultMessage: 'Created at', }), - render: (createdAt: string, record: Job) => { - if (record.created_by) { - return ( -
-
{this.formatDate(createdAt)}
- {record.created_by} -
- ); - } - return this.formatDate(createdAt); - }, + render: (_createdAt: string, job: Job) => ( +
{job.getCreatedAtLabel()}
+ ), }, { field: 'status', @@ -370,89 +300,9 @@ class ReportListingUi extends Component { id: 'xpack.reporting.listing.tableColumns.statusTitle', defaultMessage: 'Status', }), - render: (status: string, record: Job) => { - if (status === 'pending') { - return ( -
- -
- ); - } - - let maxSizeReached; - if (record.max_size_reached) { - maxSizeReached = ( - - - - ); - } - - let warnings; - if (record.warnings) { - warnings = ( - - - - - - ); - } - - let statusTimestamp; - if (status === JobStatuses.PROCESSING && record.started_at) { - statusTimestamp = this.formatDate(record.started_at); - } else if ( - record.completed_at && - ([ - JobStatuses.COMPLETED, - JobStatuses.FAILED, - JobStatuses.WARNINGS, - ] as string[]).includes(status) - ) { - statusTimestamp = this.formatDate(record.completed_at); - } - - let statusLabel = jobStatusLabelsMap.get(status as JobStatuses) || status; - - if (status === JobStatuses.PROCESSING) { - statusLabel = statusLabel + ` (attempt ${record.attempts} of ${record.max_attempts})`; - } - - if (statusTimestamp) { - return ( -
- {statusTimestamp}, - }} - /> - {maxSizeReached} - {warnings} -
- ); - } - - // unknown status - return ( -
- {statusLabel} - {maxSizeReached} -
- ); - }, + render: (_status: string, job: Job) => ( +
{job.getStatusLabel()}
+ ), }, { name: intl.formatMessage({ @@ -461,12 +311,11 @@ class ReportListingUi extends Component { }), actions: [ { - render: (record: Job) => { + render: (job: Job) => { return ( -
- - - +
+ +
); }, @@ -520,7 +369,8 @@ class ReportListingUi extends Component { selection={selection} isSelectable={true} onChange={this.onTableChange} - data-test-subj="reportJobListing" + data-test-subj={REPORT_TABLE_ID} + rowProps={() => ({ 'data-test-subj': REPORT_TABLE_ROW_ID })} /> ); diff --git a/x-pack/plugins/reporting/public/mocks.ts b/x-pack/plugins/reporting/public/mocks.ts index a6b6d835499c6..41b4d26dc5a59 100644 --- a/x-pack/plugins/reporting/public/mocks.ts +++ b/x-pack/plugins/reporting/public/mocks.ts @@ -6,6 +6,7 @@ */ import { coreMock } from 'src/core/public/mocks'; +import { ReportingAPIClient } from './lib/reporting_api_client'; import { ReportingSetup } from '.'; import { getDefaultLayoutSelectors } from '../common'; import { getSharedComponents } from './shared'; @@ -14,10 +15,11 @@ type Setup = jest.Mocked; const createSetupContract = (): Setup => { const coreSetup = coreMock.createSetup(); + const apiClient = new ReportingAPIClient(coreSetup.http, coreSetup.uiSettings, '7.15.0'); return { getDefaultLayoutSelectors: jest.fn().mockImplementation(getDefaultLayoutSelectors), usesUiCapabilities: jest.fn().mockImplementation(() => true), - components: getSharedComponents(coreSetup), + components: getSharedComponents(coreSetup, apiClient), }; }; diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts index dbd0421fdf9b0..45bd20df85660 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts @@ -8,13 +8,17 @@ import * as Rx from 'rxjs'; import { first } from 'rxjs/operators'; import { CoreStart } from 'src/core/public'; +import { coreMock } from '../../../../../src/core/public/mocks'; import { LicensingPluginSetup } from '../../../licensing/public'; +import { ReportingAPIClient } from '../lib/reporting_api_client'; import { ReportingCsvPanelAction } from './get_csv_panel_action'; type LicenseResults = 'valid' | 'invalid' | 'unavailable' | 'expired'; +const core = coreMock.createSetup(); +let apiClient: ReportingAPIClient; + describe('GetCsvReportPanelAction', () => { - let core: any; let context: any; let mockLicense$: any; let mockSearchSource: any; @@ -32,6 +36,9 @@ describe('GetCsvReportPanelAction', () => { }); beforeEach(() => { + apiClient = new ReportingAPIClient(core.http, core.uiSettings, '7.15.0'); + jest.spyOn(apiClient, 'createImmediateReport'); + mockLicense$ = (state: LicenseResults = 'valid') => { return (Rx.of({ check: jest.fn().mockImplementation(() => ({ state })), @@ -47,21 +54,6 @@ describe('GetCsvReportPanelAction', () => { null, ]; - core = { - http: { - post: jest.fn().mockImplementation(() => Promise.resolve(true)), - }, - notifications: { - toasts: { - addSuccess: jest.fn(), - addDanger: jest.fn(), - }, - }, - uiSettings: { - get: () => 'Browser', - }, - } as any; - mockSearchSource = { createCopy: () => mockSearchSource, removeField: jest.fn(), @@ -92,6 +84,7 @@ describe('GetCsvReportPanelAction', () => { it('translates empty embeddable context into job params', async () => { const panel = new ReportingCsvPanelAction({ core, + apiClient, license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: true, @@ -101,12 +94,14 @@ describe('GetCsvReportPanelAction', () => { await panel.execute(context); - expect(core.http.post).toHaveBeenCalledWith( - '/api/reporting/v1/generate/immediate/csv_searchsource', - { - body: '{"searchSource":{},"columns":[],"browserTimezone":"America/New_York"}', - } - ); + expect(apiClient.createImmediateReport).toHaveBeenCalledWith({ + browserTimezone: undefined, + columns: [], + objectType: 'downloadCsv', + searchSource: {}, + title: undefined, + version: '7.15.0', + }); }); it('translates embeddable context into job params', async () => { @@ -126,6 +121,7 @@ describe('GetCsvReportPanelAction', () => { const panel = new ReportingCsvPanelAction({ core, + apiClient, license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: true, @@ -135,18 +131,20 @@ describe('GetCsvReportPanelAction', () => { await panel.execute(context); - expect(core.http.post).toHaveBeenCalledWith( - '/api/reporting/v1/generate/immediate/csv_searchsource', - { - body: - '{"searchSource":{"testData":"testDataValue"},"columns":["column_a","column_b"],"browserTimezone":"America/New_York"}', - } - ); + expect(apiClient.createImmediateReport).toHaveBeenCalledWith({ + browserTimezone: undefined, + columns: ['column_a', 'column_b'], + objectType: 'downloadCsv', + searchSource: { testData: 'testDataValue' }, + title: undefined, + version: '7.15.0', + }); }); it('allows downloading for valid licenses', async () => { const panel = new ReportingCsvPanelAction({ core, + apiClient, license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: true, @@ -162,6 +160,7 @@ describe('GetCsvReportPanelAction', () => { it('shows a good old toastie when it successfully starts', async () => { const panel = new ReportingCsvPanelAction({ core, + apiClient, license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: true, @@ -176,14 +175,10 @@ describe('GetCsvReportPanelAction', () => { }); it('shows a bad old toastie when it successfully fails', async () => { - const coreFails = { - ...core, - http: { - post: jest.fn().mockImplementation(() => Promise.reject('No more ram!')), - }, - }; + apiClient.createImmediateReport = jest.fn().mockRejectedValue('No more ram!'); const panel = new ReportingCsvPanelAction({ - core: coreFails, + core, + apiClient, license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: true, @@ -200,6 +195,7 @@ describe('GetCsvReportPanelAction', () => { const licenseMock$ = mockLicense$('invalid'); const plugin = new ReportingCsvPanelAction({ core, + apiClient, license$: licenseMock$, startServices$: mockStartServices$, usesUiCapabilities: true, @@ -215,6 +211,7 @@ describe('GetCsvReportPanelAction', () => { it('sets a display and icon type', () => { const panel = new ReportingCsvPanelAction({ core, + apiClient, license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: true, @@ -230,6 +227,7 @@ describe('GetCsvReportPanelAction', () => { it(`doesn't allow downloads when UI capability is not enabled`, async () => { const plugin = new ReportingCsvPanelAction({ core, + apiClient, license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: true, @@ -248,6 +246,7 @@ describe('GetCsvReportPanelAction', () => { mockStartServices$ = new Rx.Subject(); const plugin = new ReportingCsvPanelAction({ core, + apiClient, license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: true, @@ -261,6 +260,7 @@ describe('GetCsvReportPanelAction', () => { it(`allows download when license is valid and deprecated roles config is enabled`, async () => { const plugin = new ReportingCsvPanelAction({ core, + apiClient, license$: mockLicense$(), startServices$: mockStartServices$, usesUiCapabilities: false, diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index 8a863e1ceaa65..8b6e258c06535 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -6,9 +6,8 @@ */ import { i18n } from '@kbn/i18n'; -import moment from 'moment-timezone'; import * as Rx from 'rxjs'; -import type { CoreSetup } from 'src/core/public'; +import type { CoreSetup, IUiSettingsClient, NotificationsSetup } from 'src/core/public'; import { CoreStart } from 'src/core/public'; import type { ISearchEmbeddable, SavedSearch } from '../../../../../src/plugins/discover/public'; import { @@ -20,9 +19,9 @@ import { ViewMode } from '../../../../../src/plugins/embeddable/public'; import type { UiActionsActionDefinition as ActionDefinition } from '../../../../../src/plugins/ui_actions/public'; import { IncompatibleActionError } from '../../../../../src/plugins/ui_actions/public'; import type { LicensingPluginSetup } from '../../../licensing/public'; -import { API_GENERATE_IMMEDIATE, CSV_REPORTING_ACTION } from '../../common/constants'; -import type { JobParamsDownloadCSV } from '../../server/export_types/csv_searchsource_immediate/types'; +import { CSV_REPORTING_ACTION } from '../../common/constants'; import { checkLicense } from '../lib/license_check'; +import { ReportingAPIClient } from '../lib/reporting_api_client'; function isSavedSearchEmbeddable( embeddable: IEmbeddable | ISearchEmbeddable @@ -35,6 +34,7 @@ interface ActionContext { } interface Params { + apiClient: ReportingAPIClient; core: CoreSetup; startServices$: Rx.Observable<[CoreStart, object, unknown]>; license$: LicensingPluginSetup['license$']; @@ -47,11 +47,16 @@ export class ReportingCsvPanelAction implements ActionDefinition public readonly id = CSV_REPORTING_ACTION; private licenseHasDownloadCsv: boolean = false; private capabilityHasDownloadCsv: boolean = false; - private core: CoreSetup; + private uiSettings: IUiSettingsClient; + private notifications: NotificationsSetup; + private apiClient: ReportingAPIClient; - constructor({ core, startServices$, license$, usesUiCapabilities }: Params) { + constructor({ core, startServices$, license$, usesUiCapabilities, apiClient }: Params) { this.isDownloading = false; - this.core = core; + + this.uiSettings = core.uiSettings; + this.notifications = core.notifications; + this.apiClient = apiClient; license$.subscribe((license) => { const results = license.check('reporting', 'basic'); @@ -83,7 +88,7 @@ export class ReportingCsvPanelAction implements ActionDefinition return await getSharingData( savedSearch.searchSource, savedSearch, // TODO: get unsaved state (using embeddale.searchScope): https://github.com/elastic/kibana/issues/43977 - this.core.uiSettings + this.uiSettings ); } @@ -111,24 +116,16 @@ export class ReportingCsvPanelAction implements ActionDefinition const savedSearch = embeddable.getSavedSearch(); const { columns, searchSource } = await this.getSearchSource(savedSearch, embeddable); - // If the TZ is set to the default "Browser", it will not be useful for - // server-side export. We need to derive the timezone and pass it as a param - // to the export API. - // TODO: create a helper utility in Reporting. This is repeated in a few places. - const kibanaTimezone = this.core.uiSettings.get('dateFormat:tz'); - const browserTimezone = kibanaTimezone === 'Browser' ? moment.tz.guess() : kibanaTimezone; - const immediateJobParams: JobParamsDownloadCSV = { + const immediateJobParams = this.apiClient.getDecoratedJobParams({ searchSource, columns, - browserTimezone, title: savedSearch.title, - }; - - const body = JSON.stringify(immediateJobParams); + objectType: 'downloadCsv', // FIXME: added for typescript, but immediate download job does not need objectType + }); this.isDownloading = true; - this.core.notifications.toasts.addSuccess({ + this.notifications.toasts.addSuccess({ title: i18n.translate('xpack.reporting.dashboard.csvDownloadStartedTitle', { defaultMessage: `CSV Download Started`, }), @@ -138,9 +135,9 @@ export class ReportingCsvPanelAction implements ActionDefinition 'data-test-subj': 'csvDownloadStarted', }); - await this.core.http - .post(`${API_GENERATE_IMMEDIATE}`, { body }) - .then((rawResponse: string) => { + await this.apiClient + .createImmediateReport(immediateJobParams) + .then((rawResponse) => { this.isDownloading = false; const download = `${savedSearch.title}.csv`; @@ -166,7 +163,7 @@ export class ReportingCsvPanelAction implements ActionDefinition private onGenerationFail(error: Error) { this.isDownloading = false; - this.core.notifications.toasts.addDanger({ + this.notifications.toasts.addDanger({ title: i18n.translate('xpack.reporting.dashboard.failedCsvDownloadTitle', { defaultMessage: `CSV download failed`, }), diff --git a/x-pack/plugins/reporting/public/plugin.ts b/x-pack/plugins/reporting/public/plugin.ts index 44ecc01bd1eb3..757f226532d95 100644 --- a/x-pack/plugins/reporting/public/plugin.ts +++ b/x-pack/plugins/reporting/public/plugin.ts @@ -11,6 +11,8 @@ import { catchError, filter, map, mergeMap, takeUntil } from 'rxjs/operators'; import { CoreSetup, CoreStart, + HttpSetup, + IUiSettingsClient, NotificationsSetup, Plugin, PluginInitializerContext, @@ -32,15 +34,14 @@ import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_ha import { getGeneralErrorToast } from './notifier'; import { ReportingCsvPanelAction } from './panel_actions/get_csv_panel_action'; import { getSharedComponents } from './shared'; -import { ReportingCsvShareProvider } from './share_context_menu/register_csv_reporting'; -import { reportingScreenshotShareProvider } from './share_context_menu/register_pdf_png_reporting'; - import type { SharePluginSetup, SharePluginStart, UiActionsSetup, UiActionsStart, } from './shared_imports'; +import { ReportingCsvShareProvider } from './share_context_menu/register_csv_reporting'; +import { reportingScreenshotShareProvider } from './share_context_menu/register_pdf_png_reporting'; export interface ClientConfigType { poll: { jobsRefresh: { interval: number; intervalErrorMultiplier: number } }; @@ -89,6 +90,8 @@ export class ReportingPublicPlugin ReportingPublicPluginSetupDendencies, ReportingPublicPluginStartDendencies > { + private kibanaVersion: string; + private apiClient?: ReportingAPIClient; private readonly stop$ = new Rx.ReplaySubject(1); private readonly title = i18n.translate('xpack.reporting.management.reportingTitle', { defaultMessage: 'Reporting', @@ -101,6 +104,17 @@ export class ReportingPublicPlugin constructor(initializerContext: PluginInitializerContext) { this.config = initializerContext.config.get(); + this.kibanaVersion = initializerContext.env.packageInfo.version; + } + + /* + * Use a single instance of ReportingAPIClient for all the reporting code + */ + private getApiClient(http: HttpSetup, uiSettings: IUiSettingsClient) { + if (!this.apiClient) { + this.apiClient = new ReportingAPIClient(http, uiSettings, this.kibanaVersion); + } + return this.apiClient; } private getContract(core?: CoreSetup) { @@ -108,7 +122,7 @@ export class ReportingPublicPlugin this.contract = { getDefaultLayoutSelectors, usesUiCapabilities: () => this.config.roles?.enabled === false, - components: getSharedComponents(core), + components: getSharedComponents(core, this.getApiClient(core.http, core.uiSettings)), }; } @@ -120,11 +134,11 @@ export class ReportingPublicPlugin } public setup(core: CoreSetup, setupDeps: ReportingPublicPluginSetupDendencies) { - const { http, getStartServices, uiSettings } = core; + const { getStartServices, uiSettings } = core; const { home, management, - licensing: { license$ }, + licensing: { license$ }, // FIXME: 'license$' is deprecated share, uiActions, } = setupDeps; @@ -132,7 +146,7 @@ export class ReportingPublicPlugin const startServices$ = Rx.from(getStartServices()); const usesUiCapabilities = !this.config.roles.enabled; - const apiClient = new ReportingAPIClient(http); + const apiClient = this.getApiClient(core.http, core.uiSettings); home.featureCatalogue.register({ id: 'reporting', @@ -181,7 +195,7 @@ export class ReportingPublicPlugin uiActions.addTriggerAction( CONTEXT_MENU_TRIGGER, - new ReportingCsvPanelAction({ core, startServices$, license$, usesUiCapabilities }) + new ReportingCsvPanelAction({ core, apiClient, startServices$, license$, usesUiCapabilities }) ); const reportingStart = this.getContract(core); @@ -213,8 +227,8 @@ export class ReportingPublicPlugin } public start(core: CoreStart) { - const { http, notifications } = core; - const apiClient = new ReportingAPIClient(http); + const { notifications } = core; + const apiClient = this.getApiClient(core.http, core.uiSettings); const streamHandler = new StreamHandler(notifications, apiClient); const interval = durationToNumber(this.config.poll.jobsRefresh.interval); Rx.timer(0, interval) diff --git a/x-pack/plugins/reporting/public/share_context_menu/__snapshots__/screen_capture_panel_content.test.tsx.snap b/x-pack/plugins/reporting/public/share_context_menu/__snapshots__/screen_capture_panel_content.test.tsx.snap index 83dc0c9e215b0..6f0fc18e90adc 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/__snapshots__/screen_capture_panel_content.test.tsx.snap +++ b/x-pack/plugins/reporting/public/share_context_menu/__snapshots__/screen_capture_panel_content.test.tsx.snap @@ -349,7 +349,7 @@ exports[`ScreenCapturePanelContent properly renders a view with "canvas" layout ; + usesUiCapabilities: boolean; +} + +export interface ReportingSharingData { + title: string; + layout: LayoutParams; +} + +export interface JobParamsProviderOptions { + sharingData: ReportingSharingData; + shareableUrl: string; + objectType: string; +} diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx index 7165fcf6f8681..040a1646ec1ba 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx @@ -6,35 +6,22 @@ */ import { i18n } from '@kbn/i18n'; -import moment from 'moment-timezone'; import React from 'react'; -import * as Rx from 'rxjs'; -import type { IUiSettingsClient, ToastsSetup } from 'src/core/public'; -import { CoreStart } from 'src/core/public'; import type { SearchSourceFields } from 'src/plugins/data/common'; +import { ExportPanelShareOpts } from '.'; import type { ShareContext } from '../../../../../src/plugins/share/public'; -import type { LicensingPluginSetup } from '../../../licensing/public'; import { CSV_JOB_TYPE } from '../../common/constants'; -import type { JobParamsCSV } from '../../server/export_types/csv_searchsource/types'; import { checkLicense } from '../lib/license_check'; -import type { ReportingAPIClient } from '../lib/reporting_api_client'; import { ReportingPanelContent } from './reporting_panel_content_lazy'; export const ReportingCsvShareProvider = ({ apiClient, toasts, + uiSettings, license$, startServices$, - uiSettings, usesUiCapabilities, -}: { - apiClient: ReportingAPIClient; - toasts: ToastsSetup; - license$: LicensingPluginSetup['license$']; - startServices$: Rx.Observable<[CoreStart, object, unknown]>; - uiSettings: IUiSettingsClient; - usesUiCapabilities: boolean; -}) => { +}: ExportPanelShareOpts) => { let licenseToolTipContent = ''; let licenseHasCsvReporting = false; let licenseDisabled = true; @@ -56,22 +43,12 @@ export const ReportingCsvShareProvider = ({ capabilityHasCsvReporting = true; // deprecated } - // If the TZ is set to the default "Browser", it will not be useful for - // server-side export. We need to derive the timezone and pass it as a param - // to the export API. - // TODO: create a helper utility in Reporting. This is repeated in a few places. - const browserTimezone = - uiSettings.get('dateFormat:tz') === 'Browser' - ? moment.tz.guess() - : uiSettings.get('dateFormat:tz'); - const getShareMenuItems = ({ objectType, objectId, sharingData, onClose }: ShareContext) => { if ('search' !== objectType) { return []; } - const jobParams: JobParamsCSV = { - browserTimezone, + const jobParams = { title: sharingData.title as string, objectType, searchSource: sharingData.searchSource as SearchSourceFields, @@ -104,6 +81,7 @@ export const ReportingCsvShareProvider = ({ requiresSavedState={false} apiClient={apiClient} toasts={toasts} + uiSettings={uiSettings} reportType={CSV_JOB_TYPE} layoutId={undefined} objectId={objectId} diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx index eb80f64be55e1..b37e31578be6d 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx @@ -6,83 +6,53 @@ */ import { i18n } from '@kbn/i18n'; -import moment from 'moment-timezone'; import React from 'react'; -import * as Rx from 'rxjs'; -import type { IUiSettingsClient, ToastsSetup } from 'src/core/public'; -import { CoreStart } from 'src/core/public'; import { ShareContext } from 'src/plugins/share/public'; -import type { LicensingPluginSetup } from '../../../licensing/public'; -import type { LayoutParams } from '../../common/types'; -import type { JobParamsPNG } from '../../server/export_types/png/types'; -import type { JobParamsPDF } from '../../server/export_types/printable_pdf/types'; +import { ExportPanelShareOpts, JobParamsProviderOptions, ReportingSharingData } from '.'; import { checkLicense } from '../lib/license_check'; -import type { ReportingAPIClient } from '../lib/reporting_api_client'; +import { ReportingAPIClient } from '../lib/reporting_api_client'; import { ScreenCapturePanelContent } from './screen_capture_panel_content_lazy'; -interface JobParamsProviderOptions { - shareableUrl: string; - apiClient: ReportingAPIClient; - objectType: string; - browserTimezone: string; - sharingData: Record; -} - -const jobParamsProvider = ({ - objectType, - browserTimezone, - sharingData, -}: JobParamsProviderOptions) => { - return { +const getJobParams = ( + apiClient: ReportingAPIClient, + opts: JobParamsProviderOptions, + type: 'pdf' | 'png' +) => () => { + const { objectType, - browserTimezone, - layout: sharingData.layout as LayoutParams, - title: sharingData.title as string, + sharingData: { title, layout }, + } = opts; + + const baseParams = { + objectType, + layout, + title, }; -}; -const getPdfJobParams = (opts: JobParamsProviderOptions) => (): JobParamsPDF => { // Relative URL must have URL prefix (Spaces ID prefix), but not server basePath // Replace hashes with original RISON values. const relativeUrl = opts.shareableUrl.replace( - window.location.origin + opts.apiClient.getServerBasePath(), + window.location.origin + apiClient.getServerBasePath(), '' ); - return { - ...jobParamsProvider(opts), - relativeUrls: [relativeUrl], // multi URL for PDF - }; -}; - -const getPngJobParams = (opts: JobParamsProviderOptions) => (): JobParamsPNG => { - // Replace hashes with original RISON values. - const relativeUrl = opts.shareableUrl.replace( - window.location.origin + opts.apiClient.getServerBasePath(), - '' - ); + if (type === 'pdf') { + // multi URL for PDF + return { ...baseParams, relativeUrls: [relativeUrl] }; + } - return { - ...jobParamsProvider(opts), - relativeUrl, // single URL for PNG - }; + // single URL for PNG + return { ...baseParams, relativeUrl }; }; export const reportingScreenshotShareProvider = ({ apiClient, toasts, + uiSettings, license$, startServices$, - uiSettings, usesUiCapabilities, -}: { - apiClient: ReportingAPIClient; - toasts: ToastsSetup; - license$: LicensingPluginSetup['license$']; - startServices$: Rx.Observable<[CoreStart, object, unknown]>; - uiSettings: IUiSettingsClient; - usesUiCapabilities: boolean; -}) => { +}: ExportPanelShareOpts) => { let licenseToolTipContent = ''; let licenseDisabled = true; let licenseHasScreenshotReporting = false; @@ -110,22 +80,13 @@ export const reportingScreenshotShareProvider = ({ capabilityHasVisualizeScreenshotReporting = true; } - // If the TZ is set to the default "Browser", it will not be useful for - // server-side export. We need to derive the timezone and pass it as a param - // to the export API. - // TODO: create a helper utility in Reporting. This is repeated in a few places. - const browserTimezone = - uiSettings.get('dateFormat:tz') === 'Browser' - ? moment.tz.guess() - : uiSettings.get('dateFormat:tz'); - const getShareMenuItems = ({ objectType, objectId, - sharingData, isDirty, onClose, shareableUrl, + ...shareOpts }: ShareContext) => { if (!licenseHasScreenshotReporting) { return []; @@ -143,6 +104,7 @@ export const reportingScreenshotShareProvider = ({ return []; } + const { sharingData } = (shareOpts as unknown) as { sharingData: ReportingSharingData }; const shareActions = []; const pngPanelTitle = i18n.translate('xpack.reporting.shareContextMenu.pngReportsButtonLabel', { @@ -165,16 +127,11 @@ export const reportingScreenshotShareProvider = ({ @@ -202,17 +159,12 @@ export const reportingScreenshotShareProvider = ({ diff --git a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.test.tsx b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.test.tsx index 6c5b8df104ecd..6ad894bf3ac2f 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.test.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.test.tsx @@ -7,26 +7,56 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; -import { notificationServiceMock } from 'src/core/public/mocks'; - -import { ReportingPanelContent, Props } from './reporting_panel_content'; +import { + httpServiceMock, + notificationServiceMock, + uiSettingsServiceMock, +} from 'src/core/public/mocks'; +import { ReportingAPIClient } from '../lib/reporting_api_client'; +import { ReportingPanelContent, ReportingPanelProps as Props } from './reporting_panel_content'; describe('ReportingPanelContent', () => { - const mountComponent = (props: Partial) => + const props: Partial = { + layoutId: 'super_cool_layout_id_X', + }; + const jobParams = { + appState: 'very_cool_app_state_X', + objectType: 'noice_object', + title: 'ultimate_title', + }; + const toasts = notificationServiceMock.createSetupContract().toasts; + const http = httpServiceMock.createSetupContract(); + const uiSettings = uiSettingsServiceMock.createSetupContract(); + let apiClient: ReportingAPIClient; + + beforeEach(() => { + props.layoutId = 'super_cool_layout_id_X'; + uiSettings.get.mockImplementation((key: string) => { + switch (key) { + case 'dateFormat:tz': + return 'Mars'; + } + }); + apiClient = new ReportingAPIClient(http, uiSettings, '7.15.0-test'); + }); + + const mountComponent = (newProps: Partial) => mountWithIntl( 'test' } as any} - toasts={notificationServiceMock.createSetupContract().toasts} + objectId="my-object-id" + layoutId={props.layoutId} + getJobParams={() => jobParams} + apiClient={apiClient} + toasts={toasts} + uiSettings={uiSettings} {...props} + {...newProps} /> ); + describe('saved state', () => { it('prevents generating reports when saving is required and we have unsaved changes', () => { const wrapper = mountComponent({ @@ -51,5 +81,20 @@ describe('ReportingPanelContent', () => { false ); }); + + it('changing the layout triggers refreshing the state with the latest job params', () => { + const wrapper = mountComponent({ requiresSavedState: false }); + wrapper.update(); + expect(wrapper.find('EuiCopy').prop('textToCopy')).toMatchInlineSnapshot( + `"http://localhost/api/reporting/generate/test?jobParams=%28appState%3Avery_cool_app_state_X%2CbrowserTimezone%3AMars%2CobjectType%3Anoice_object%2Ctitle%3Aultimate_title%2Cversion%3A%277.15.0-test%27%29"` + ); + + jobParams.appState = 'very_NOT_cool_app_state_Y'; + wrapper.setProps({ layoutId: 'super_cool_layout_id_Y' }); // update the component internal state + wrapper.update(); + expect(wrapper.find('EuiCopy').prop('textToCopy')).toMatchInlineSnapshot( + `"http://localhost/api/reporting/generate/test?jobParams=%28appState%3Avery_NOT_cool_app_state_Y%2CbrowserTimezone%3AMars%2CobjectType%3Anoice_object%2Ctitle%3Aultimate_title%2Cversion%3A%277.15.0-test%27%29"` + ); + }); }); }); diff --git a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx index 4d7828b789407..af6cd0010de09 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx @@ -18,29 +18,30 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component, ReactElement } from 'react'; -import { ToastsSetup } from 'src/core/public'; +import { ToastsSetup, IUiSettingsClient } from 'src/core/public'; import url from 'url'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { CSV_REPORT_TYPE, PDF_REPORT_TYPE, PNG_REPORT_TYPE } from '../../common/constants'; import { BaseParams } from '../../common/types'; import { ReportingAPIClient } from '../lib/reporting_api_client'; -export interface Props { +export interface ReportingPanelProps { apiClient: ReportingAPIClient; toasts: ToastsSetup; + uiSettings: IUiSettingsClient; reportType: string; - /** Whether the report to be generated requires saved state that is not captured in the URL submitted to the report generator. **/ - requiresSavedState: boolean; - layoutId: string | undefined; + requiresSavedState: boolean; // Whether the report to be generated requires saved state that is not captured in the URL submitted to the report generator. + layoutId?: string; objectId?: string; - getJobParams: () => BaseParams; + getJobParams: () => Omit; options?: ReactElement | null; isDirty?: boolean; onClose?: () => void; - intl: InjectedIntl; } +export type Props = ReportingPanelProps & { intl: InjectedIntl }; + interface State { isStale: boolean; absoluteUrl: string; @@ -68,12 +69,12 @@ class ReportingPanelContentUi extends Component { private getAbsoluteReportGenerationUrl = (props: Props) => { const relativePath = this.props.apiClient.getReportingJobPath( props.reportType, - props.getJobParams() + this.props.apiClient.getDecoratedJobParams(this.props.getJobParams()) ); - return url.resolve(window.location.href, relativePath); + return url.resolve(window.location.href, relativePath); // FIXME: '(from: string, to: string): string' is deprecated }; - public componentDidUpdate(prevProps: Props, prevState: State) { + public componentDidUpdate(_prevProps: Props, prevState: State) { if (this.props.layoutId && this.props.layoutId !== prevState.layoutId) { this.setState({ ...prevState, @@ -231,9 +232,12 @@ class ReportingPanelContentUi extends Component { private createReportingJob = () => { const { intl } = this.props; + const decoratedJobParams = this.props.apiClient.getDecoratedJobParams( + this.props.getJobParams() + ); return this.props.apiClient - .createReportingJob(this.props.reportType, this.props.getJobParams()) + .createReportingJob(this.props.reportType, decoratedJobParams) .then(() => { this.props.toasts.addSuccess({ title: intl.formatMessage( diff --git a/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.test.tsx b/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.test.tsx index a023eae512d54..3fdb2c7e98f82 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.test.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.test.tsx @@ -8,25 +8,33 @@ import { mount } from 'enzyme'; import React from 'react'; import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; -import { coreMock } from '../../../../../src/core/public/mocks'; -import { BaseParams } from '../../common/types'; +import { coreMock } from 'src/core/public/mocks'; import { ReportingAPIClient } from '../lib/reporting_api_client'; import { ScreenCapturePanelContent } from './screen_capture_panel_content'; -const getJobParamsDefault: () => BaseParams = () => ({ +const { http, uiSettings, ...coreSetup } = coreMock.createSetup(); +uiSettings.get.mockImplementation((key: string) => { + switch (key) { + case 'dateFormat:tz': + return 'Mars'; + } +}); +const apiClient = new ReportingAPIClient(http, uiSettings, '7.15.0'); + +const getJobParamsDefault = () => ({ objectType: 'test-object-type', title: 'Test Report Title', browserTimezone: 'America/New_York', }); test('ScreenCapturePanelContent renders the default view properly', () => { - const coreSetup = coreMock.createSetup(); const component = mount( @@ -38,14 +46,14 @@ test('ScreenCapturePanelContent renders the default view properly', () => { }); test('ScreenCapturePanelContent properly renders a view with "canvas" layout option', () => { - const coreSetup = coreMock.createSetup(); const component = mount( @@ -56,14 +64,14 @@ test('ScreenCapturePanelContent properly renders a view with "canvas" layout opt }); test('ScreenCapturePanelContent properly renders a view with "print" layout option', () => { - const coreSetup = coreMock.createSetup(); const component = mount( @@ -72,3 +80,22 @@ test('ScreenCapturePanelContent properly renders a view with "print" layout opti expect(component.find('EuiForm')).toMatchSnapshot(); expect(component.text()).toMatch('Optimize for printing'); }); + +test('ScreenCapturePanelContent decorated job params are visible in the POST URL', () => { + const component = mount( + + + + ); + + expect(component.find('EuiCopy').prop('textToCopy')).toMatchInlineSnapshot( + `"http://localhost/api/reporting/generate/Analytical%20App?jobParams=%28browserTimezone%3AAmerica%2FNew_York%2Clayout%3A%28dimensions%3A%28height%3A768%2Cwidth%3A1024%29%2Cid%3Apreserve_layout%2Cselectors%3A%28itemsCountAttribute%3Adata-shared-items-count%2CrenderComplete%3A%5Bdata-shared-item%5D%2Cscreenshot%3A%5Bdata-shared-items-container%5D%2CtimefilterDurationAttribute%3Adata-shared-timefilter-duration%29%29%2CobjectType%3Atest-object-type%2Ctitle%3A%27Test%20Report%20Title%27%2Cversion%3A%277.15.0%27%29"` + ); +}); diff --git a/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.tsx b/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.tsx index fd6003f8656e8..73c4d10856a53 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.tsx @@ -7,24 +7,13 @@ import { EuiFormRow, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import moment from 'moment'; import React, { Component } from 'react'; -import { ToastsSetup } from 'src/core/public'; import { getDefaultLayoutSelectors } from '../../common'; -import { BaseParams, LayoutParams } from '../../common/types'; -import { ReportingAPIClient } from '../lib/reporting_api_client'; -import { ReportingPanelContent } from './reporting_panel_content'; - -export interface Props { - apiClient: ReportingAPIClient; - toasts: ToastsSetup; - reportType: string; +import { LayoutParams } from '../../common/types'; +import { ReportingPanelContent, ReportingPanelProps } from './reporting_panel_content'; + +export interface Props extends ReportingPanelProps { layoutOption?: 'canvas' | 'print'; - objectId?: string; - getJobParams: () => BaseParams; - requiresSavedState: boolean; - isDirty?: boolean; - onClose?: () => void; } interface State { @@ -45,16 +34,10 @@ export class ScreenCapturePanelContent extends Component { public render() { return ( ); } @@ -147,17 +130,10 @@ export class ScreenCapturePanelContent extends Component { return { id: 'preserve_layout', dimensions, selectors }; }; - private getJobParams = (): Required => { - const outerParams = this.props.getJobParams(); - let browserTimezone = outerParams.browserTimezone; - if (!browserTimezone) { - browserTimezone = moment.tz.guess(); - } - + private getJobParams = () => { return { ...this.props.getJobParams(), layout: this.getLayout(), - browserTimezone, }; }; } diff --git a/x-pack/plugins/reporting/public/shared/get_shared_components.tsx b/x-pack/plugins/reporting/public/shared/get_shared_components.tsx index 87ddf0cfdb389..659eaf2678164 100644 --- a/x-pack/plugins/reporting/public/shared/get_shared_components.tsx +++ b/x-pack/plugins/reporting/public/shared/get_shared_components.tsx @@ -23,7 +23,7 @@ type PropsPDF = Pick & * This is not planned to expand, as work is to be done on moving the export-type implementations out of Reporting * Related Discuss issue: https://github.com/elastic/kibana/issues/101422 */ -export function getSharedComponents(core: CoreSetup) { +export function getSharedComponents(core: CoreSetup, apiClient: ReportingAPIClient) { return { ReportingPanelPDF(props: PropsPDF) { return ( @@ -31,8 +31,9 @@ export function getSharedComponents(core: CoreSetup) { layoutOption={props.layoutOption} requiresSavedState={false} reportType={PDF_REPORT_TYPE} - apiClient={new ReportingAPIClient(core.http)} + apiClient={apiClient} toasts={core.notifications.toasts} + uiSettings={core.uiSettings} {...props} /> ); diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index eb2abf4036c03..7aaa9c78602a9 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -17,7 +17,6 @@ import { InnerSubscriber } from 'rxjs/internal/InnerSubscriber'; import { ignoreElements, map, mergeMap, tap } from 'rxjs/operators'; import { getChromiumDisconnectedError } from '../'; import { ReportingCore } from '../../..'; -import { BROWSER_TYPE } from '../../../../common/constants'; import { durationToNumber } from '../../../../common/schema_utils'; import { CaptureConfig } from '../../../../server/types'; import { LevelLogger } from '../../../lib'; @@ -70,7 +69,7 @@ export class HeadlessChromiumDriverFactory { }); } - type = BROWSER_TYPE; + type = 'chromium'; /* * Return an observable to objects which will drive screenshot capture for a page diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index b7f3ebe9dcfa8..708b9b1bdbea5 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -58,6 +58,7 @@ export interface ReportingInternalStart { } export class ReportingCore { + private kibanaVersion: string; private pluginSetupDeps?: ReportingInternalSetup; private pluginStartDeps?: ReportingInternalStart; private readonly pluginSetup$ = new Rx.ReplaySubject(); // observe async background setupDeps and config each are done @@ -72,6 +73,7 @@ export class ReportingCore { public getContract: () => ReportingSetup; constructor(private logger: LevelLogger, context: PluginInitializerContext) { + this.kibanaVersion = context.env.packageInfo.version; const syncConfig = context.config.get(); this.deprecatedAllowedRoles = syncConfig.roles.enabled ? syncConfig.roles.allow : false; this.executeTask = new ExecuteReportTask(this, syncConfig, this.logger); @@ -84,6 +86,10 @@ export class ReportingCore { this.executing = new Set(); } + public getKibanaVersion() { + return this.kibanaVersion; + } + /* * Register setupDeps */ diff --git a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts index 32b5370371cce..0e8a7016b853b 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts @@ -175,7 +175,7 @@ describe('CSV Execute Job', function () { ); expect(mockEsClient.scroll).toHaveBeenCalledWith( - expect.objectContaining({ scroll_id: scrollId }) + expect.objectContaining({ body: { scroll_id: scrollId } }) ); }); @@ -261,7 +261,7 @@ describe('CSV Execute Job', function () { ); expect(mockEsClient.clearScroll).toHaveBeenCalledWith( - expect.objectContaining({ scroll_id: lastScrollId }) + expect.objectContaining({ body: { scroll_id: lastScrollId } }) ); }); @@ -295,7 +295,7 @@ describe('CSV Execute Job', function () { ); expect(mockEsClient.clearScroll).toHaveBeenCalledWith( - expect.objectContaining({ scroll_id: lastScrollId }) + expect.objectContaining({ body: { scroll_id: lastScrollId } }) ); }); }); @@ -753,7 +753,7 @@ describe('CSV Execute Job', function () { expect(mockEsClient.clearScroll).toHaveBeenCalledWith( expect.objectContaining({ - scroll_id: scrollId, + body: { scroll_id: scrollId }, }) ); }); @@ -1150,7 +1150,7 @@ describe('CSV Execute Job', function () { await runTask('job123', jobParams, cancellationToken); expect(mockEsClient.scroll).toHaveBeenCalledWith( - expect.objectContaining({ scroll: scrollDuration }) + expect.objectContaining({ body: { scroll: scrollDuration, scroll_id: 'scrollId' } }) ); }); }); diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.ts index 72935e64dd6b5..9014e4f85b3b2 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/hit_iterator.ts @@ -60,12 +60,14 @@ export function createHitIterator(logger: LevelLogger) { ); } - async function scroll(scrollId: string | undefined) { + async function scroll(scrollId: string) { logger.debug('executing scroll request'); return parseResponse( await elasticsearchClient.scroll({ - scroll_id: scrollId, - scroll: scrollSettings.duration, + body: { + scroll_id: scrollId, + scroll: scrollSettings.duration, + }, }) ); } @@ -74,7 +76,7 @@ export function createHitIterator(logger: LevelLogger) { logger.debug('executing clearScroll request'); try { await elasticsearchClient.clearScroll({ - scroll_id: scrollId, + body: { scroll_id: scrollId }, }); } catch (err) { // Do not throw the error, as the job can still be completed successfully diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts index c9d57370ab766..b96828bb06334 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts @@ -59,6 +59,7 @@ test('gets the csv content from job parameters', async () => { searchSource: {}, objectType: 'search', title: 'Test Search', + version: '7.13.0', }, new CancellationToken() ); diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts index 254bc0ae21f6c..7eaf1ef95c149 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts @@ -6,12 +6,13 @@ */ import { i18n } from '@kbn/i18n'; -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { IScopedClusterClient, IUiSettingsClient } from 'src/core/server'; import { IScopedSearchClient } from 'src/plugins/data/server'; import { Datatable } from 'src/plugins/expressions/server'; import { ReportingConfig } from '../../..'; import { + cellHasFormulas, ES_SEARCH_STRATEGY, FieldFormat, FieldFormatConfig, @@ -22,7 +23,6 @@ import { SearchFieldValue, SearchSourceFields, tabifyDocs, - cellHasFormulas, } from '../../../../../../../src/plugins/data/common'; import { KbnServerError } from '../../../../../../../src/plugins/kibana_utils/server'; import { CancellationToken } from '../../../../common'; @@ -68,7 +68,7 @@ export class CsvGenerator { private csvRowCount = 0; constructor( - private job: JobParamsCSV, + private job: Omit, private config: ReportingConfig, private clients: Clients, private dependencies: Dependencies, @@ -93,7 +93,7 @@ export class CsvGenerator { }; const results = ( await this.clients.data.search(searchParams, { strategy: ES_SEARCH_STRATEGY }).toPromise() - ).rawResponse as SearchResponse; + ).rawResponse as estypes.SearchResponse; return results; } @@ -107,7 +107,7 @@ export class CsvGenerator { scroll_id: scrollId, }, }) - ).body as SearchResponse; + ).body; return results; } @@ -219,7 +219,6 @@ export class CsvGenerator { */ private generateHeader( columns: string[], - table: Datatable, builder: MaxSizeStringBuilder, settings: CsvExportSettings ) { @@ -321,13 +320,13 @@ export class CsvGenerator { if (this.cancellationToken.isCancelled()) { break; } - let results: SearchResponse | undefined; + let results: estypes.SearchResponse | undefined; if (scrollId == null) { // open a scroll cursor in Elasticsearch results = await this.scan(index, searchSource, scrollSettings); scrollId = results?._scroll_id; if (results.hits?.total != null) { - totalRecords = results.hits.total; + totalRecords = results.hits.total as number; this.logger.debug(`Total search results: ${totalRecords}`); } } else { @@ -357,7 +356,7 @@ export class CsvGenerator { if (first) { first = false; - this.generateHeader(columns, table, builder, settings); + this.generateHeader(columns, builder, settings); } if (table.rows.length < 1) { diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/types.d.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/types.d.ts index d2a9e2b5bf783..170b03c2dfbff 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/types.d.ts @@ -11,7 +11,6 @@ import type { BaseParams, BasePayload } from '../../types'; export type RawValue = string | object | null | undefined; interface BaseParamsCSV { - browserTimezone: string; searchSource: SearchSourceFields; columns?: string[]; } diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts index c8475e85bd847..e59c38e16ab47 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts @@ -32,7 +32,7 @@ export const runTaskFnFactory: RunTaskFnFactory = function e const config = reporting.getConfig(); const logger = parentLogger.clone([CSV_SEARCHSOURCE_IMMEDIATE_TYPE, 'execute-job']); - return async function runTask(jobId, immediateJobParams, context, req) { + return async function runTask(_jobId, immediateJobParams, context, req) { const job = { objectType: 'immediate-search', ...immediateJobParams, diff --git a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts index 488a339e3ef4b..cbfbc4a4d34b2 100644 --- a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts @@ -16,24 +16,16 @@ export const createJobFnFactory: CreateJobFnFactory< const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); - return async function createJob( - { objectType, title, relativeUrl, browserTimezone, layout }, - context, - req - ) { + return async function createJob(jobParams, _context, req) { const serializedEncryptedHeaders = await crypto.encrypt(req.headers); - validateUrls([relativeUrl]); + validateUrls([jobParams.relativeUrl]); return { headers: serializedEncryptedHeaders, spaceId: reporting.getSpaceId(req, logger), - objectType, - title, - relativeUrl, - browserTimezone, - layout, forceNow: new Date().toISOString(), + ...jobParams, }; }; }; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts index c0f30f96415f4..9dac1560ddbdc 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts @@ -16,24 +16,16 @@ export const createJobFnFactory: CreateJobFnFactory< const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); - return async function createJob( - { title, relativeUrls, browserTimezone, layout, objectType }, - context, - req - ) { + return async function createJob(jobParams, _context, req) { const serializedEncryptedHeaders = await crypto.encrypt(req.headers); - validateUrls(relativeUrls); + validateUrls(jobParams.relativeUrls); return { headers: serializedEncryptedHeaders, spaceId: reporting.getSpaceId(req, logger), - browserTimezone, forceNow: new Date().toISOString(), - layout, - relativeUrls, - title, - objectType, + ...jobParams, }; }; }; diff --git a/x-pack/plugins/reporting/server/lib/check_params_version.ts b/x-pack/plugins/reporting/server/lib/check_params_version.ts new file mode 100644 index 0000000000000..7298384b87571 --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/check_params_version.ts @@ -0,0 +1,20 @@ +/* + * 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 { UNVERSIONED_VERSION } from '../../common/constants'; +import type { BaseParams } from '../../common/types'; +import type { LevelLogger } from './'; + +export function checkParamsVersion(jobParams: BaseParams, logger: LevelLogger) { + if (jobParams.version) { + logger.debug(`Using reporting job params v${jobParams.version}`); + return jobParams.version; + } + + logger.warning(`No version provided in report job params. Assuming ${UNVERSIONED_VERSION}`); + return UNVERSIONED_VERSION; +} diff --git a/x-pack/plugins/reporting/server/lib/enqueue_job.test.ts b/x-pack/plugins/reporting/server/lib/enqueue_job.test.ts index d9d1815835baa..abfa2b88258fc 100644 --- a/x-pack/plugins/reporting/server/lib/enqueue_job.test.ts +++ b/x-pack/plugins/reporting/server/lib/enqueue_job.test.ts @@ -14,17 +14,28 @@ import { createMockLevelLogger, createMockReportingCore, } from '../test_helpers'; -import { BasePayload, ReportingRequestHandlerContext } from '../types'; +import { ReportingRequestHandlerContext } from '../types'; import { ExportTypesRegistry, ReportingStore } from './'; import { enqueueJobFactory } from './enqueue_job'; import { Report } from './store'; -import { TaskRunResult } from './tasks'; describe('Enqueue Job', () => { const logger = createMockLevelLogger(); let mockReporting: ReportingCore; let mockExportTypesRegistry: ExportTypesRegistry; + const mockBaseParams = { + browserTimezone: 'UTC', + headers: 'cool_encrypted_headers', + objectType: 'cool_object_type', + title: 'cool_title', + version: 'unknown' as any, + }; + + beforeEach(() => { + mockBaseParams.version = '7.15.0-test'; + }); + beforeAll(async () => { mockExportTypesRegistry = new ExportTypesRegistry(); mockExportTypesRegistry.register({ @@ -34,10 +45,8 @@ describe('Enqueue Job', () => { jobContentEncoding: 'base64', jobContentExtension: 'pdf', validLicenses: ['turquoise'], - createJobFnFactory: () => async () => - (({ createJobTest: { test1: 'yes' } } as unknown) as BasePayload), - runTaskFnFactory: () => async () => - (({ runParamsTest: { test2: 'yes' } } as unknown) as TaskRunResult), + createJobFnFactory: () => async () => mockBaseParams, + runTaskFnFactory: jest.fn(), }); mockReporting = await createMockReportingCore(createMockConfigSchema()); mockReporting.getExportTypesRegistry = () => mockExportTypesRegistry; @@ -66,26 +75,59 @@ describe('Enqueue Job', () => { const enqueueJob = enqueueJobFactory(mockReporting, logger); const report = await enqueueJob( 'printablePdf', - { - objectType: 'visualization', - title: 'cool-viz', - }, + mockBaseParams, false, ({} as unknown) as ReportingRequestHandlerContext, ({} as unknown) as KibanaRequest ); - expect(report).toMatchObject({ - _id: expect.any(String), - _index: '.reporting-foo-index-234', - attempts: 0, - created_by: false, - created_at: expect.any(String), - jobtype: 'printable_pdf', - meta: { objectType: 'visualization' }, - output: null, - payload: { createJobTest: { test1: 'yes' } }, - status: 'pending', - }); + const { _id, created_at: _created_at, ...snapObj } = report; + expect(snapObj).toMatchInlineSnapshot(` + Object { + "_index": ".reporting-foo-index-234", + "_primary_term": undefined, + "_seq_no": undefined, + "attempts": 0, + "browser_type": undefined, + "completed_at": undefined, + "created_by": false, + "jobtype": "printable_pdf", + "kibana_id": undefined, + "kibana_name": undefined, + "max_attempts": undefined, + "meta": Object { + "layout": undefined, + "objectType": "cool_object_type", + }, + "migration_version": "7.14.0", + "output": null, + "payload": Object { + "browserTimezone": "UTC", + "headers": "cool_encrypted_headers", + "objectType": "cool_object_type", + "title": "cool_title", + "version": "7.15.0-test", + }, + "process_expiration": undefined, + "started_at": undefined, + "status": "pending", + "timeout": undefined, + } + `); + }); + + it('provides a default kibana version field for older POST URLs', async () => { + const enqueueJob = enqueueJobFactory(mockReporting, logger); + mockBaseParams.version = undefined; + const report = await enqueueJob( + 'printablePdf', + mockBaseParams, + false, + ({} as unknown) as ReportingRequestHandlerContext, + ({} as unknown) as KibanaRequest + ); + + const { _id, created_at: _created_at, ...snapObj } = report; + expect(snapObj.payload.version).toBe('7.14.0'); }); }); diff --git a/x-pack/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/plugins/reporting/server/lib/enqueue_job.ts index ec2e443d86c80..1c73b0d925ad0 100644 --- a/x-pack/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/plugins/reporting/server/lib/enqueue_job.ts @@ -7,10 +7,10 @@ import { KibanaRequest } from 'src/core/server'; import { ReportingCore } from '../'; +import type { ReportingRequestHandlerContext } from '../types'; import { BaseParams, ReportingUser } from '../types'; -import { LevelLogger } from './'; +import { checkParamsVersion, LevelLogger } from './'; import { Report } from './store'; -import type { ReportingRequestHandlerContext } from '../types'; export type EnqueueJobFn = ( exportTypeId: string, @@ -47,6 +47,7 @@ export function enqueueJobFactory( reporting.getStore(), ]); + jobParams.version = checkParamsVersion(jobParams, logger); const job = await createJob!(jobParams, context, request); // 1. Add the report to ReportingStore to show as pending diff --git a/x-pack/plugins/reporting/server/lib/index.ts b/x-pack/plugins/reporting/server/lib/index.ts index b2a2a1edcd6a5..37f57d97d3d4c 100644 --- a/x-pack/plugins/reporting/server/lib/index.ts +++ b/x-pack/plugins/reporting/server/lib/index.ts @@ -6,6 +6,7 @@ */ export { checkLicense } from './check_license'; +export { checkParamsVersion } from './check_params_version'; export { cryptoFactory } from './crypto'; export { ExportTypesRegistry, getExportTypesRegistry } from './export_types_registry'; export { LevelLogger } from './level_logger'; diff --git a/x-pack/plugins/reporting/server/lib/store/report.test.ts b/x-pack/plugins/reporting/server/lib/store/report.test.ts index 4bc45fd745a56..f9cd413b3e5a7 100644 --- a/x-pack/plugins/reporting/server/lib/store/report.test.ts +++ b/x-pack/plugins/reporting/server/lib/store/report.test.ts @@ -15,7 +15,13 @@ describe('Class Report', () => { created_by: 'created_by_test_string', browser_type: 'browser_type_test_string', max_attempts: 50, - payload: { headers: 'payload_test_field', objectType: 'testOt', title: 'cool report' }, + payload: { + headers: 'payload_test_field', + objectType: 'testOt', + title: 'cool report', + version: '7.14.0', + browserTimezone: 'UTC', + }, meta: { objectType: 'test' }, timeout: 30000, }); @@ -64,7 +70,13 @@ describe('Class Report', () => { created_by: 'created_by_test_string', browser_type: 'browser_type_test_string', max_attempts: 50, - payload: { headers: 'payload_test_field', objectType: 'testOt', title: 'hot report' }, + payload: { + headers: 'payload_test_field', + objectType: 'testOt', + title: 'hot report', + version: '7.14.0', + browserTimezone: 'UTC', + }, meta: { objectType: 'stange' }, timeout: 30000, }); diff --git a/x-pack/plugins/reporting/server/lib/store/store.test.ts b/x-pack/plugins/reporting/server/lib/store/store.test.ts index f46e55c9cc41b..9bb9c8a113d3e 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.test.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.test.ts @@ -255,6 +255,7 @@ describe('ReportingStore', () => { headers: 'rp_test_headers', objectType: 'testOt', browserTimezone: 'ABC', + version: '7.14.0', }, timeout: 30000, }); @@ -285,6 +286,7 @@ describe('ReportingStore', () => { headers: 'rp_test_headers', objectType: 'testOt', browserTimezone: 'BCD', + version: '7.14.0', }, timeout: 30000, }); @@ -315,6 +317,7 @@ describe('ReportingStore', () => { headers: 'rp_test_headers', objectType: 'testOt', browserTimezone: 'CDE', + version: '7.14.0', }, timeout: 30000, }); @@ -345,6 +348,7 @@ describe('ReportingStore', () => { headers: 'rp_test_headers', objectType: 'testOt', browserTimezone: 'utc', + version: '7.14.0', }, timeout: 30000, }); @@ -390,6 +394,7 @@ describe('ReportingStore', () => { headers: 'rp_test_headers', objectType: 'testOt', browserTimezone: 'utc', + version: '7.14.0', }, timeout: 30000, }); diff --git a/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts b/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts index 2da509f024c25..8d31c03c618c9 100644 --- a/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts @@ -55,13 +55,14 @@ export function registerGenerateCsvFromSavedObjectImmediate( searchSource: schema.object({}, { unknowns: 'allow' }), browserTimezone: schema.string({ defaultValue: 'UTC' }), title: schema.string(), + version: schema.maybe(schema.string()), }), }, options: { tags: kibanaAccessControlTags, }, }, - userHandler(async (user, context, req: CsvFromSavedObjectRequest, res) => { + userHandler(async (_user, context, req: CsvFromSavedObjectRequest, res) => { const logger = parentLogger.clone(['csv_searchsource_immediate']); const runTaskFn = runTaskFnFactory(reporting, logger); diff --git a/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts b/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts index 55d12e5c6d442..69b3f216886e6 100644 --- a/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts +++ b/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts @@ -95,7 +95,7 @@ export function registerGenerateFromJobParams( path: `${BASE_GENERATE}/{p*}`, validate: false, }, - (context, req, res) => { + (_context, _req, res) => { return res.customError({ statusCode: 405, body: 'GET is not allowed' }); } ); diff --git a/x-pack/plugins/reporting/server/routes/generation.test.ts b/x-pack/plugins/reporting/server/routes/generation.test.ts index c6889f3612b59..df5a85d71f49f 100644 --- a/x-pack/plugins/reporting/server/routes/generation.test.ts +++ b/x-pack/plugins/reporting/server/routes/generation.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import rison from 'rison-node'; import { UnwrapPromise } from '@kbn/utility-types'; import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import { of } from 'rxjs'; @@ -129,7 +130,7 @@ describe('POST /api/reporting/generate', () => { await supertest(httpSetup.server.listener) .post('/api/reporting/generate/TonyHawksProSkater2') - .send({ jobParams: `abc` }) + .send({ jobParams: rison.encode({ title: `abc` }) }) .expect(400) .then(({ body }) => expect(body.message).toMatchInlineSnapshot('"Invalid export-type of TonyHawksProSkater2"') @@ -145,7 +146,7 @@ describe('POST /api/reporting/generate', () => { await supertest(httpSetup.server.listener) .post('/api/reporting/generate/printablePdf') - .send({ jobParams: `abc` }) + .send({ jobParams: rison.encode({ title: `abc` }) }) .expect(500); }); @@ -157,7 +158,7 @@ describe('POST /api/reporting/generate', () => { await supertest(httpSetup.server.listener) .post('/api/reporting/generate/printablePdf') - .send({ jobParams: `abc` }) + .send({ jobParams: rison.encode({ title: `abc` }) }) .expect(200) .then(({ body }) => { expect(body).toMatchObject({ diff --git a/x-pack/plugins/reporting/server/routes/generation.ts b/x-pack/plugins/reporting/server/routes/generation.ts index 5c9fd25b76c39..ce6d1a2f2641f 100644 --- a/x-pack/plugins/reporting/server/routes/generation.ts +++ b/x-pack/plugins/reporting/server/routes/generation.ts @@ -6,7 +6,6 @@ */ import Boom from '@hapi/boom'; -import { errors as elasticsearchErrors } from 'elasticsearch'; import { kibanaResponseFactory } from 'src/core/server'; import { ReportingCore } from '../'; import { API_BASE_URL } from '../../common/constants'; @@ -16,8 +15,6 @@ import { registerGenerateFromJobParams } from './generate_from_jobparams'; import { registerGenerateCsvFromSavedObjectImmediate } from './csv_searchsource_immediate'; import { HandlerFunction } from './types'; -const esErrors = elasticsearchErrors as Record; - const getDownloadBaseUrl = (reporting: ReportingCore) => { const config = reporting.getConfig(); return config.kbnConfig.get('server', 'basePath') + `${API_BASE_URL}/jobs/download`; @@ -77,24 +74,6 @@ export function registerJobGenerationRoutes(reporting: ReportingCore, logger: Lo }); } - if (err instanceof esErrors['401']) { - return res.unauthorized({ - body: `Sorry, you aren't authenticated`, - }); - } - - if (err instanceof esErrors['403']) { - return res.forbidden({ - body: `Sorry, you are not authorized`, - }); - } - - if (err instanceof esErrors['404']) { - return res.notFound({ - body: err.message, - }); - } - // unknown error, can't convert to 4xx throw err; } diff --git a/x-pack/plugins/reporting/server/routes/jobs.ts b/x-pack/plugins/reporting/server/routes/jobs.ts index 0d0332983d6bc..37557c3afb0c7 100644 --- a/x-pack/plugins/reporting/server/routes/jobs.ts +++ b/x-pack/plugins/reporting/server/routes/jobs.ts @@ -93,49 +93,6 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { }) ); - // return the raw output from a job - router.get( - { - path: `${MAIN_ENTRY}/output/{docId}`, - validate: { - params: schema.object({ - docId: schema.string({ minLength: 2 }), - }), - }, - }, - userHandler(async (user, context, req, res) => { - // ensure the async dependencies are loaded - if (!context.reporting) { - return handleUnavailable(res); - } - - const { docId } = req.params; - const { - management: { jobTypes = [] }, - } = await reporting.getLicenseInfo(); - - const jobsQuery = jobsQueryFactory(reporting); - const result = await jobsQuery.getContent(user, docId); - - if (!result) { - throw Boom.notFound(); - } - - const { jobtype: jobType, output } = result; - - if (!jobTypes.includes(jobType)) { - throw Boom.unauthorized(`Sorry, you are not authorized to download ${jobType} reports`); - } - - return res.ok({ - body: output?.content ?? {}, - headers: { - 'content-type': 'application/json', - }, - }); - }) - ); - // return some info about the job router.get( { diff --git a/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts b/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts index 76896a7472d59..a0d6962074a70 100644 --- a/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts +++ b/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts @@ -12,7 +12,8 @@ import { i18n } from '@kbn/i18n'; import { UnwrapPromise } from '@kbn/utility-types'; import { ElasticsearchClient } from 'src/core/server'; import { ReportingCore } from '../../'; -import { ReportApiJSON, ReportDocument, ReportSource } from '../../../common/types'; +import { JobContent, ReportApiJSON, ReportDocument, ReportSource } from '../../../common/types'; +import { statuses } from '../../lib/statuses'; import { Report } from '../../lib/store'; import { ReportingUser } from '../../types'; @@ -47,6 +48,7 @@ interface JobsQueryFactory { count(jobTypes: string[], user: ReportingUser): Promise; get(user: ReportingUser, id: string): Promise; getContent(user: ReportingUser, id: string): Promise; + getError(user: ReportingUser, id: string): Promise<(ReportContent & JobContent) | void>; delete(deleteIndex: string, id: string): Promise>; } @@ -205,6 +207,20 @@ export function jobsQueryFactory(reportingCore: ReportingCore): JobsQueryFactory }; }, + async getError(user, id) { + const content = await this.getContent(user, id); + if (content && content?.output?.content) { + if (content.status !== statuses.JOB_STATUS_FAILED) { + throw new Error(`Can not get error for ${id}`); + } + return { + ...content, + content: content.output.content, + content_type: false, + }; + } + }, + async delete(deleteIndex, id) { try { const { asInternalUser: elasticsearchClient } = await reportingCore.getEsClient(); diff --git a/x-pack/plugins/rollup/server/rollup_data_enricher.ts b/x-pack/plugins/rollup/server/rollup_data_enricher.ts index 8f115687d5433..2c334780720fa 100644 --- a/x-pack/plugins/rollup/server/rollup_data_enricher.ts +++ b/x-pack/plugins/rollup/server/rollup_data_enricher.ts @@ -5,20 +5,19 @@ * 2.0. */ +import { IScopedClusterClient } from 'kibana/server'; import { Index } from '../../../plugins/index_management/server'; -export const rollupDataEnricher = async (indicesList: Index[], callWithRequest: any) => { +export const rollupDataEnricher = async (indicesList: Index[], client: IScopedClusterClient) => { if (!indicesList || !indicesList.length) { return Promise.resolve(indicesList); } - const params = { - path: '/_all/_rollup/data', - method: 'GET', - }; - try { - const rollupJobData = await callWithRequest('transport.request', params); + const { body: rollupJobData } = await client.asCurrentUser.rollup.getRollupIndexCaps({ + index: '_all', + }); + return indicesList.map((index) => { const isRollupIndex = !!rollupJobData[index.name]; return { diff --git a/x-pack/plugins/canvas/public/services/legacy/stubs/embeddables.ts b/x-pack/plugins/rule_registry/server/mocks.ts similarity index 57% rename from x-pack/plugins/canvas/public/services/legacy/stubs/embeddables.ts rename to x-pack/plugins/rule_registry/server/mocks.ts index 2bcc162cff99c..cc5c3cfd484a7 100644 --- a/x-pack/plugins/canvas/public/services/legacy/stubs/embeddables.ts +++ b/x-pack/plugins/rule_registry/server/mocks.ts @@ -5,10 +5,8 @@ * 2.0. */ -import { EmbeddablesService } from '../embeddables'; +import { createLifecycleAlertServicesMock } from './utils/lifecycle_alert_services_mock'; -const noop = (..._args: any[]): any => {}; - -export const embeddablesService: EmbeddablesService = { - getEmbeddableFactories: noop, +export const ruleRegistryMocks = { + createLifecycleAlertServices: createLifecycleAlertServicesMock, }; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_executor_mock.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_executor_mock.ts new file mode 100644 index 0000000000000..c519674569a51 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_executor_mock.ts @@ -0,0 +1,35 @@ +/* + * 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 { + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, +} from '../../../../plugins/alerting/server'; +import { AlertExecutorOptionsWithExtraServices } from '../types'; + +import { LifecycleAlertServices, LifecycleRuleExecutor } from './create_lifecycle_executor'; + +export const createLifecycleRuleExecutorMock = < + Params extends AlertTypeParams = never, + State extends AlertTypeState = never, + InstanceState extends AlertInstanceState = never, + InstanceContext extends AlertInstanceContext = never, + ActionGroupIds extends string = never +>( + executor: LifecycleRuleExecutor +) => async ( + options: AlertExecutorOptionsWithExtraServices< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + LifecycleAlertServices + > +) => await executor(options); diff --git a/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services_mock.ts b/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services_mock.ts new file mode 100644 index 0000000000000..37b4847bc9c69 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services_mock.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AlertInstanceContext, AlertInstanceState } from '../../../alerting/server'; +import { alertsMock } from '../../../alerting/server/mocks'; +import { LifecycleAlertServices } from './create_lifecycle_executor'; + +/** + * This wraps the alerts to enable the preservation of the generic type + * arguments of the factory function. + **/ +class AlertsMockWrapper< + InstanceState extends AlertInstanceState = AlertInstanceState, + InstanceContext extends AlertInstanceContext = AlertInstanceContext +> { + createAlertServices() { + return alertsMock.createAlertServices(); + } +} + +type AlertServices< + InstanceState extends AlertInstanceState = AlertInstanceState, + InstanceContext extends AlertInstanceContext = AlertInstanceContext +> = ReturnType['createAlertServices']>; + +export const createLifecycleAlertServicesMock = < + InstanceState extends AlertInstanceState = never, + InstanceContext extends AlertInstanceContext = never, + ActionGroupIds extends string = never +>( + alertServices: AlertServices +): LifecycleAlertServices => ({ + alertWithLifecycle: ({ id }) => alertServices.alertInstanceFactory(id), +}); diff --git a/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx b/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx index f2d3fcd6ab3ca..adaca23be5dae 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx @@ -64,6 +64,7 @@ describe('SecurityNavControl', () => { onClick={[Function]} > diff --git a/x-pack/plugins/security/public/nav_control/nav_control_component.tsx b/x-pack/plugins/security/public/nav_control/nav_control_component.tsx index 56eb784467c05..3b45b5164c6cf 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_component.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_component.tsx @@ -100,7 +100,7 @@ export class SecurityNavControl extends Component { (authenticatedUser && (authenticatedUser.full_name || authenticatedUser.username)) || ''; const buttonContents = authenticatedUser ? ( - + ) : ( ); diff --git a/x-pack/plugins/security/public/nav_control/nav_control_service.test.ts b/x-pack/plugins/security/public/nav_control/nav_control_service.test.ts index ece00bcd43578..9839d29291629 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_service.test.ts +++ b/x-pack/plugins/security/public/nav_control/nav_control_service.test.ts @@ -86,6 +86,7 @@ describe('SecurityNavControlService', () => {
diff --git a/x-pack/plugins/security/server/errors.test.ts b/x-pack/plugins/security/server/errors.test.ts index 9aa8635793281..90860689b2852 100644 --- a/x-pack/plugins/security/server/errors.test.ts +++ b/x-pack/plugins/security/server/errors.test.ts @@ -7,7 +7,6 @@ import { errors as esErrors } from '@elastic/elasticsearch'; import Boom from '@hapi/boom'; -import { errors as legacyESErrors } from 'elasticsearch'; import * as errors from './errors'; import { securityMock } from './mocks'; @@ -72,11 +71,6 @@ describe('lib/errors', () => { ).toBe(401); }); - it('extracts status code from legacy Elasticsearch client error', () => { - expect(errors.getErrorStatusCode(new legacyESErrors.BadRequest())).toBe(400); - expect(errors.getErrorStatusCode(new legacyESErrors.AuthenticationException())).toBe(401); - }); - it('extracts status code from `status` property', () => { expect(errors.getErrorStatusCode({ statusText: 'Bad Request', status: 400 })).toBe(400); expect(errors.getErrorStatusCode({ statusText: 'Unauthorized', status: 401 })).toBe(401); @@ -120,13 +114,6 @@ describe('lib/errors', () => { ).toBe(JSON.stringify({ field1: 'value-1', field2: 'value-2' })); }); - it('extracts status code from legacy Elasticsearch client error', () => { - expect(errors.getDetailedErrorMessage(new legacyESErrors.BadRequest())).toBe('Bad Request'); - expect(errors.getDetailedErrorMessage(new legacyESErrors.AuthenticationException())).toBe( - 'Authentication Exception' - ); - }); - it('extracts `message` property', () => { expect(errors.getDetailedErrorMessage(new Error('some-message'))).toBe('some-message'); }); diff --git a/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.test.ts b/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.test.ts index 03736c3bfb4bc..11849a1f50bce 100644 --- a/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { DeeplyMockedKeys } from '@kbn/utility-types/target/jest'; +import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import type { RequestHandler, RouteConfig } from 'src/core/server'; import { kibanaResponseFactory } from 'src/core/server'; import { httpServerMock } from 'src/core/server/mocks'; diff --git a/x-pack/plugins/security/server/routes/users/change_password.test.ts b/x-pack/plugins/security/server/routes/users/change_password.test.ts index fba7bf4f872e7..35972e103855e 100644 --- a/x-pack/plugins/security/server/routes/users/change_password.test.ts +++ b/x-pack/plugins/security/server/routes/users/change_password.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { errors } from 'elasticsearch'; +import { errors } from '@elastic/elasticsearch'; import type { ObjectType } from '@kbn/config-schema'; import type { PublicMethodsOf } from '@kbn/utility-types'; @@ -18,6 +18,7 @@ import { mockAuthenticatedUser } from '../../../common/model/authenticated_user. import { AuthenticationResult } from '../../authentication'; import type { InternalAuthenticationServiceStart } from '../../authentication'; import { authenticationServiceMock } from '../../authentication/authentication_service.mock'; +import { securityMock } from '../../mocks'; import type { Session } from '../../session_management'; import { sessionMock } from '../../session_management/session.mock'; import type { SecurityRequestHandlerContext, SecurityRouter } from '../../types'; @@ -109,9 +110,9 @@ describe('Change password', () => { }); it('returns 403 if old password is wrong.', async () => { - const changePasswordFailure = new (errors.AuthenticationException as any)('Unauthorized', { - body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, - }); + const changePasswordFailure = new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 401, body: {} }) + ); mockContext.core.elasticsearch.client.asCurrentUser.security.changePassword.mockRejectedValue( changePasswordFailure ); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_signals_index_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_signals_index_schema.ts index 81761dd085df6..ed3664b6792b3 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_signals_index_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_signals_index_schema.ts @@ -6,13 +6,13 @@ */ import * as t from 'io-ts'; -import { PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-types'; +import { PositiveInteger } from '@kbn/securitysolution-io-ts-types'; export const querySignalsSchema = t.exact( t.partial({ query: t.object, aggs: t.object, - size: PositiveIntegerGreaterThanZero, + size: PositiveInteger, track_total_hits: t.boolean, _source: t.array(t.string), }) diff --git a/x-pack/plugins/security_solution/cypress/README.md b/x-pack/plugins/security_solution/cypress/README.md index 1bc4a47d6d9c6..d70011f864860 100644 --- a/x-pack/plugins/security_solution/cypress/README.md +++ b/x-pack/plugins/security_solution/cypress/README.md @@ -2,6 +2,48 @@ The `security_solution/cypress` directory contains functional UI tests that execute using [Cypress](https://www.cypress.io/). +Currently with Cypress you can develop `functional` tests and coming soon `CCS` and `Upgrade` functional tests. + +If you are still having doubts, questions or queries, please feel free to ping our Cypress champions: + +- Functional Tests: + - Gloria Hornero, Frank Hassanabad and Patryk Kopycinsky + +- CCS Tests: + - Technical questions around the https://github.com/elastic/integration-test repo: + - Domenico Andreoli + - Doubts regarding testing CCS and Cypress best practices: + - Gloria Hornero + +## Table of Contents + +[**How to add a new Cypress test**](#how-to-add-a-new-cypress-test) + +[**Running the tests**](#running-the-tests) + +[**Debugging your test**](#debugging-your-test) + +[**Folder structure**](#folder-structure) + +[**Test data**](#test-data) + +[**Development Best Practices**](#development-best-practices) + +[**Test Artifacts**](#test-artifacts) + +[**Linting**](#linting) + +## How to add a new Cypress test + +Before considering adding a new Cypress tests, please make sure you have added unit and API tests first. Note that, the aim of Cypress + is to test that the user interface operates as expected, hence, you should not be using this tool to test REST API or data contracts. + +First take a look to the [**Development Best Practices**](#development-best-practices) section. +Then check check [**Folder structure**](#folder-structure) section to know where is the best place to put your test, [**Test data**](#test-data) section if you need to create any type +of data for your test, [**Running the tests**](#running-the-tests) to know how to execute the tests and [**Debugging your test**](#debugging-your-test) to debug your test if needed. + +Please, before opening a PR with the new test, please make sure that the test fails. If you never see your test fail you don’t know if your test is actually testing the right thing, or testing anything at all. + ## Running the tests There are currently four ways to run the tests, comprised of two execution modes and two target environments, which will be detailed below. @@ -165,7 +207,7 @@ node ../../../scripts/es_archiver load auditbeat --dir ../../test/security_solut # launch the cypress test runner with overridden environment variables cd x-pack/plugins/security_solution -CYPRESS_BASE_URL=http(s)://:@ CYPRESS_ELASTICSEARCH_URL=http(s)://:@ CYPRESS_ELASTICSEARCH_USERNAME= CYPRESS_ELASTICSEARCH_PASSWORD=password yarn cypress:run +CYPRESS_base_url=http(s)://:@ CYPRESS_ELASTICSEARCH_URL=http(s)://:@ CYPRESS_ELASTICSEARCH_USERNAME= CYPRESS_ELASTICSEARCH_PASSWORD= CYPRESS_protocol= CYPRESS_hostname= CYPRESS_configport= CYPRESS_KIBANA_URL= yarn cypress:run ``` #### Custom Target + Headless (Firefox) @@ -183,7 +225,7 @@ node ../../../scripts/es_archiver load auditbeat --dir ../../test/security_solut # launch the cypress test runner with overridden environment variables cd x-pack/plugins/security_solution -CYPRESS_BASE_URL=http(s)://:@ CYPRESS_ELASTICSEARCH_URL=http(s)://:@ CYPRESS_ELASTICSEARCH_USERNAME= CYPRESS_ELASTICSEARCH_PASSWORD=password yarn cypress:run:firefox +CYPRESS_base_url=http(s)://:@ CYPRESS_ELASTICSEARCH_URL=http(s)://:@ CYPRESS_ELASTICSEARCH_USERNAME= CYPRESS_ELASTICSEARCH_PASSWORD= CYPRESS_protocol= CYPRESS_hostname= CYPRESS_configport= CYPRESS_KIBANA_URL= yarn cypress:run:firefox ``` #### CCS Custom Target + Headless @@ -216,8 +258,16 @@ Similar sequence, just ending with `yarn cypress:open:ccs`, can be used for inte Appending `--browser firefox` to the `yarn cypress:run:ccs` command above will run the tests on Firefox instead of Chrome. +## Debugging your test +In order to be able to debug any Cypress test you need to open Cypress on visual mode. [Here](https://docs.cypress.io/guides/guides/debugging) +you can find an extended guide about how to proceed. + +If you are debugging a flaky test, a good tip is to insert a `cy.wait()` around async parts of the tes code base, such as network calls which can make an indeterministic test, deterministically fail locally. + ## Folder Structure +Below you can find the folder structure used on our Cypress tests. + ### ccs_integration/ Contains the specs that are executed in a Cross Cluster Search configuration. @@ -352,6 +402,11 @@ export const unmappedCCSRule: CustomRule = { Similar approach should be used in defining all index patterns, rules, and queries to be applied on remote data. ## Development Best Practices +Below you will a set of best practices that should be followed when writing Cypress tests. + +### Make sure your test fail + +Before open a PR with the new test, please make sure that the test fail. If you never see your test fail you don’t know if your test is actually testing the right thing, or testing anything at all. ### Clean up the state @@ -372,6 +427,14 @@ taken into consideration until another solution is implemented: Remember that minimizing the number of times the web page is loaded, we minimize as well the execution time. +### Cypress-pipe +It is very common in the code to don't have click handlers regitered. In this specific case, please use [Cypress pipe](https://www.cypress.io/blog/2019/01/22/when-can-the-test-click/). + +### CCS test specific +When testing CCS we want to put our focus in making sure that our `Source` instance is receiving properly the data that comes from the `Remote` instances, as well as the data is displayed as we expect on the `Source`. + +For that reason and in order to make our test more stable, use the API to execute all the actions needed before the assertions, and use Cypress to assert that the UI is displaying all the expected things. + ## Test Artifacts When Cypress tests are run headless on the command line, artifacts diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/connector_options.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/connector_options.spec.ts index 0959f999a4b53..ced815c4b58c1 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/connector_options.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/connector_options.spec.ts @@ -31,27 +31,31 @@ describe('Cases connector incident fields', () => { beforeEach(() => { cleanKibana(); cy.intercept('GET', '/api/cases/configure/connectors/_find', getMockConnectorsResponse()); - cy.intercept('POST', `/api/actions/action/${getConnectorIds().sn}/_execute`, (req) => { + cy.intercept('POST', `/api/actions/connector/${getConnectorIds().sn}/_execute`, (req) => { const response = req.body.params.subAction === 'getChoices' ? getExecuteResponses().servicenow.choices : { status: 'ok', data: [] }; req.reply(response); }); - cy.intercept('POST', `/api/actions/action/${getConnectorIds().jira}/_execute`, (req) => { + cy.intercept('POST', `/api/actions/connector/${getConnectorIds().jira}/_execute`, (req) => { const response = req.body.params.subAction === 'issueTypes' ? getExecuteResponses().jira.issueTypes : getExecuteResponses().jira.fieldsByIssueType; req.reply(response); }); - cy.intercept('POST', `/api/actions/action/${getConnectorIds().resilient}/_execute`, (req) => { - const response = - req.body.params.subAction === 'incidentTypes' - ? getExecuteResponses().resilient.incidentTypes - : getExecuteResponses().resilient.severity; - req.reply(response); - }); + cy.intercept( + 'POST', + `/api/actions/connector/${getConnectorIds().resilient}/_execute`, + (req) => { + const response = + req.body.params.subAction === 'incidentTypes' + ? getExecuteResponses().resilient.incidentTypes + : getExecuteResponses().resilient.severity; + req.reply(response); + } + ); }); it('Correct incident fields show when connector is changed', () => { diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts index 3b524bd252cdd..dda86d2717386 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CELL_TEXT, JSON_LINES, TABLE_ROWS } from '../../screens/alerts_details'; +import { ALERT_FLYOUT, CELL_TEXT, JSON_LINES, TABLE_ROWS } from '../../screens/alerts_details'; import { expandFirstAlert, @@ -41,12 +41,14 @@ describe('Alert details with unmapped fields', () => { openJsonView(); scrollJsonViewToBottom(); - cy.get(JSON_LINES).then((elements) => { - const length = elements.length; - cy.wrap(elements) - .eq(length - expectedUnmappedField.line) - .should('have.text', expectedUnmappedField.text); - }); + cy.get(ALERT_FLYOUT) + .find(JSON_LINES) + .then((elements) => { + const length = elements.length; + cy.wrap(elements) + .eq(length - expectedUnmappedField.line) + .should('have.text', expectedUnmappedField.text); + }); }); it('Displays the unmapped field on the table', () => { @@ -57,8 +59,8 @@ describe('Alert details with unmapped fields', () => { }; openTable(); - - cy.get(TABLE_ROWS) + cy.get(ALERT_FLYOUT) + .find(TABLE_ROWS) .eq(expectedUnmmappedField.row) .within(() => { cy.get(CELL_TEXT).eq(2).should('have.text', expectedUnmmappedField.field); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts index a89ddf3e0b250..5e851cecbd86b 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts @@ -44,7 +44,7 @@ describe('timeline data providers', () => { closeTimeline(); }); - it('renders the data provider of a host dragged from the All Hosts widget on the hosts page', () => { + it.skip('renders the data provider of a host dragged from the All Hosts widget on the hosts page', () => { dragAndDropFirstHostToTimeline(); openTimelineUsingToggle(); cy.get(`${TIMELINE_FLYOUT} ${TIMELINE_DROPPED_DATA_PROVIDERS}`) @@ -78,7 +78,7 @@ describe('timeline data providers', () => { }); }); - it('sets correct classes when the user starts dragging a host, but is not hovering over the data providers', () => { + it.skip('sets correct classes when the user starts dragging a host, but is not hovering over the data providers', () => { dragFirstHostToTimeline(); cy.get(IS_DRAGGING_DATA_PROVIDERS) @@ -87,7 +87,7 @@ describe('timeline data providers', () => { .should('have.class', 'drop-target-data-providers'); }); - it('render an extra highlighted area in dataProvider when the user starts dragging a host AND is hovering over the data providers', () => { + it.skip('render an extra highlighted area in dataProvider when the user starts dragging a host AND is hovering over the data providers', () => { dragFirstHostToEmptyTimelineDataProviders(); cy.get(IS_DRAGGING_DATA_PROVIDERS) diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts index 38c6f41f1049c..ac34d65f0fd0a 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts @@ -79,7 +79,7 @@ describe('timeline flyout button', () => { closeTimelineUsingCloseButton(); }); - it('sets correct classes when the user starts dragging a host, but is not hovering over the data providers', () => { + it.skip('sets correct classes when the user starts dragging a host, but is not hovering over the data providers', () => { dragFirstHostToTimeline(); cy.get(IS_DRAGGING_DATA_PROVIDERS) diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts index 6da09bc0da8cd..2449a90f5328c 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts @@ -5,6 +5,8 @@ * 2.0. */ +export const ALERT_FLYOUT = '[data-test-subj="timeline:details-panel:flyout"]'; + export const CELL_TEXT = '.euiText'; export const JSON_CONTENT = '[data-test-subj="jsonView"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/hosts/all_hosts.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/all_hosts.ts index 615421b1ef323..cf1bac421b447 100644 --- a/x-pack/plugins/security_solution/cypress/screens/hosts/all_hosts.ts +++ b/x-pack/plugins/security_solution/cypress/screens/hosts/all_hosts.ts @@ -7,6 +7,6 @@ export const ALL_HOSTS_TABLE = '[data-test-subj="table-allHosts-loading-false"]'; -export const HOSTS_NAMES = '[data-test-subj="draggable-content-host.name"] a.euiLink'; +export const HOSTS_NAMES = '[data-test-subj="render-content-host.name"] a.euiLink'; -export const HOSTS_NAMES_DRAGGABLE = '[data-test-subj="draggable-content-host.name"]'; +export const HOSTS_NAMES_DRAGGABLE = '[data-test-subj="render-content-host.name"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/hosts/uncommon_processes.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/uncommon_processes.ts index 536379654862c..f2a712f868850 100644 --- a/x-pack/plugins/security_solution/cypress/screens/hosts/uncommon_processes.ts +++ b/x-pack/plugins/security_solution/cypress/screens/hosts/uncommon_processes.ts @@ -5,6 +5,6 @@ * 2.0. */ -export const PROCESS_NAME_FIELD = '[data-test-subj="draggable-content-process.name"]'; +export const PROCESS_NAME_FIELD = '[data-test-subj="render-content-process.name"]'; export const UNCOMMON_PROCESSES_TABLE = '[data-test-subj="table-uncommonProcesses-loading-false"]'; diff --git a/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx index 6d87b5d3a68b9..ad15f0a5fa9fb 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx @@ -340,13 +340,9 @@ describe.each(chartDataSets)('BarChart with stackByField', () => { const dataProviderId = `draggableId.content.draggable-legend-item-uuid_v4()-${escapeDataProviderId( stackByField )}-${escapeDataProviderId(datum.key)}`; - - expect( - wrapper - .find(`[draggableId="${dataProviderId}"] [data-test-subj="providerContainer"]`) - .first() - .text() - ).toEqual(datum.key); + expect(wrapper.find(`div[data-provider-id="${dataProviderId}"]`).first().text()).toEqual( + datum.key + ); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.tsx index 6017501f87dcc..493ce4da78eba 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.tsx @@ -46,6 +46,7 @@ const DraggableLegendItemComponent: React.FC<{ data-test-subj={`legend-item-${dataProviderId}`} field={field} id={dataProviderId} + isDraggable={false} timelineId={timelineId} value={value} /> diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap index aa8214938c2b0..0b25ff2c8c5ee 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap @@ -17,6 +17,7 @@ exports[`DraggableWrapper rendering it renders against the snapshot 1`] = ` }, } } + isDraggable={true} render={[Function]} /> `; diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx index bdc5545880e1c..d27ad96ff3c4f 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx @@ -42,7 +42,11 @@ describe('DraggableWrapper', () => { const wrapper = shallow( - message} /> + message} + /> ); @@ -54,7 +58,11 @@ describe('DraggableWrapper', () => { const wrapper = mount( - message} /> + message} + /> ); @@ -66,19 +74,27 @@ describe('DraggableWrapper', () => { const wrapper = mount( - message} /> + message} + /> ); - expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(false); + expect(wrapper.find('[data-test-subj="hover-actions-copy-button"]').exists()).toBe(false); }); test('it renders hover actions when the mouse is over the text of draggable wrapper', async () => { const wrapper = mount( - message} /> + message} + /> ); @@ -88,7 +104,7 @@ describe('DraggableWrapper', () => { wrapper.update(); jest.runAllTimers(); wrapper.update(); - expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(true); + expect(wrapper.find('[data-test-subj="hover-actions-copy-button"]').exists()).toBe(true); }); }); }); @@ -98,7 +114,12 @@ describe('DraggableWrapper', () => { const wrapper = mount( - message} truncate /> + message} + truncate + /> ); @@ -112,7 +133,11 @@ describe('DraggableWrapper', () => { const wrapper = mount( - message} /> + message} + /> ); diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx index 9db5b3899d8bc..d008aad213392 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx @@ -7,7 +7,7 @@ import { EuiScreenReaderOnly } from '@elastic/eui'; import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid'; -import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Draggable, DraggableProvided, @@ -25,12 +25,13 @@ import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../../../timelines/com import { TruncatableText } from '../truncatable_text'; import { WithHoverActions } from '../with_hover_actions'; -import { DraggableWrapperHoverContent, useGetTimelineId } from './draggable_wrapper_hover_content'; + import { getDraggableId, getDroppableId } from './helpers'; import { ProviderContainer } from './provider_container'; import * as i18n from './translations'; import { useKibana } from '../../lib/kibana'; +import { useHoverActions } from '../hover_actions/use_hover_actions'; // As right now, we do not know what we want there, we will keep it as a placeholder export const DragEffects = styled.div``; @@ -80,7 +81,7 @@ const Wrapper = styled.div` Wrapper.displayName = 'Wrapper'; -const ProviderContentWrapper = styled.span` +export const ProviderContentWrapper = styled.span` > span.euiToolTipAnchor { display: block; /* allow EuiTooltip content to be truncatable */ } @@ -95,6 +96,7 @@ type RenderFunctionProp = ( interface Props { dataProvider: DataProvider; disabled?: boolean; + isDraggable?: boolean; inline?: boolean; render: RenderFunctionProp; timelineId?: string; @@ -121,55 +123,35 @@ export const getStyle = ( }; }; -const draggableContainsLinks = (draggableElement: HTMLDivElement | null) => { - const links = draggableElement?.querySelectorAll('.euiLink') ?? []; - return links.length > 0; -}; - -const DraggableWrapperComponent: React.FC = ({ +const DraggableOnWrapperComponent: React.FC = ({ dataProvider, onFilterAdded, render, timelineId, truncate, }) => { - const keyboardHandlerRef = useRef(null); - const draggableRef = useRef(null); - const [closePopOverTrigger, setClosePopOverTrigger] = useState(false); - const [showTopN, setShowTopN] = useState(false); - const [goGetTimelineId, setGoGetTimelineId] = useState(false); - const timelineIdFind = useGetTimelineId(draggableRef, goGetTimelineId); const [providerRegistered, setProviderRegistered] = useState(false); const isDisabled = dataProvider.id.includes(`-${ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID}-`); - const [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState(false); const dispatch = useDispatch(); const { timelines } = useKibana().services; - - const handleClosePopOverTrigger = useCallback(() => { - setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger); - setHoverActionsOwnFocus((prevHoverActionsOwnFocus) => { - if (prevHoverActionsOwnFocus) { - setTimeout(() => { - keyboardHandlerRef.current?.focus(); - }, 0); - } - return false; // always give up ownership - }); - - setTimeout(() => { - setHoverActionsOwnFocus(false); - }, 0); // invoked on the next tick, because we want to restore focus first - }, [keyboardHandlerRef]); - - const toggleTopN = useCallback(() => { - setShowTopN((prevShowTopN) => { - const newShowTopN = !prevShowTopN; - if (newShowTopN === false) { - handleClosePopOverTrigger(); - } - return newShowTopN; - }); - }, [handleClosePopOverTrigger]); + const { + closePopOverTrigger, + handleClosePopOverTrigger, + hoverActionsOwnFocus, + hoverContent, + keyboardHandlerRef, + onCloseRequested, + openPopover, + onFocus, + setContainerRef, + showTopN, + } = useHoverActions({ + dataProvider, + onFilterAdded, + render, + timelineId, + truncate, + }); const registerProvider = useCallback(() => { if (!isDisabled) { @@ -192,49 +174,6 @@ const DraggableWrapperComponent: React.FC = ({ [unRegisterProvider] ); - const hoverContent = useMemo(() => { - // display links as additional content in the hover menu to enable keyboard - // navigation of links (when the draggable contains them): - const additionalContent = - hoverActionsOwnFocus && !showTopN && draggableContainsLinks(draggableRef.current) ? ( - - {render(dataProvider, null, { isDragging: false, isDropAnimating: false })} - - ) : null; - - return ( - - ); - }, [ - dataProvider, - handleClosePopOverTrigger, - hoverActionsOwnFocus, - onFilterAdded, - render, - showTopN, - timelineId, - timelineIdFind, - toggleTopN, - ]); - const RenderClone = useCallback( (provided, snapshot) => ( @@ -264,7 +203,7 @@ const DraggableWrapperComponent: React.FC = ({ {...provided.dragHandleProps} ref={(e: HTMLDivElement) => { provided.innerRef(e); - draggableRef.current = e; + setContainerRef(e); }} data-test-subj="providerContainer" isDragging={snapshot.isDragging} @@ -292,13 +231,9 @@ const DraggableWrapperComponent: React.FC = ({ )} ), - [dataProvider, registerProvider, render, truncate] + [dataProvider, registerProvider, render, setContainerRef, truncate] ); - const openPopover = useCallback(() => { - setHoverActionsOwnFocus(true); - }, []); - const { onBlur, onKeyDown } = timelines.getUseDraggableKeyboardWrapper()({ closePopover: handleClosePopOverTrigger, draggableId: getDraggableId(dataProvider.id), @@ -307,24 +242,6 @@ const DraggableWrapperComponent: React.FC = ({ openPopover, }); - const onFocus = useCallback(() => { - if (!hoverActionsOwnFocus) { - keyboardHandlerRef.current?.focus(); - } - }, [hoverActionsOwnFocus, keyboardHandlerRef]); - - const onCloseRequested = useCallback(() => { - setShowTopN(false); - - if (hoverActionsOwnFocus) { - setHoverActionsOwnFocus(false); - - setTimeout(() => { - onFocus(); // return focus to this draggable on the next tick, because we owned focus - }, 0); - } - }, [onFocus, hoverActionsOwnFocus]); - const DroppableContent = useCallback( (droppableProvided) => (
@@ -350,7 +267,7 @@ const DraggableWrapperComponent: React.FC = ({ {droppableProvided.placeholder}
), - [DraggableContent, dataProvider.id, isDisabled, onBlur, onFocus, onKeyDown] + [DraggableContent, dataProvider.id, isDisabled, keyboardHandlerRef, onBlur, onFocus, onKeyDown] ); const content = useMemo( @@ -385,6 +302,75 @@ const DraggableWrapperComponent: React.FC = ({ ); }; +const DraggableWrapperComponent: React.FC = ({ + dataProvider, + isDraggable = false, + onFilterAdded, + render, + timelineId, + truncate, +}) => { + const { + closePopOverTrigger, + hoverActionsOwnFocus, + hoverContent, + onCloseRequested, + setContainerRef, + showTopN, + } = useHoverActions({ + dataProvider, + isDraggable, + onFilterAdded, + render, + timelineId, + truncate, + }); + const renderContent = useCallback( + () => ( +
{ + setContainerRef(e); + }} + tabIndex={-1} + data-provider-id={getDraggableId(dataProvider.id)} + > + {truncate ? ( + + {render(dataProvider, null, { isDragging: false, isDropAnimating: false })} + + ) : ( + + {render(dataProvider, null, { isDragging: false, isDropAnimating: false })} + + )} +
+ ), + [dataProvider, render, setContainerRef, truncate] + ); + if (!isDraggable) { + return ( + + ); + } + return ( + + ); +}; + export const DraggableWrapper = React.memo(DraggableWrapperComponent); DraggableWrapper.displayName = 'DraggableWrapper'; diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx deleted file mode 100644 index 2531780ec4bd5..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx +++ /dev/null @@ -1,564 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { waitFor } from '@testing-library/react'; -import { mount, ReactWrapper } from 'enzyme'; - -import { coreMock } from '../../../../../../../src/core/public/mocks'; -import { mockBrowserFields } from '../../containers/source/mock'; -import '../../mock/match_media'; -import { useKibana } from '../../lib/kibana'; -import { TestProviders } from '../../mock'; -import { FilterManager } from '../../../../../../../src/plugins/data/public'; -import { useSourcererScope } from '../../containers/sourcerer'; -import { DraggableWrapperHoverContent } from './draggable_wrapper_hover_content'; -import { TimelineId } from '../../../../common/types/timeline'; -import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; - -jest.mock('../link_to'); -jest.mock('../../lib/kibana'); -jest.mock('../../containers/sourcerer', () => { - const original = jest.requireActual('../../containers/sourcerer'); - - return { - ...original, - useSourcererScope: jest.fn(), - }; -}); - -jest.mock('uuid', () => { - return { - v1: jest.fn(() => 'uuid.v1()'), - v4: jest.fn(() => 'uuid.v4()'), - }; -}); -const mockStartDragToTimeline = jest.fn(); -jest.mock('../../../../../timelines/public/hooks/use_add_to_timeline', () => { - const original = jest.requireActual('../../../../../timelines/public/hooks/use_add_to_timeline'); - return { - ...original, - useAddToTimeline: () => ({ startDragToTimeline: mockStartDragToTimeline }), - }; -}); -const mockAddFilters = jest.fn(); -jest.mock('../../../common/hooks/use_selector', () => ({ - useShallowEqualSelector: jest.fn(), - useDeepEqualSelector: jest.fn(), -})); -jest.mock('../../../common/hooks/use_invalid_filter_query.tsx'); - -const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; -const timelineId = TimelineId.active; -const field = 'process.name'; -const value = 'nice'; -const toggleTopN = jest.fn(); -const goGetTimelineId = jest.fn(); -const defaultProps = { - field, - goGetTimelineId, - ownFocus: false, - showTopN: false, - timelineId, - toggleTopN, - value, -}; - -describe('DraggableWrapperHoverContent', () => { - beforeAll(() => { - mockStartDragToTimeline.mockReset(); - (useDeepEqualSelector as jest.Mock).mockReturnValue({ - filterManager: { addFilters: mockAddFilters }, - }); - (useSourcererScope as jest.Mock).mockReturnValue({ - browserFields: mockBrowserFields, - selectedPatterns: [], - indexPattern: {}, - }); - }); - - /** - * The tests for "Filter for value" and "Filter out value" are similar enough - * to combine them into "table tests" using this array - */ - const forOrOut = ['for', 'out']; - - forOrOut.forEach((hoverAction) => { - describe(`Filter ${hoverAction} value`, () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - test(`it renders the 'Filter ${hoverAction} value' button when showTopN is false`, () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().exists() - ).toBe(true); - }); - - test(`it does NOT render the 'Filter ${hoverAction} value' button when showTopN is true`, () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().exists() - ).toBe(false); - }); - - test(`it should call goGetTimelineId when user is over the 'Filter ${hoverAction} value' button`, () => { - const wrapper = mount( - - - - ); - const button = wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first(); - button.simulate('mouseenter'); - expect(goGetTimelineId).toHaveBeenCalledWith(true); - }); - - describe('when run in the context of a timeline', () => { - let wrapper: ReactWrapper; - let onFilterAdded: () => void; - - beforeEach(() => { - onFilterAdded = jest.fn(); - - wrapper = mount( - - - - ); - }); - - test('when clicked, it adds a filter to the timeline when running in the context of a timeline', () => { - wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click'); - wrapper.update(); - - expect(mockAddFilters).toBeCalledWith({ - meta: { - alias: null, - disabled: false, - key: 'process.name', - negate: hoverAction === 'out' ? true : false, - params: { query: 'nice' }, - type: 'phrase', - value: 'nice', - }, - query: { match: { 'process.name': { query: 'nice', type: 'phrase' } } }, - }); - }); - - test('when clicked, invokes onFilterAdded when running in the context of a timeline', () => { - wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click'); - wrapper.update(); - - expect(onFilterAdded).toBeCalled(); - }); - }); - - describe('when NOT run in the context of a timeline', () => { - let wrapper: ReactWrapper; - let onFilterAdded: () => void; - const kibana = useKibana(); - - beforeEach(() => { - kibana.services.data.query.filterManager.addFilters = jest.fn(); - onFilterAdded = jest.fn(); - - wrapper = mount( - - - - ); - }); - - test('when clicked, it adds a filter to the global filters when NOT running in the context of a timeline', () => { - wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click'); - wrapper.update(); - - expect(kibana.services.data.query.filterManager.addFilters).toBeCalledWith({ - meta: { - alias: null, - disabled: false, - key: 'process.name', - negate: hoverAction === 'out' ? true : false, - params: { query: 'nice' }, - type: 'phrase', - value: 'nice', - }, - query: { match: { 'process.name': { query: 'nice', type: 'phrase' } } }, - }); - }); - - test('when clicked, invokes onFilterAdded when NOT running in the context of a timeline', () => { - wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click'); - wrapper.update(); - - expect(onFilterAdded).toBeCalled(); - }); - }); - - describe('an empty string value when run in the context of a timeline', () => { - let filterManager: FilterManager; - let wrapper: ReactWrapper; - let onFilterAdded: () => void; - - beforeEach(() => { - filterManager = new FilterManager(mockUiSettingsForFilterManager); - filterManager.addFilters = jest.fn(); - onFilterAdded = jest.fn(); - - wrapper = mount( - - - - ); - }); - - const expectedFilterTypeDescription = - hoverAction === 'for' ? 'a "NOT exists"' : 'an "exists"'; - test(`when clicked, it adds ${expectedFilterTypeDescription} filter to the timeline when run in the context of a timeline`, () => { - const expected = - hoverAction === 'for' - ? { - exists: { field: 'process.name' }, - meta: { - alias: null, - disabled: false, - key: 'process.name', - negate: true, - type: 'exists', - value: 'exists', - }, - } - : { - exists: { field: 'process.name' }, - meta: { - alias: null, - disabled: false, - key: 'process.name', - negate: false, - type: 'exists', - value: 'exists', - }, - }; - - wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click'); - wrapper.update(); - - expect(mockAddFilters).toBeCalledWith(expected); - }); - }); - - describe('an empty string value when NOT run in the context of a timeline', () => { - let wrapper: ReactWrapper; - let onFilterAdded: () => void; - const kibana = useKibana(); - - beforeEach(() => { - kibana.services.data.query.filterManager.addFilters = jest.fn(); - onFilterAdded = jest.fn(); - - wrapper = mount( - - - - ); - }); - - const expectedFilterTypeDescription = - hoverAction === 'for' ? 'a "NOT exists"' : 'an "exists"'; - test(`when clicked, it adds ${expectedFilterTypeDescription} filter to the global filters when NOT running in the context of a timeline`, () => { - const expected = - hoverAction === 'for' - ? { - exists: { field: 'process.name' }, - meta: { - alias: null, - disabled: false, - key: 'process.name', - negate: true, - type: 'exists', - value: 'exists', - }, - } - : { - exists: { field: 'process.name' }, - meta: { - alias: null, - disabled: false, - key: 'process.name', - negate: false, - type: 'exists', - value: 'exists', - }, - }; - - wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click'); - wrapper.update(); - - expect(kibana.services.data.query.filterManager.addFilters).toBeCalledWith(expected); - }); - }); - }); - }); - - describe('Add to timeline', () => { - const aggregatableStringField = 'cloud.account.id'; - const draggableId = 'draggable.id'; - - [false, true].forEach((showTopN) => { - [value, null].forEach((maybeValue) => { - [draggableId, undefined].forEach((maybeDraggableId) => { - const shouldRender = !showTopN && maybeValue != null && maybeDraggableId != null; - const assertion = shouldRender ? 'should render' : 'should NOT render'; - - test(`it ${assertion} the 'Add to timeline investigation' button when showTopN is ${showTopN}, value is ${maybeValue}, and a draggableId is ${maybeDraggableId}`, () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="add-to-timeline"]').first().exists()).toBe( - shouldRender - ); - }); - }); - }); - }); - - test('when clicked, it invokes the `startDragToTimeline` function returned by the `useAddToTimeline` hook', async () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="add-to-timeline"]').first().simulate('click'); - - await waitFor(() => { - wrapper.update(); - expect(mockStartDragToTimeline).toHaveBeenCalled(); - }); - }); - }); - - describe('Top N', () => { - test(`it renders the 'Show top field' button when showTopN is false and an aggregatable string field is provided`, () => { - const aggregatableStringField = 'cloud.account.id'; - const wrapper = mount( - - - - ); - - wrapper.update(); - - expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(true); - }); - - test(`it renders the 'Show top field' button when showTopN is false and a allowlisted signal field is provided`, () => { - const allowlistedField = 'signal.rule.name'; - const wrapper = mount( - - - - ); - - wrapper.update(); - - expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(true); - }); - - test(`it does NOT render the 'Show top field' button when showTopN is false and a field not known to BrowserFields is provided`, () => { - const notKnownToBrowserFields = 'unknown.field'; - const wrapper = mount( - - - - ); - - wrapper.update(); - - expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(false); - }); - - test(`it should invokes goGetTimelineId when user is over the 'Show top field' button`, async () => { - const allowlistedField = 'signal.rule.name'; - const wrapper = mount( - - - - ); - const button = wrapper.find(`[data-test-subj="show-top-field"]`).first(); - button.simulate('mouseenter'); - await waitFor(() => { - expect(goGetTimelineId).toHaveBeenCalledWith(true); - }); - }); - - test(`invokes the toggleTopN function when the 'Show top field' button is clicked`, () => { - const allowlistedField = 'signal.rule.name'; - const wrapper = mount( - - - - ); - - wrapper.update(); - - wrapper.find('[data-test-subj="show-top-field"]').first().simulate('click'); - wrapper.update(); - - expect(toggleTopN).toBeCalled(); - }); - - test(`it does NOT render the Top N histogram when when showTopN is false`, () => { - const allowlistedField = 'signal.rule.name'; - const wrapper = mount( - - - - ); - - wrapper.update(); - - expect(wrapper.find('[data-test-subj="eventsByDatasetOverviewPanel"]').first().exists()).toBe( - false - ); - }); - - test(`it does NOT render the 'Show top field' button when showTopN is true`, () => { - const allowlistedField = 'signal.rule.name'; - const wrapper = mount( - - - - ); - - wrapper.update(); - - expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(false); - }); - - test(`it renders the Top N histogram when when showTopN is true`, () => { - const allowlistedField = 'signal.rule.name'; - const wrapper = mount( - - - - ); - - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="eventsByDatasetOverview-uuid.v4()Panel"]').first().exists() - ).toBe(true); - }); - }); - - describe('Copy to Clipboard', () => { - test(`it renders the 'Copy to Clipboard' button when showTopN is false`, () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="copy-to-clipboard"]`).first().exists()).toBe(true); - }); - - test(`it does NOT render the 'Copy to Clipboard' button when showTopN is true`, () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="copy-to-clipboard"]`).first().exists()).toBe(false); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx deleted file mode 100644 index 71c3114015a03..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx +++ /dev/null @@ -1,425 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiButtonIcon, - EuiFocusTrap, - EuiPanel, - EuiScreenReaderOnly, - EuiToolTip, -} from '@elastic/eui'; - -import React, { useCallback, useEffect, useRef, useMemo, useState } from 'react'; -import { DraggableId } from 'react-beautiful-dnd'; -import styled from 'styled-components'; - -import { getAllFieldsByName } from '../../containers/source'; -import { COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME } from '../../lib/clipboard/clipboard'; -import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; -import { useKibana } from '../../lib/kibana'; -import { createFilter } from '../add_filter_to_global_search_bar'; -import { StatefulTopN } from '../top_n'; - -import { allowTopN } from './helpers'; -import * as i18n from './translations'; -import { useDeepEqualSelector } from '../../hooks/use_selector'; -import { TimelineId } from '../../../../common/types/timeline'; -import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles'; -import { SourcererScopeName } from '../../store/sourcerer/model'; -import { useSourcererScope } from '../../containers/sourcerer'; -import { timelineSelectors } from '../../../timelines/store/timeline'; -import { stopPropagationAndPreventDefault } from '../../../../../timelines/public'; -import { TooltipWithKeyboardShortcut } from '../accessibility'; - -export const AdditionalContent = styled.div` - padding: 2px; -`; - -AdditionalContent.displayName = 'AdditionalContent'; - -const getAdditionalScreenReaderOnlyContext = ({ - field, - value, -}: { - field: string; - value?: string[] | string | null; -}): string => { - if (value == null) { - return field; - } - - return Array.isArray(value) ? `${field} ${value.join(' ')}` : `${field} ${value}`; -}; - -const FILTER_FOR_VALUE_KEYBOARD_SHORTCUT = 'f'; -const FILTER_OUT_VALUE_KEYBOARD_SHORTCUT = 'o'; -const ADD_TO_TIMELINE_KEYBOARD_SHORTCUT = 'a'; -const SHOW_TOP_N_KEYBOARD_SHORTCUT = 't'; -const COPY_TO_CLIPBOARD_KEYBOARD_SHORTCUT = 'c'; - -interface Props { - additionalContent?: React.ReactNode; - closePopOver?: () => void; - draggableId?: DraggableId; - field: string; - goGetTimelineId?: (args: boolean) => void; - onFilterAdded?: () => void; - ownFocus: boolean; - showTopN: boolean; - timelineId?: string | null; - toggleTopN: () => void; - value?: string[] | string | null; -} - -/** Returns a value for the `disabled` prop of `EuiFocusTrap` */ -const isFocusTrapDisabled = ({ - ownFocus, - showTopN, -}: { - ownFocus: boolean; - showTopN: boolean; -}): boolean => { - if (showTopN) { - return false; // we *always* want to trap focus when showing Top N - } - - return !ownFocus; -}; - -const DraggableWrapperHoverContentComponent: React.FC = ({ - additionalContent = null, - closePopOver, - draggableId, - field, - goGetTimelineId, - onFilterAdded, - ownFocus, - showTopN, - timelineId, - toggleTopN, - value, -}) => { - const kibana = useKibana(); - const { timelines } = kibana.services; - const { startDragToTimeline } = timelines.getUseAddToTimeline()({ - draggableId, - fieldName: field, - }); - const filterManagerBackup = useMemo(() => kibana.services.data.query.filterManager, [ - kibana.services.data.query.filterManager, - ]); - const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []); - const { filterManager: activeFilterMananager } = useDeepEqualSelector((state) => - getManageTimeline(state, timelineId ?? '') - ); - const defaultFocusedButtonRef = useRef(null); - const panelRef = useRef(null); - - const filterManager = useMemo( - () => (timelineId === TimelineId.active ? activeFilterMananager : filterManagerBackup), - [timelineId, activeFilterMananager, filterManagerBackup] - ); - - // Regarding data from useManageTimeline: - // * `indexToAdd`, which enables the alerts index to be appended to - // the `indexPattern` returned by `useWithSource`, may only be populated when - // this component is rendered in the context of the active timeline. This - // behavior enables the 'All events' view by appending the alerts index - // to the index pattern. - const activeScope: SourcererScopeName = - timelineId === TimelineId.active - ? SourcererScopeName.timeline - : timelineId != null && - [TimelineId.detectionsPage, TimelineId.detectionsRulesDetailsPage].includes( - timelineId as TimelineId - ) - ? SourcererScopeName.detections - : SourcererScopeName.default; - const { browserFields, indexPattern } = useSourcererScope(activeScope); - const handleStartDragToTimeline = useCallback(() => { - startDragToTimeline(); - if (closePopOver != null) { - closePopOver(); - } - }, [closePopOver, startDragToTimeline]); - - const filterForValue = useCallback(() => { - const filter = - value?.length === 0 ? createFilter(field, undefined) : createFilter(field, value); - const activeFilterManager = filterManager; - - if (activeFilterManager != null) { - activeFilterManager.addFilters(filter); - if (closePopOver != null) { - closePopOver(); - } - if (onFilterAdded != null) { - onFilterAdded(); - } - } - }, [closePopOver, field, value, filterManager, onFilterAdded]); - - const filterOutValue = useCallback(() => { - const filter = - value?.length === 0 ? createFilter(field, null, false) : createFilter(field, value, true); - const activeFilterManager = filterManager; - - if (activeFilterManager != null) { - activeFilterManager.addFilters(filter); - - if (closePopOver != null) { - closePopOver(); - } - if (onFilterAdded != null) { - onFilterAdded(); - } - } - }, [closePopOver, field, value, filterManager, onFilterAdded]); - - const isInit = useRef(true); - - useEffect(() => { - if (isInit.current && goGetTimelineId != null && timelineId == null) { - isInit.current = false; - goGetTimelineId(true); - } - }, [goGetTimelineId, timelineId]); - - useEffect(() => { - if (ownFocus) { - setTimeout(() => { - defaultFocusedButtonRef.current?.focus(); - }, 0); - } - }, [ownFocus]); - - const onKeyDown = useCallback( - (keyboardEvent: React.KeyboardEvent) => { - if (!ownFocus) { - return; - } - - switch (keyboardEvent.key) { - case FILTER_FOR_VALUE_KEYBOARD_SHORTCUT: - stopPropagationAndPreventDefault(keyboardEvent); - filterForValue(); - break; - case FILTER_OUT_VALUE_KEYBOARD_SHORTCUT: - stopPropagationAndPreventDefault(keyboardEvent); - filterOutValue(); - break; - case ADD_TO_TIMELINE_KEYBOARD_SHORTCUT: - stopPropagationAndPreventDefault(keyboardEvent); - handleStartDragToTimeline(); - break; - case SHOW_TOP_N_KEYBOARD_SHORTCUT: - stopPropagationAndPreventDefault(keyboardEvent); - toggleTopN(); - break; - case COPY_TO_CLIPBOARD_KEYBOARD_SHORTCUT: - stopPropagationAndPreventDefault(keyboardEvent); - const copyToClipboardButton = panelRef.current?.querySelector( - `.${COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME}` - ); - if (copyToClipboardButton != null) { - copyToClipboardButton.click(); - if (closePopOver != null) { - closePopOver(); - } - } - break; - case 'Enter': - break; - case 'Escape': - stopPropagationAndPreventDefault(keyboardEvent); - if (closePopOver != null) { - closePopOver(); - } - break; - default: - break; - } - }, - - [closePopOver, filterForValue, filterOutValue, handleStartDragToTimeline, ownFocus, toggleTopN] - ); - - return ( - - - -

{i18n.YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS(field)}

-
- - {additionalContent != null && {additionalContent}} - - {!showTopN && value != null && ( - - } - > - - - )} - - {!showTopN && value != null && ( - - } - > - - - )} - - {!showTopN && value != null && draggableId != null && ( - - } - > - - - )} - - <> - {allowTopN({ - browserField: getAllFieldsByName(browserFields)[field], - fieldName: field, - }) && ( - <> - {!showTopN && ( - - } - > - - - )} - - {showTopN && ( - - )} - - )} - - - {!showTopN && ( - - )} -
-
- ); -}; - -DraggableWrapperHoverContentComponent.displayName = 'DraggableWrapperHoverContentComponent'; - -export const DraggableWrapperHoverContent = React.memo(DraggableWrapperHoverContentComponent); - -export const useGetTimelineId = function ( - elem: React.MutableRefObject, - getTimelineId: boolean = false -) { - const [timelineId, setTimelineId] = useState(null); - - useEffect(() => { - let startElem: Element | (Node & ParentNode) | null = elem.current; - if (startElem != null && getTimelineId) { - for (; startElem && startElem !== document; startElem = startElem.parentNode) { - const myElem: Element = startElem as Element; - if ( - myElem != null && - myElem.classList != null && - myElem.classList.contains(SELECTOR_TIMELINE_GLOBAL_CONTAINER) && - myElem.hasAttribute('data-timeline-id') - ) { - setTimelineId(myElem.getAttribute('data-timeline-id')); - break; - } - } - } - }, [elem, getTimelineId]); - - return timelineId; -}; diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/use_get_timeline_id_from_dom.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/use_get_timeline_id_from_dom.tsx new file mode 100644 index 0000000000000..fcb547842aec4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/use_get_timeline_id_from_dom.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; + +import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles'; + +export const useGetTimelineId = function ( + elem: React.MutableRefObject, + getTimelineId: boolean = false +) { + const [timelineId, setTimelineId] = useState(null); + + useEffect(() => { + let startElem: Element | (Node & ParentNode) | null = elem.current; + if (startElem != null && getTimelineId) { + for (; startElem && startElem !== document; startElem = startElem.parentNode) { + const myElem: Element = startElem as Element; + if ( + myElem != null && + myElem.classList != null && + myElem.classList.contains(SELECTOR_TIMELINE_GLOBAL_CONTAINER) && + myElem.hasAttribute('data-timeline-id') + ) { + setTimelineId(myElem.getAttribute('data-timeline-id')); + break; + } + } + } + }, [elem, getTimelineId]); + + return timelineId; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/draggables/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/draggables/__snapshots__/index.test.tsx.snap index 93608a181adff..6b27cf5969f1a 100644 --- a/x-pack/plugins/security_solution/public/common/components/draggables/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/draggables/__snapshots__/index.test.tsx.snap @@ -36,6 +36,7 @@ exports[`draggables rendering it renders the default DefaultDraggable 1`] = ` }, } } + isDraggable={true} render={[Function]} /> `; diff --git a/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx b/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx index df92da0c7d056..6ac1746d77709 100644 --- a/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx @@ -20,6 +20,7 @@ import { Provider } from '../../../timelines/components/timeline/data_providers/ export interface DefaultDraggableType { id: string; + isDraggable?: boolean; field: string; value?: string | null; name?: string | null; @@ -79,6 +80,7 @@ Content.displayName = 'Content'; * that's only displayed when the specified value is non-`null`. * * @param id - a unique draggable id, which typically follows the format `${contextId}-${eventId}-${field}-${value}` + * @param isDraggable - optional prop to disable drag & drop and it will defaulted to true * @param field - the name of the field, e.g. `network.transport` * @param value - value of the field e.g. `tcp` * @param name - defaulting to `field`, this optional human readable name is used by the `DataProvider` that represents the data @@ -88,7 +90,17 @@ Content.displayName = 'Content'; * @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data */ export const DefaultDraggable = React.memo( - ({ id, field, value, name, children, timelineId, tooltipContent, queryValue }) => { + ({ + id, + isDraggable = true, + field, + value, + name, + children, + timelineId, + tooltipContent, + queryValue, + }) => { const dataProviderProp: DataProvider = useMemo( () => ({ and: [], @@ -125,6 +137,7 @@ export const DefaultDraggable = React.memo( return ( @@ -155,6 +168,7 @@ export type BadgeDraggableType = Omit & { * @param field - the name of the field, e.g. `network.transport` * @param value - value of the field e.g. `tcp` * @param iconType -the (optional) type of icon e.g. `snowflake` to display on the badge + * @param isDraggable * @param name - defaulting to `field`, this optional human readable name is used by the `DataProvider` that represents the data * @param color - defaults to `hollow`, optionally overwrite the color of the badge icon * @param children - defaults to displaying `value`, this allows an arbitrary visualization to be displayed in lieu of the default behavior @@ -168,6 +182,7 @@ const DraggableBadgeComponent: React.FC = ({ field, value, iconType, + isDraggable, name, color = 'hollow', children, @@ -177,6 +192,7 @@ const DraggableBadgeComponent: React.FC = ({ value != null ? ( = ({ key={key} contextId={key} eventId={eventId} + isDraggable={false} fieldName={fieldName || 'unknown'} value={value} /> diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx index 80c014771ae68..28a90e94c0ca4 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx @@ -245,7 +245,7 @@ describe('EventFieldsBrowser', () => { /> ); - expect(wrapper.find('[data-test-subj="draggable-content-@timestamp"]').at(0).text()).toEqual( + expect(wrapper.find('[data-test-subj="localized-date-tool-tip"]').at(0).text()).toEqual( 'Feb 28, 2019 @ 16:50:54.621' ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx index f5cf600e281ad..67b1874eea0a0 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx @@ -6,11 +6,10 @@ */ import React, { useCallback, useState, useRef } from 'react'; -import { getDraggableId } from '@kbn/securitysolution-t-grid'; import { HoverActions } from '../../hover_actions'; import { useActionCellDataProvider } from './use_action_cell_data_provider'; import { EventFieldsData } from '../types'; -import { useGetTimelineId } from '../../drag_and_drop/draggable_wrapper_hover_content'; +import { useGetTimelineId } from '../../drag_and_drop/use_get_timeline_id_from_dom'; import { ColumnHeaderOptions } from '../../../../../common/types/timeline'; import { BrowserField } from '../../../containers/source'; @@ -66,11 +65,10 @@ export const ActionCell: React.FC = React.memo( }); }, []); - const draggableIds = actionCellConfig?.idList.map((id) => getDraggableId(id)); return ( ({ + and: [], + enabled: true, + id: escapeDataProviderId(id), + name: field, + excluded: false, + kqlQuery: '', + queryMatch: { + field, + value, + operator: IS_OPERATOR, + }, +}); + export const useActionCellDataProvider = ({ contextId, eventId, @@ -50,72 +66,90 @@ export const useActionCellDataProvider = ({ isObjectArray, linkValue, values, -}: UseActionCellDataProvider): { idList: string[]; stringValues: string[] } | null => { - if (values === null || values === undefined) return null; - - const stringifiedValues: string[] = []; - const arrayValues = Array.isArray(values) ? values : [values]; +}: UseActionCellDataProvider): { + stringValues: string[]; + dataProvider: DataProvider[]; +} | null => { + const cellData = useMemo(() => { + if (values === null || values === undefined) return null; + const arrayValues = Array.isArray(values) ? values : [values]; + return arrayValues.reduce<{ + stringValues: string[]; + dataProvider: DataProvider[]; + }>( + (memo, value, index) => { + let id: string = ''; + let valueAsString: string = isString(value) ? value : `${values}`; + const appendedUniqueId = `${contextId}-${eventId}-${field}-${index}-${value}`; + if (fieldFromBrowserField == null) { + memo.stringValues.push(valueAsString); + return memo; + } - const idList: string[] = arrayValues.reduce((memo, value, index) => { - let id = null; - let valueAsString: string = isString(value) ? value : `${values}`; - if (fieldFromBrowserField == null) { - stringifiedValues.push(valueAsString); - return memo; - } - const appendedUniqueId = `${contextId}-${eventId}-${field}-${index}-${value}-${eventId}-${field}-${value}`; - if (isObjectArray || fieldType === GEO_FIELD_TYPE || [MESSAGE_FIELD_NAME].includes(field)) { - stringifiedValues.push(valueAsString); - return memo; - } else if (fieldType === IP_FIELD_TYPE) { - id = `formatted-ip-data-provider-${contextId}-${field}-${value}-${eventId}`; - if (isString(value) && !isEmpty(value)) { - try { - const addresses = JSON.parse(value); - if (isArray(addresses)) { - valueAsString = addresses.join(','); + if (isObjectArray || fieldType === GEO_FIELD_TYPE || [MESSAGE_FIELD_NAME].includes(field)) { + memo.stringValues.push(valueAsString); + return memo; + } else if (fieldType === IP_FIELD_TYPE) { + id = `formatted-ip-data-provider-${contextId}-${field}-${value}-${eventId}`; + if (isString(value) && !isEmpty(value)) { + try { + const addresses = JSON.parse(value); + if (isArray(addresses)) { + valueAsString = addresses.join(','); + addresses.forEach((ip) => memo.dataProvider.push(getDataProvider(field, id, ip))); + } + } catch (_) { + // Default to keeping the existing string value + } + memo.stringValues.push(valueAsString); + return memo; } - } catch (_) { - // Default to keeping the existing string value + } else if (PORT_NAMES.some((portName) => field === portName)) { + id = `port-default-draggable-${appendedUniqueId}`; + } else if (field === EVENT_DURATION_FIELD_NAME) { + id = `duration-default-draggable-${appendedUniqueId}`; + } else if (field === HOST_NAME_FIELD_NAME) { + id = `event-details-value-default-draggable-${appendedUniqueId}`; + } else if (fieldFormat === BYTES_FORMAT) { + id = `bytes-default-draggable-${appendedUniqueId}`; + } else if (field === SIGNAL_RULE_NAME_FIELD_NAME) { + id = `event-details-value-default-draggable-${appendedUniqueId}-${linkValue}`; + } else if (field === EVENT_MODULE_FIELD_NAME) { + id = `event-details-value-default-draggable-${appendedUniqueId}-${value}`; + } else if (field === SIGNAL_STATUS_FIELD_NAME) { + id = `alert-details-value-default-draggable-${appendedUniqueId}`; + } else if (field === AGENT_STATUS_FIELD_NAME) { + const valueToUse = typeof value === 'string' ? value : ''; + id = `event-details-value-default-draggable-${appendedUniqueId}`; + valueAsString = valueToUse; + } else if ( + [ + RULE_REFERENCE_FIELD_NAME, + REFERENCE_URL_FIELD_NAME, + EVENT_URL_FIELD_NAME, + INDICATOR_REFERENCE, + ].includes(field) + ) { + id = `event-details-value-default-draggable-${appendedUniqueId}-${value}`; + } else { + id = `event-details-value-default-draggable-${appendedUniqueId}`; } - } - } else if (PORT_NAMES.some((portName) => field === portName)) { - id = `port-default-draggable-${appendedUniqueId}`; - } else if (field === EVENT_DURATION_FIELD_NAME) { - id = `duration-default-draggable-${appendedUniqueId}`; - } else if (field === HOST_NAME_FIELD_NAME) { - id = `event-details-value-default-draggable-${appendedUniqueId}`; - } else if (fieldFormat === BYTES_FORMAT) { - id = `bytes-default-draggable-${appendedUniqueId}`; - } else if (field === SIGNAL_RULE_NAME_FIELD_NAME) { - id = `event-details-value-default-draggable-${appendedUniqueId}-${linkValue}`; - } else if (field === EVENT_MODULE_FIELD_NAME) { - id = `event-details-value-default-draggable-${appendedUniqueId}-${value}`; - } else if (field === SIGNAL_STATUS_FIELD_NAME) { - id = `alert-details-value-default-draggable-${appendedUniqueId}`; - } else if (field === AGENT_STATUS_FIELD_NAME) { - const valueToUse = typeof value === 'string' ? value : ''; - id = `event-details-value-default-draggable-${appendedUniqueId}`; - valueAsString = valueToUse; - } else if ( - [ - RULE_REFERENCE_FIELD_NAME, - REFERENCE_URL_FIELD_NAME, - EVENT_URL_FIELD_NAME, - INDICATOR_REFERENCE, - ].includes(field) - ) { - id = `event-details-value-default-draggable-${appendedUniqueId}-${value}`; - } else { - id = `event-details-value-default-draggable-${appendedUniqueId}`; - } - stringifiedValues.push(valueAsString); - memo.push(escapeDataProviderId(id)); - return memo; - }, [] as string[]); - - return { - idList, - stringValues: stringifiedValues, - }; + memo.stringValues.push(valueAsString); + memo.dataProvider.push(getDataProvider(field, id, value)); + return memo; + }, + { stringValues: [], dataProvider: [] } + ); + }, [ + contextId, + eventId, + field, + fieldFormat, + fieldFromBrowserField, + fieldType, + isObjectArray, + linkValue, + values, + ]); + return cellData; }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 5347ee875181b..83006f09a14be 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -823,7 +823,8 @@ describe('Exception helpers', () => { }, ]); }); - + }); + describe('ransomware protection exception items', () => { test('it should return pre-populated ransomware items for event code `ransomware`', () => { const defaultItems = defaultEndpointExceptionItems('list_id', 'my_rule', { _id: '123', @@ -938,7 +939,9 @@ describe('Exception helpers', () => { }, ]); }); + }); + describe('memory protection exception items', () => { test('it should return pre-populated memory signature items for event code `memory_signature`', () => { const defaultItems = defaultEndpointExceptionItems('list_id', 'my_rule', { _id: '123', @@ -990,6 +993,44 @@ describe('Exception helpers', () => { ]); }); + test('it should return pre-populated memory signature items for event code `memory_signature` and skip Empty', () => { + const defaultItems = defaultEndpointExceptionItems('list_id', 'my_rule', { + _id: '123', + process: { + name: '', // name is empty + // executable: '', left intentionally commented + hash: { + sha256: 'some hash', + }, + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + Memory_protection: { + feature: 'signature', + }, + event: { + code: 'memory_signature', + }, + }); + + // should not contain name or executable + expect(defaultItems[0].entries).toEqual([ + { + field: 'Memory_protection.feature', + operator: 'included', + type: 'match', + value: 'signature', + id: '123', + }, + { + field: 'process.hash.sha256', + operator: 'included', + type: 'match', + value: 'some hash', + id: '123', + }, + ]); + }); + test('it should return pre-populated memory shellcode items for event code `malicious_thread`', () => { const defaultItems = defaultEndpointExceptionItems('list_id', 'my_rule', { _id: '123', @@ -1085,7 +1126,115 @@ describe('Exception helpers', () => { value: '4000', id: '123', }, - { field: 'region_size', operator: 'included', type: 'match', value: '4000', id: '123' }, + { + field: 'region_size', + operator: 'included', + type: 'match', + value: '4000', + id: '123', + }, + { + field: 'region_protection', + operator: 'included', + type: 'match', + value: 'RWX', + id: '123', + }, + { + field: 'memory_pe.imphash', + operator: 'included', + type: 'match', + value: 'a hash', + id: '123', + }, + ], + id: '123', + }, + ]); + }); + + test('it should return pre-populated memory shellcode items for event code `malicious_thread` and skip empty', () => { + const defaultItems = defaultEndpointExceptionItems('list_id', 'my_rule', { + _id: '123', + process: { + name: '', // name is empty + // executable: '', left intentionally commented + Ext: { + token: { + integrity_level_name: 'high', + }, + }, + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + Memory_protection: { + feature: 'shellcode_thread', + self_injection: true, + }, + event: { + code: 'malicious_thread', + }, + Target: { + process: { + thread: { + Ext: { + start_address_allocation_offset: 0, + start_address_bytes_disasm_hash: 'a disam hash', + start_address_details: { + // allocation_type: '', left intentionally commented + allocation_size: 4000, + region_size: 4000, + region_protection: 'RWX', + memory_pe: { + imphash: 'a hash', + }, + }, + }, + }, + }, + }, + }); + + // no name, no exceutable, no allocation_type + expect(defaultItems[0].entries).toEqual([ + { + field: 'Memory_protection.feature', + operator: 'included', + type: 'match', + value: 'shellcode_thread', + id: '123', + }, + { + field: 'Memory_protection.self_injection', + operator: 'included', + type: 'match', + value: 'true', + id: '123', + }, + { + field: 'process.Ext.token.integrity_level_name', + operator: 'included', + type: 'match', + value: 'high', + id: '123', + }, + { + field: 'Target.process.thread.Ext.start_address_details', + type: 'nested', + entries: [ + { + field: 'allocation_size', + operator: 'included', + type: 'match', + value: '4000', + id: '123', + }, + { + field: 'region_size', + operator: 'included', + type: 'match', + value: '4000', + id: '123', + }, { field: 'region_protection', operator: 'included', diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index 613d295545461..62250a0933ffb 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -343,6 +343,29 @@ export const getCodeSignatureValue = ( } }; +// helper type to filter empty-valued exception entries +interface ExceptionEntry { + value?: string; + entries?: ExceptionEntry[]; +} + +/** + * Takes an array of Entries and filter out the ones with empty values. + * It will also filter out empty values for nested entries. + */ +function filterEmptyExceptionEntries(entries: T[]): T[] { + const finalEntries: T[] = []; + for (const entry of entries) { + if (entry.entries !== undefined) { + entry.entries = entry.entries.filter((el) => el.value !== undefined && el.value.length > 0); + finalEntries.push(entry); + } else if (entry.value !== undefined && entry.value.length > 0) { + finalEntries.push(entry); + } + } + return finalEntries; +} + /** * Returns the default values from the alert data to autofill new endpoint exceptions */ @@ -510,34 +533,35 @@ export const getPrepopulatedMemorySignatureException = ({ alertEcsData: Flattened; }): ExceptionsBuilderExceptionItem => { const { process } = alertEcsData; + const entries = filterEmptyExceptionEntries([ + { + field: 'Memory_protection.feature', + operator: 'included' as const, + type: 'match' as const, + value: alertEcsData.Memory_protection?.feature ?? '', + }, + { + field: 'process.executable.caseless', + operator: 'included' as const, + type: 'match' as const, + value: process?.executable ?? '', + }, + { + field: 'process.name.caseless', + operator: 'included' as const, + type: 'match' as const, + value: process?.name ?? '', + }, + { + field: 'process.hash.sha256', + operator: 'included' as const, + type: 'match' as const, + value: process?.hash?.sha256 ?? '', + }, + ]); return { ...getNewExceptionItem({ listId, namespaceType: listNamespace, ruleName }), - entries: addIdToEntries([ - { - field: 'Memory_protection.feature', - operator: 'included', - type: 'match', - value: alertEcsData.Memory_protection?.feature ?? '', - }, - { - field: 'process.executable.caseless', - operator: 'included', - type: 'match', - value: process?.executable ?? '', - }, - { - field: 'process.name.caseless', - operator: 'included', - type: 'match', - value: process?.name ?? '', - }, - { - field: 'process.hash.sha256', - operator: 'included', - type: 'match', - value: process?.hash?.sha256 ?? '', - }, - ]), + entries: addIdToEntries(entries), }; }; export const getPrepopulatedMemoryShellcodeException = ({ @@ -554,81 +578,83 @@ export const getPrepopulatedMemoryShellcodeException = ({ alertEcsData: Flattened; }): ExceptionsBuilderExceptionItem => { const { process, Target } = alertEcsData; + const entries = filterEmptyExceptionEntries([ + { + field: 'Memory_protection.feature', + operator: 'included' as const, + type: 'match' as const, + value: alertEcsData.Memory_protection?.feature ?? '', + }, + { + field: 'Memory_protection.self_injection', + operator: 'included' as const, + type: 'match' as const, + value: String(alertEcsData.Memory_protection?.self_injection) ?? '', + }, + { + field: 'process.executable.caseless', + operator: 'included' as const, + type: 'match' as const, + value: process?.executable ?? '', + }, + { + field: 'process.name.caseless', + operator: 'included' as const, + type: 'match' as const, + value: process?.name ?? '', + }, + { + field: 'process.Ext.token.integrity_level_name', + operator: 'included' as const, + type: 'match' as const, + value: process?.Ext?.token?.integrity_level_name ?? '', + }, + { + field: 'Target.process.thread.Ext.start_address_details', + type: 'nested' as const, + entries: [ + { + field: 'allocation_type', + operator: 'included' as const, + type: 'match' as const, + value: Target?.process?.thread?.Ext?.start_address_details?.allocation_type ?? '', + }, + { + field: 'allocation_size', + operator: 'included' as const, + type: 'match' as const, + value: String(Target?.process?.thread?.Ext?.start_address_details?.allocation_size) ?? '', + }, + { + field: 'region_size', + operator: 'included' as const, + type: 'match' as const, + value: String(Target?.process?.thread?.Ext?.start_address_details?.region_size) ?? '', + }, + { + field: 'region_protection', + operator: 'included' as const, + type: 'match' as const, + value: + String(Target?.process?.thread?.Ext?.start_address_details?.region_protection) ?? '', + }, + { + field: 'memory_pe.imphash', + operator: 'included' as const, + type: 'match' as const, + value: + String(Target?.process?.thread?.Ext?.start_address_details?.memory_pe?.imphash) ?? '', + }, + ], + }, + ]); + return { ...getNewExceptionItem({ listId, namespaceType: listNamespace, ruleName }), - entries: addIdToEntries([ - { - field: 'Memory_protection.feature', - operator: 'included', - type: 'match', - value: alertEcsData.Memory_protection?.feature ?? '', - }, - { - field: 'Memory_protection.self_injection', - operator: 'included', - type: 'match', - value: String(alertEcsData.Memory_protection?.self_injection) ?? '', - }, - { - field: 'process.executable.caseless', - operator: 'included', - type: 'match', - value: process?.executable ?? '', - }, - { - field: 'process.name.caseless', - operator: 'included', - type: 'match', - value: process?.name ?? '', - }, - { - field: 'process.Ext.token.integrity_level_name', - operator: 'included', - type: 'match', - value: process?.Ext?.token?.integrity_level_name ?? '', - }, - { - field: 'Target.process.thread.Ext.start_address_details', - type: 'nested', - entries: [ - { - field: 'allocation_type', - operator: 'included', - type: 'match', - value: Target?.process?.thread?.Ext?.start_address_details?.allocation_type ?? '', - }, - { - field: 'allocation_size', - operator: 'included', - type: 'match', - value: - String(Target?.process?.thread?.Ext?.start_address_details?.allocation_size) ?? '', - }, - { - field: 'region_size', - operator: 'included', - type: 'match', - value: String(Target?.process?.thread?.Ext?.start_address_details?.region_size) ?? '', - }, - { - field: 'region_protection', - operator: 'included', - type: 'match', - value: - String(Target?.process?.thread?.Ext?.start_address_details?.region_protection) ?? '', - }, - { - field: 'memory_pe.imphash', - operator: 'included', - type: 'match', - value: - String(Target?.process?.thread?.Ext?.start_address_details?.memory_pe?.imphash) ?? '', - }, - ], - }, - ]), + entries: addIdToEntries(entries), }; }; + /** * Determines whether or not any entries within the given exceptionItems contain values not in the specified ECS mapping */ diff --git a/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx index fe32b7addd25e..f733331bcd691 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx @@ -49,6 +49,7 @@ export interface HeaderSectionProps extends HeaderProps { tooltip?: string; growLeftSplit?: boolean; inspectMultiple?: boolean; + hideSubtitle?: boolean; } const HeaderSectionComponent: React.FC = ({ @@ -64,6 +65,7 @@ const HeaderSectionComponent: React.FC = ({ tooltip, growLeftSplit = true, inspectMultiple = false, + hideSubtitle = false, }) => (
@@ -82,7 +84,9 @@ const HeaderSectionComponent: React.FC = ({ - + {!hideSubtitle && ( + + )} {id && ( diff --git a/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx b/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx index 31bdf78626e7c..a1ba33f30cc55 100644 --- a/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx @@ -6,16 +6,17 @@ */ import { EuiFocusTrap, EuiScreenReaderOnly } from '@elastic/eui'; -import React, { useCallback, useEffect, useRef, useMemo } from 'react'; +import React, { useCallback, useEffect, useRef, useMemo, useState } from 'react'; import { DraggableId } from 'react-beautiful-dnd'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; -import { getAllFieldsByName } from '../../containers/source'; -import { COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME } from '../../lib/clipboard/clipboard'; +import { isEmpty } from 'lodash'; + import { useKibana } from '../../lib/kibana'; +import { getAllFieldsByName } from '../../containers/source'; import { allowTopN } from './utils'; import { useDeepEqualSelector } from '../../hooks/use_selector'; -import { ColumnHeaderOptions, TimelineId } from '../../../../common/types/timeline'; +import { ColumnHeaderOptions, DataProvider, TimelineId } from '../../../../common/types/timeline'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { useSourcererScope } from '../../containers/sourcerer'; import { timelineSelectors } from '../../../timelines/store/timeline'; @@ -38,43 +39,51 @@ export const AdditionalContent = styled.div` AdditionalContent.displayName = 'AdditionalContent'; -const StyledHoverActionsContainer = styled.div<{ $showTopN: boolean }>` +const StyledHoverActionsContainer = styled.div<{ $showTopN: boolean; $showOwnFocus: boolean }>` padding: ${(props) => `0 ${props.theme.eui.paddingSizes.s}`}; display: flex; - &:focus-within { - .timelines__hoverActionButton, - .securitySolution__hoverActionButton { - opacity: 1; + ${(props) => + props.$showOwnFocus + ? ` + &:focus-within { + .timelines__hoverActionButton, + .securitySolution__hoverActionButton { + opacity: 1; + } } - } - &:hover { - .timelines__hoverActionButton, - .securitySolution__hoverActionButton { - opacity: 1; + &:hover { + .timelines__hoverActionButton, + .securitySolution__hoverActionButton { + opacity: 1; + } } - } .timelines__hoverActionButton, .securitySolution__hoverActionButton { - opacity: ${(props) => (props.$showTopN ? 1 : 0)}; + opacity: ${props.$showTopN ? 1 : 0}; - &:focus { - opacity: 1; + &:focus { + opacity: 1; + } } - } + ` + : ''} `; interface Props { additionalContent?: React.ReactNode; + closePopOver?: () => void; + dataProvider?: DataProvider | DataProvider[]; dataType?: string; - draggableIds?: DraggableId[]; + draggableId?: DraggableId; field: string; goGetTimelineId?: (args: boolean) => void; isObjectArray: boolean; onFilterAdded?: () => void; ownFocus: boolean; + showOwnFocus?: boolean; showTopN: boolean; timelineId?: string | null; toggleColumn?: (column: ColumnHeaderOptions) => void; @@ -100,13 +109,15 @@ const isFocusTrapDisabled = ({ export const HoverActions: React.FC = React.memo( ({ additionalContent = null, + dataProvider, dataType, - draggableIds, + draggableId, field, goGetTimelineId, isObjectArray, onFilterAdded, ownFocus, + showOwnFocus = true, showTopN, timelineId, toggleColumn, @@ -117,29 +128,13 @@ export const HoverActions: React.FC = React.memo( const { timelines } = kibana.services; // Common actions used by the alert table and alert flyout const { - addToTimeline: { - AddToTimelineButton, - keyboardShortcut: addToTimelineKeyboardShortcut, - useGetHandleStartDragToTimeline, - }, - columnToggle: { - ColumnToggleButton, - columnToggleFn, - keyboardShortcut: columnToggleKeyboardShortcut, - }, - copy: { CopyButton, keyboardShortcut: copyKeyboardShortcut }, - filterForValue: { - FilterForValueButton, - filterForValueFn, - keyboardShortcut: filterForValueKeyboardShortcut, - }, - filterOutValue: { - FilterOutValueButton, - filterOutValueFn, - keyboardShortcut: filterOutValueKeyboardShortcut, - }, + getAddToTimelineButton, + getColumnToggleButton, + getCopyButton, + getFilterForValueButton, + getFilterOutValueButton, } = timelines.getHoverActions(); - + const [stKeyboardEvent, setStKeyboardEvent] = useState(); const filterManagerBackup = useMemo(() => kibana.services.data.query.filterManager, [ kibana.services.data.query.filterManager, ]); @@ -169,30 +164,8 @@ export const HoverActions: React.FC = React.memo( : SourcererScopeName.default; const { browserFields } = useSourcererScope(activeScope); - const handleStartDragToTimeline = (() => { - const handleStartDragToTimelineFns = draggableIds?.map((draggableId) => { - // eslint-disable-next-line react-hooks/rules-of-hooks - return useGetHandleStartDragToTimeline({ draggableId, field }); - }); - return () => handleStartDragToTimelineFns?.forEach((dragFn) => dragFn()); - })(); - - const handleFilterForValue = useCallback(() => { - filterForValueFn({ field, value: values, filterManager, onFilterAdded }); - }, [filterForValueFn, field, values, filterManager, onFilterAdded]); - - const handleFilterOutValue = useCallback(() => { - filterOutValueFn({ field, value: values, filterManager, onFilterAdded }); - }, [filterOutValueFn, field, values, filterManager, onFilterAdded]); - - const handleToggleColumn = useCallback( - () => (toggleColumn ? columnToggleFn({ toggleColumn, field }) : null), - [columnToggleFn, field, toggleColumn] - ); - const isInit = useRef(true); const defaultFocusedButtonRef = useRef(null); - const panelRef = useRef(null); useEffect(() => { if (isInit.current && goGetTimelineId != null && timelineId == null) { @@ -215,31 +188,6 @@ export const HoverActions: React.FC = React.memo( return; } switch (keyboardEvent.key) { - case addToTimelineKeyboardShortcut: - stopPropagationAndPreventDefault(keyboardEvent); - handleStartDragToTimeline(); - break; - case columnToggleKeyboardShortcut: - stopPropagationAndPreventDefault(keyboardEvent); - handleToggleColumn(); - break; - case copyKeyboardShortcut: - stopPropagationAndPreventDefault(keyboardEvent); - const copyToClipboardButton = panelRef.current?.querySelector( - `.${COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME}` - ); - if (copyToClipboardButton != null) { - copyToClipboardButton.click(); - } - break; - case filterForValueKeyboardShortcut: - stopPropagationAndPreventDefault(keyboardEvent); - handleFilterForValue(); - break; - case filterOutValueKeyboardShortcut: - stopPropagationAndPreventDefault(keyboardEvent); - handleFilterOutValue(); - break; case SHOW_TOP_N_KEYBOARD_SHORTCUT: stopPropagationAndPreventDefault(keyboardEvent); toggleTopN(); @@ -250,33 +198,26 @@ export const HoverActions: React.FC = React.memo( stopPropagationAndPreventDefault(keyboardEvent); break; default: + setStKeyboardEvent(keyboardEvent); break; } }, - [ - addToTimelineKeyboardShortcut, - columnToggleKeyboardShortcut, - copyKeyboardShortcut, - filterForValueKeyboardShortcut, - filterOutValueKeyboardShortcut, - handleFilterForValue, - handleFilterOutValue, - handleStartDragToTimeline, - handleToggleColumn, - ownFocus, - toggleTopN, - ] + [ownFocus, toggleTopN] ); const showFilters = values != null; return ( - - +

{YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS(field)}

@@ -286,46 +227,58 @@ export const HoverActions: React.FC = React.memo( {showFilters && ( <> - - +
+ {getFilterForValueButton({ + defaultFocusedButtonRef, + field, + filterManager, + keyboardEvent: stKeyboardEvent, + onFilterAdded, + ownFocus, + showTooltip: true, + value: values, + })} +
+
+ {getFilterOutValueButton({ + field, + filterManager, + keyboardEvent: stKeyboardEvent, + onFilterAdded, + ownFocus, + showTooltip: true, + value: values, + })} +
)} {toggleColumn && ( - +
+ {getColumnToggleButton({ + field, + isDisabled: isObjectArray && dataType !== 'geo_point', + isObjectArray, + keyboardEvent: stKeyboardEvent, + ownFocus, + showTooltip: true, + toggleColumn, + value: values, + })} +
)} - {showFilters && draggableIds != null && ( - + {showFilters && (draggableId != null || !isEmpty(dataProvider)) && ( +
+ {getAddToTimelineButton({ + dataProvider, + draggableId, + field, + keyboardEvent: stKeyboardEvent, + ownFocus, + showTooltip: true, + value: values, + })} +
)} {allowTopN({ browserField: getAllFieldsByName(browserFields)[field], @@ -342,18 +295,20 @@ export const HoverActions: React.FC = React.memo( value={values} /> )} - {showFilters && ( - + {field != null && ( +
+ {getCopyButton({ + field, + isHoverAction: true, + keyboardEvent: stKeyboardEvent, + ownFocus, + showTooltip: true, + value: values, + })} +
)} -
-
+ + ); } ); diff --git a/x-pack/plugins/security_solution/public/common/components/hover_actions/use_hover_actions.tsx b/x-pack/plugins/security_solution/public/common/components/hover_actions/use_hover_actions.tsx new file mode 100644 index 0000000000000..373f944b70a81 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/hover_actions/use_hover_actions.tsx @@ -0,0 +1,178 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo, useState, useRef } from 'react'; +import { DraggableProvided, DraggableStateSnapshot } from 'react-beautiful-dnd'; +import { HoverActions } from '.'; + +import { DataProvider } from '../../../../common/types'; +import { ProviderContentWrapper } from '../drag_and_drop/draggable_wrapper'; +import { getDraggableId } from '../drag_and_drop/helpers'; +import { useGetTimelineId } from '../drag_and_drop/use_get_timeline_id_from_dom'; + +const draggableContainsLinks = (draggableElement: HTMLDivElement | null) => { + const links = draggableElement?.querySelectorAll('.euiLink') ?? []; + return links.length > 0; +}; + +type RenderFunctionProp = ( + props: DataProvider, + provided: DraggableProvided | null, + state: DraggableStateSnapshot +) => React.ReactNode; + +interface Props { + dataProvider: DataProvider; + disabled?: boolean; + isDraggable?: boolean; + inline?: boolean; + render: RenderFunctionProp; + timelineId?: string; + truncate?: boolean; + onFilterAdded?: () => void; +} + +export const useHoverActions = ({ + dataProvider, + isDraggable, + onFilterAdded, + render, + timelineId, +}: Props) => { + const containerRef = useRef(null); + const keyboardHandlerRef = useRef(null); + const [closePopOverTrigger, setClosePopOverTrigger] = useState(false); + const [showTopN, setShowTopN] = useState(false); + const [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState(false); + const [goGetTimelineId, setGoGetTimelineId] = useState(false); + const timelineIdFind = useGetTimelineId(containerRef, goGetTimelineId); + + const handleClosePopOverTrigger = useCallback(() => { + setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger); + setHoverActionsOwnFocus((prevHoverActionsOwnFocus) => { + if (prevHoverActionsOwnFocus) { + setTimeout(() => { + keyboardHandlerRef.current?.focus(); + }, 0); + } + return false; // always give up ownership + }); + + setTimeout(() => { + setHoverActionsOwnFocus(false); + }, 0); // invoked on the next tick, because we want to restore focus first + }, [keyboardHandlerRef]); + + const toggleTopN = useCallback(() => { + setShowTopN((prevShowTopN) => { + const newShowTopN = !prevShowTopN; + if (newShowTopN === false) { + handleClosePopOverTrigger(); + } + return newShowTopN; + }); + }, [handleClosePopOverTrigger]); + + const hoverContent = useMemo(() => { + // display links as additional content in the hover menu to enable keyboard + // navigation of links (when the draggable contains them): + const additionalContent = + hoverActionsOwnFocus && !showTopN && draggableContainsLinks(containerRef.current) ? ( + + {render(dataProvider, null, { isDragging: false, isDropAnimating: false })} + + ) : null; + + return ( + + ); + }, [ + dataProvider, + handleClosePopOverTrigger, + hoverActionsOwnFocus, + isDraggable, + onFilterAdded, + render, + showTopN, + timelineId, + timelineIdFind, + toggleTopN, + ]); + + const setContainerRef = useCallback((e: HTMLDivElement) => { + containerRef.current = e; + }, []); + + const onFocus = useCallback(() => { + if (!hoverActionsOwnFocus) { + keyboardHandlerRef.current?.focus(); + } + }, [hoverActionsOwnFocus, keyboardHandlerRef]); + + const onCloseRequested = useCallback(() => { + setShowTopN(false); + + if (hoverActionsOwnFocus) { + setHoverActionsOwnFocus(false); + + setTimeout(() => { + onFocus(); // return focus to this draggable on the next tick, because we owned focus + }, 0); + } + }, [onFocus, hoverActionsOwnFocus]); + + const openPopover = useCallback(() => { + setHoverActionsOwnFocus(true); + }, []); + + return useMemo( + () => ({ + closePopOverTrigger, + handleClosePopOverTrigger, + hoverActionsOwnFocus, + hoverContent, + keyboardHandlerRef, + onCloseRequested, + onFocus, + openPopover, + setContainerRef, + showTopN, + }), + [ + closePopOverTrigger, + handleClosePopOverTrigger, + hoverActionsOwnFocus, + hoverContent, + onCloseRequested, + onFocus, + openPopover, + setContainerRef, + showTopN, + ] + ); +}; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx index 2ecda8482e340..45883019b9ff8 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx @@ -70,67 +70,6 @@ describe('get_anomalies_host_table_columns', () => { expect(columns.some((col) => col.name === i18n.HOST_NAME)).toEqual(false); }); - test('on host page, we should escape the draggable id', () => { - const columns = getAnomaliesHostTableColumnsCurated( - HostsType.page, - startDate, - endDate, - interval, - narrowDateRange - ); - const column = columns.find((col) => col.name === i18n.SCORE) as Columns< - string, - AnomaliesByHost - >; - const anomaly: AnomaliesByHost = { - hostName: 'host.name', - anomaly: { - detectorIndex: 0, - entityName: 'entity-name-1', - entityValue: 'entity-value-1', - influencers: [], - jobId: 'job-1', - rowId: 'row-1', - severity: 100, - time: new Date('01/01/2000').valueOf(), - source: { - job_id: 'job-1', - result_type: 'result-1', - probability: 50, - multi_bucket_impact: 0, - record_score: 0, - initial_record_score: 0, - bucket_span: 0, - detector_index: 0, - is_interim: true, - timestamp: new Date('01/01/2000').valueOf(), - by_field_name: 'some field name', - by_field_value: 'some field value', - partition_field_name: 'partition field name', - partition_field_value: 'partition field value', - function: 'function-1', - function_description: 'description-1', - typical: [5, 3], - actual: [7, 4], - influencers: [], - }, - }, - }; - if (column != null && column.render != null) { - const wrapper = mount({column.render('', anomaly)}); - expect( - wrapper - .find( - '[draggableId="draggableId.content.anomalies-host-table-severity-host_name-entity-name-1-entity-value-1-100-job-1"]' - ) - .first() - .exists() - ).toBe(true); - } else { - expect(column).not.toBe(null); - } - }); - test('on host page, undefined influencers should turn into an empty column string', () => { const columns = getAnomaliesHostTableColumnsCurated( HostsType.page, diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx index 48c2ec3ee38d8..817205ce22808 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx @@ -43,62 +43,6 @@ describe('get_anomalies_network_table_columns', () => { expect(columns.some((col) => col.name === i18n.NETWORK_NAME)).toEqual(false); }); - test('on network page, we should escape the draggable id', () => { - const columns = getAnomaliesNetworkTableColumnsCurated(NetworkType.page, startDate, endDate); - const column = columns.find((col) => col.name === i18n.SCORE) as Columns< - string, - AnomaliesByNetwork - >; - const anomaly: AnomaliesByNetwork = { - type: 'source.ip', - ip: '127.0.0.1', - anomaly: { - detectorIndex: 0, - entityName: 'entity-name-1', - entityValue: 'entity-value-1', - influencers: [], - jobId: 'job-1', - rowId: 'row-1', - severity: 100, - time: new Date('01/01/2000').valueOf(), - source: { - job_id: 'job-1', - result_type: 'result-1', - probability: 50, - multi_bucket_impact: 0, - record_score: 0, - initial_record_score: 0, - bucket_span: 0, - detector_index: 0, - is_interim: true, - timestamp: new Date('01/01/2000').valueOf(), - by_field_name: 'some field name', - by_field_value: 'some field value', - partition_field_name: 'partition field name', - partition_field_value: 'partition field value', - function: 'function-1', - function_description: 'description-1', - typical: [5, 3], - actual: [7, 4], - influencers: [], - }, - }, - }; - if (column != null && column.render != null) { - const wrapper = mount({column.render('', anomaly)}); - expect( - wrapper - .find( - '[draggableId="draggableId.content.anomalies-network-table-severity-127_0_0_1-entity-name-1-entity-value-1-100-job-1"]' - ) - .first() - .exists() - ).toBe(true); - } else { - expect(column).not.toBe(null); - } - }); - test('on network page, undefined influencers should turn into an empty column string', () => { const columns = getAnomaliesNetworkTableColumnsCurated(NetworkType.page, startDate, endDate); const column = columns.find((col) => col.name === i18n.INFLUENCED_BY) as Columns< diff --git a/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx index c122138f9547a..10e4538c802ad 100644 --- a/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx @@ -55,7 +55,7 @@ describe('Table Helpers', () => { displayCount: 0, }); const wrapper = mount({rowItem}); - expect(wrapper.find('[data-test-subj="draggable-content-attrName"]').first().text()).toBe( + expect(wrapper.find('[data-test-subj="render-content-attrName"]').first().text()).toBe( '(Empty String)' ); }); @@ -81,7 +81,7 @@ describe('Table Helpers', () => { render: renderer, }); const wrapper = mount({rowItem}); - expect(wrapper.find('[data-test-subj="draggable-content-attrName"]').first().text()).toBe( + expect(wrapper.find('[data-test-subj="render-content-attrName"]').first().text()).toBe( 'Hi item1 renderer' ); }); @@ -116,7 +116,7 @@ describe('Table Helpers', () => { idPrefix: 'idPrefix', }); const wrapper = mount({rowItems}); - expect(wrapper.find('[data-test-subj="draggable-content-attrName"]').first().text()).toBe( + expect(wrapper.find('[data-test-subj="render-content-attrName"]').first().text()).toBe( '(Empty String)' ); }); @@ -163,7 +163,7 @@ describe('Table Helpers', () => { displayCount: 2, }); const wrapper = mount({rowItems}); - expect(wrapper.find('[data-test-subj="draggableWrapperDiv"]').hostNodes().length).toBe(2); + expect(wrapper.find('[data-test-subj="withHoverActionsButton"]').hostNodes().length).toBe(2); }); test('it uses custom renderer', () => { @@ -175,7 +175,7 @@ describe('Table Helpers', () => { render: renderer, }); const wrapper = mount({rowItems}); - expect(wrapper.find('[data-test-subj="draggable-content-attrName"]').first().text()).toBe( + expect(wrapper.find('[data-test-subj="render-content-attrName"]').first().text()).toBe( 'Hi item1 renderer' ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx index f643c5690e284..2286a53030784 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx @@ -26,6 +26,7 @@ import { combineQueries } from '../../../timelines/components/timeline/helpers'; import { getOptions } from './helpers'; import { TopN } from './top_n'; import { TimelineId, TimelineTabs } from '../../../../common/types/timeline'; +import { AlertsStackByField } from '../../../detections/components/alerts_kpis/common/types'; const EMPTY_FILTERS: Filter[] = []; const EMPTY_QUERY: Query = { query: '', language: 'kuery' }; @@ -153,7 +154,7 @@ const StatefulTopNComponent: React.FC = ({ data-test-subj="top-n" defaultView={defaultView} deleteQuery={timelineId === TimelineId.active ? undefined : deleteQuery} - field={field} + field={field as AlertsStackByField} filters={timelineId === TimelineId.active ? EMPTY_FILTERS : globalFilters} from={timelineId === TimelineId.active ? activeTimelineFrom : from} indexPattern={indexPattern} diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx index cee4254fd7358..8cb56d7581b36 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx @@ -35,7 +35,7 @@ jest.mock('uuid', () => { }; }); -const field = 'process.name'; +const field = 'host.name'; const value = 'nice'; const combinedQueries = { bool: { diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx index 9d38d6b4a59e3..0d4d52d338e56 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx @@ -22,6 +22,7 @@ import { TopNOption } from './helpers'; import * as i18n from './translations'; import { getIndicesSelector, IndicesSelector } from './selectors'; import { State } from '../../store'; +import { AlertsStackByField } from '../../../detections/components/alerts_kpis/common/types'; const TopNContainer = styled.div` width: 600px; @@ -50,7 +51,7 @@ const TopNContent = styled.div` export interface Props extends Pick { combinedQueries?: string; defaultView: TimelineEventsType; - field: string; + field: AlertsStackByField; filters: Filter[]; indexPattern: IIndexPattern; options: TopNOption[]; @@ -137,14 +138,11 @@ const TopNComponent: React.FC = ({ )} diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts index b9bbf7afd3626..adc06468d9a02 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts @@ -62,6 +62,7 @@ export const convertArrayToCamelCase = (arrayOfSnakes: unknown[]): unknown[] => return [...acc, value]; } }, []); + export const convertToCamelCase = (snakeCase: T): U => Object.entries(snakeCase).reduce((acc, [key, value]) => { if (isArray(value)) { @@ -73,6 +74,7 @@ export const convertToCamelCase = (snakeCase: T): U => } return acc; }, {} as U); + export const useCurrentUser = (): AuthenticatedElasticUser | null => { const isMounted = useRef(false); const [user, setUser] = useState(null); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/translations.ts deleted file mode 100644 index 5515d07fc8040..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/translations.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const STACK_BY_LABEL = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.stackByLabel', - { - defaultMessage: 'Stack by', - } -); - -export const STACK_BY_RISK_SCORES = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.riskScoresDropDown', - { - defaultMessage: 'Risk scores', - } -); - -export const STACK_BY_SEVERITIES = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.severitiesDropDown', - { - defaultMessage: 'Severities', - } -); - -export const STACK_BY_DESTINATION_IPS = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.destinationIpsDropDown', - { - defaultMessage: 'Top destination IPs', - } -); - -export const STACK_BY_SOURCE_IPS = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.sourceIpsDropDown', - { - defaultMessage: 'Top source IPs', - } -); - -export const STACK_BY_ACTIONS = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.eventActionsDropDown', - { - defaultMessage: 'Top event actions', - } -); - -export const STACK_BY_CATEGORIES = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.eventCategoriesDropDown', - { - defaultMessage: 'Top event categories', - } -); - -export const STACK_BY_HOST_NAMES = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.hostNamesDropDown', - { - defaultMessage: 'Top host names', - } -); - -export const STACK_BY_RULE_TYPES = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.ruleTypesDropDown', - { - defaultMessage: 'Top rule types', - } -); - -export const STACK_BY_RULE_NAMES = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.rulesDropDown', - { - defaultMessage: 'Top rules', - } -); - -export const STACK_BY_USERS = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.usersDropDown', - { - defaultMessage: 'Top users', - } -); - -export const TOP = (fieldName: string) => - i18n.translate('xpack.securitySolution.detectionEngine.alerts.histogram.topNLabel', { - values: { fieldName }, - defaultMessage: `Top {fieldName}`, - }); - -export const HISTOGRAM_HEADER = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.headerTitle', - { - defaultMessage: 'Trend', - } -); - -export const ALL_OTHERS = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.allOthersGroupingLabel', - { - defaultMessage: 'All others', - } -); - -export const VIEW_ALERTS = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.viewAlertsButtonLabel', - { - defaultMessage: 'View alerts', - } -); - -export const SHOWING_ALERTS = ( - totalAlertsFormatted: string, - totalAlerts: number, - modifier: string -) => - i18n.translate('xpack.securitySolution.detectionEngine.alerts.histogram.showingAlertsTitle', { - values: { totalAlertsFormatted, totalAlerts, modifier }, - defaultMessage: - 'Showing: {modifier}{totalAlertsFormatted} {totalAlerts, plural, =1 {alert} other {alerts}}', - }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.test.tsx new file mode 100644 index 0000000000000..561126f3264ad --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.test.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { shallow, mount } from 'enzyme'; + +import { AlertsCount } from './alerts_count'; +import { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types'; +import { TestProviders } from '../../../../common/mock'; +import { DragDropContextWrapper } from '../../../../common/components/drag_and_drop/drag_drop_context_wrapper'; +import { mockBrowserFields } from '../../../../common/containers/source/mock'; +import { AlertsCountAggregation } from './types'; + +jest.mock('../../../../common/lib/kibana'); +const mockDispatch = jest.fn(); + +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + +describe('AlertsCount', () => { + it('renders correctly', () => { + const wrapper = shallow( + } + loading={false} + selectedStackByOption={'test_selected_field'} + /> + ); + + expect(wrapper.find('[data-test-subj="alertsCountTable"]').exists()).toBeTruthy(); + }); + + it('renders the given alert item', () => { + const alertFiedlKey = 'test_stack_by_test_key'; + const alertFiedlCount = 999; + const alertData = { + took: 0, + timeout: false, + hits: { + hits: [], + sequences: [], + events: [], + total: { + relation: 'eq', + value: 0, + }, + }, + _shards: { + failed: 0, + skipped: 0, + successful: 1, + total: 1, + }, + aggregations: { + alertsByGroupingCount: { + buckets: [ + { + key: alertFiedlKey, + doc_count: alertFiedlCount, + }, + ], + }, + alertsByGrouping: { buckets: [] }, + }, + } as AlertSearchResponse; + + const wrapper = mount( + + + + + + ); + + expect(wrapper.text()).toContain(alertFiedlKey); + expect(wrapper.text()).toContain(alertFiedlCount); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx new file mode 100644 index 0000000000000..2c59868d8a6fe --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx @@ -0,0 +1,93 @@ +/* + * 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 { EuiProgress, EuiBasicTableColumn, EuiInMemoryTable } from '@elastic/eui'; +import React, { memo, useMemo } from 'react'; +import styled from 'styled-components'; +import numeral from '@elastic/numeral'; +import { useUiSetting$ } from '../../../../common/lib/kibana'; +import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; +import * as i18n from './translations'; +import { DefaultDraggable } from '../../../../common/components/draggables'; +import type { GenericBuckets } from '../../../../../common'; +import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types'; +import type { AlertsCountAggregation } from './types'; +import { MISSING_IP } from '../common/helpers'; + +interface AlertsCountProps { + loading: boolean; + data: AlertSearchResponse | null; + selectedStackByOption: string; +} + +const Wrapper = styled.div` + overflow: scroll; + margin-top: -8px; +`; + +const StyledSpan = styled.span` + padding-left: 8px; +`; + +const getAlertsCountTableColumns = ( + selectedStackByOption: string, + defaultNumberFormat: string +): Array> => { + return [ + { + field: 'key', + name: selectedStackByOption, + truncateText: true, + render: function DraggableStackOptionField(value: string) { + return value === i18n.ALL_OTHERS || value === MISSING_IP ? ( + {value} + ) : ( + + ); + }, + }, + { + field: 'doc_count', + name: i18n.COUNT_TABLE_COLUMN_TITLE, + sortable: true, + textOnly: true, + dataType: 'number', + render: (item: string) => numeral(item).format(defaultNumberFormat), + }, + ]; +}; + +export const AlertsCount = memo(({ loading, selectedStackByOption, data }) => { + const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const listItems: GenericBuckets[] = data?.aggregations?.alertsByGroupingCount?.buckets ?? []; + const tableColumns = useMemo( + () => getAlertsCountTableColumns(selectedStackByOption, defaultNumberFormat), + [selectedStackByOption, defaultNumberFormat] + ); + + return ( + <> + {loading && } + + + + + + ); +}); + +AlertsCount.displayName = 'AlertsCount'; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/helpers.tsx new file mode 100644 index 0000000000000..e43381ce25530 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/helpers.tsx @@ -0,0 +1,52 @@ +/* + * 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 { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../common/constants'; +import { getMissingFields } from '../common/helpers'; +import type { AlertsStackByField } from '../common/types'; + +export const getAlertsCountQuery = ( + stackByField: AlertsStackByField, + from: string, + to: string, + additionalFilters: Array<{ + bool: { filter: unknown[]; should: unknown[]; must_not: unknown[]; must: unknown[] }; + }> = [] +) => { + const missing = getMissingFields(stackByField); + + return { + size: 0, + aggs: { + alertsByGroupingCount: { + terms: { + field: stackByField, + ...missing, + order: { + _count: 'desc', + }, + size: DEFAULT_MAX_TABLE_QUERY_SIZE, + }, + }, + }, + query: { + bool: { + filter: [ + ...additionalFilters, + { + range: { + '@timestamp': { + gte: from, + lte: to, + }, + }, + }, + ], + }, + }, + }; +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx new file mode 100644 index 0000000000000..f1b7d8b06644d --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { waitFor, act } from '@testing-library/react'; + +import { mount } from 'enzyme'; +import { esQuery } from '../../../../../../../../src/plugins/data/public'; + +import { TestProviders } from '../../../../common/mock'; + +import { AlertsCountPanel } from './index'; + +describe('AlertsCountPanel', () => { + const defaultProps = { + signalIndexName: 'signalIndexName', + }; + + it('renders correctly', async () => { + await act(async () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="alertsCountPanel"]').exists()).toBeTruthy(); + }); + }); + + describe('Query', () => { + it('it render with a illegal KQL', async () => { + const spyOnBuildEsQuery = jest.spyOn(esQuery, 'buildEsQuery'); + spyOnBuildEsQuery.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + const props = { ...defaultProps, query: { query: 'host.name: "', language: 'kql' } }; + const wrapper = mount( + + + + ); + + await waitFor(() => { + expect(wrapper.find('[data-test-subj="alertsCountPanel"]').exists()).toBeTruthy(); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx new file mode 100644 index 0000000000000..001567d7d2cc8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo, useState, useEffect } from 'react'; +import uuid from 'uuid'; + +import { useGlobalTime } from '../../../../common/containers/use_global_time'; +import { HeaderSection } from '../../../../common/components/header_section'; + +import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query'; +import { InspectButtonContainer } from '../../../../common/components/inspect'; + +import { getAlertsCountQuery } from './helpers'; +import * as i18n from './translations'; +import { AlertsCount } from './alerts_count'; +import type { AlertsCountAggregation } from './types'; +import { DEFAULT_STACK_BY_FIELD } from '../common/config'; +import type { AlertsStackByField } from '../common/types'; +import { Filter, esQuery, Query } from '../../../../../../../../src/plugins/data/public'; +import { KpiPanel, StackBySelect } from '../common/components'; +import { useInspectButton } from '../common/hooks'; + +export const DETECTIONS_ALERTS_COUNT_ID = 'detections-alerts-count'; + +interface AlertsCountPanelProps { + filters?: Filter[]; + query?: Query; + signalIndexName: string | null; +} + +export const AlertsCountPanel = memo( + ({ filters, query, signalIndexName }) => { + const { to, from, deleteQuery, setQuery } = useGlobalTime(); + + // create a unique, but stable (across re-renders) query id + const uniqueQueryId = useMemo(() => `${DETECTIONS_ALERTS_COUNT_ID}-${uuid.v4()}`, []); + const [selectedStackByOption, setSelectedStackByOption] = useState( + DEFAULT_STACK_BY_FIELD + ); + + const additionalFilters = useMemo(() => { + try { + return [ + esQuery.buildEsQuery( + undefined, + query != null ? [query] : [], + filters?.filter((f) => f.meta.disabled === false) ?? [] + ), + ]; + } catch (e) { + return []; + } + }, [query, filters]); + + const { + loading: isLoadingAlerts, + data: alertsData, + setQuery: setAlertsQuery, + response, + request, + refetch, + } = useQueryAlerts<{}, AlertsCountAggregation>({ + query: getAlertsCountQuery(selectedStackByOption, from, to, additionalFilters), + indexName: signalIndexName, + }); + + useEffect(() => { + setAlertsQuery(getAlertsCountQuery(selectedStackByOption, from, to, additionalFilters)); + }, [setAlertsQuery, selectedStackByOption, from, to, additionalFilters]); + + useInspectButton({ + setQuery, + response, + request, + refetch, + uniqueQueryId, + deleteQuery, + loading: isLoadingAlerts, + }); + + return ( + + + + + + + + + ); + } +); + +AlertsCountPanel.displayName = 'AlertsCountPanel'; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/translations.ts new file mode 100644 index 0000000000000..6f2e428b6b519 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/translations.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const COUNT_TABLE_COLUMN_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.count.countTableColumnTitle', + { + defaultMessage: 'Count', + } +); + +export const COUNT_TABLE_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.count.countTableTitle', + { + defaultMessage: 'Count', + } +); + +export * from '../common/translations'; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/types.ts new file mode 100644 index 0000000000000..06cdee581d3fd --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/types.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { GenericBuckets } from '../../../../../common'; + +export interface AlertsCountAggregation { + alertsByGroupingCount: { + buckets: GenericBuckets[]; + }; +} diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/alerts_histogram.test.tsx similarity index 82% rename from x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/alerts_histogram.test.tsx index 440d942bc117c..11ab2c49a5dc0 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/alerts_histogram.test.tsx @@ -8,10 +8,10 @@ import React from 'react'; import { shallow } from 'enzyme'; -import '../../../common/mock/match_media'; +import '../../../../common/mock/match_media'; import { AlertsHistogram } from './alerts_histogram'; -jest.mock('../../../common/lib/kibana'); +jest.mock('../../../../common/lib/kibana'); describe('AlertsHistogram', () => { it('renders correctly', () => { @@ -26,6 +26,6 @@ describe('AlertsHistogram', () => { /> ); - expect(wrapper.find('Chart')).toBeTruthy(); + expect(wrapper.find('Chart').exists()).toBeTruthy(); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/alerts_histogram.tsx similarity index 87% rename from x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/alerts_histogram.tsx index ab5ad0557cc99..09d8d52271674 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/alerts_histogram.tsx @@ -16,12 +16,12 @@ import { import { EuiFlexGroup, EuiFlexItem, EuiProgress } from '@elastic/eui'; import React, { useMemo } from 'react'; -import { useTheme, UpdateDateRange } from '../../../common/components/charts/common'; -import { histogramDateTimeFormatter } from '../../../common/components/utils'; -import { DraggableLegend } from '../../../common/components/charts/draggable_legend'; -import { LegendItem } from '../../../common/components/charts/draggable_legend_item'; +import { useTheme, UpdateDateRange } from '../../../../common/components/charts/common'; +import { histogramDateTimeFormatter } from '../../../../common/components/utils'; +import { DraggableLegend } from '../../../../common/components/charts/draggable_legend'; +import { LegendItem } from '../../../../common/components/charts/draggable_legend_item'; -import { HistogramData } from './types'; +import type { HistogramData } from './types'; const DEFAULT_CHART_HEIGHT = 174; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/helpers.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/helpers.test.tsx diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/helpers.tsx similarity index 77% rename from x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/helpers.tsx index c6cf896937f48..298158440224f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/helpers.tsx @@ -7,10 +7,11 @@ import moment from 'moment'; -import { showAllOthersBucket } from '../../../../common/constants'; -import { HistogramData, AlertsAggregation, AlertsBucket, AlertsGroupBucket } from './types'; -import { AlertSearchResponse } from '../../containers/detection_engine/alerts/types'; -import * as i18n from './translations'; +import { isEmpty } from 'lodash/fp'; +import type { HistogramData, AlertsAggregation, AlertsBucket, AlertsGroupBucket } from './types'; +import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types'; +import { getMissingFields } from '../common/helpers'; +import type { AlertsStackByField } from '../common/types'; const EMPTY_ALERTS_DATA: HistogramData[] = []; @@ -33,18 +34,14 @@ export const formatAlertsData = (alertsData: AlertSearchResponse<{}, AlertsAggre }; export const getAlertsHistogramQuery = ( - stackByField: string, + stackByField: AlertsStackByField, from: string, to: string, additionalFilters: Array<{ bool: { filter: unknown[]; should: unknown[]; must_not: unknown[]; must: unknown[] }; }> ) => { - const missing = showAllOthersBucket.includes(stackByField) - ? { - missing: stackByField.endsWith('.ip') ? '0.0.0.0' : i18n.ALL_OTHERS, - } - : {}; + const missing = getMissingFields(stackByField); return { aggs: { @@ -103,3 +100,19 @@ export const showInitialLoadingSpinner = ({ isInitialLoading: boolean; isLoadingAlerts: boolean; }): boolean => isInitialLoading && isLoadingAlerts; + +export const parseCombinedQueries = (query?: string) => { + try { + return query != null && !isEmpty(query) ? JSON.parse(query) : {}; + } catch { + return {}; + } +}; + +export const buildCombinedQueries = (query?: string) => { + try { + return isEmpty(query) ? [] : [parseCombinedQueries(query)]; + } catch { + return []; + } +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx similarity index 65% rename from x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx index 70101021bc4f0..0d6793eb2b886 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx @@ -6,15 +6,14 @@ */ import React from 'react'; -import { waitFor } from '@testing-library/react'; -import { shallow, mount } from 'enzyme'; +import { waitFor, act } from '@testing-library/react'; +import { mount } from 'enzyme'; -import '../../../common/mock/match_media'; -import { esQuery } from '../../../../../../../src/plugins/data/public'; -import { TestProviders } from '../../../common/mock'; -import { SecurityPageName } from '../../../app/types'; +import { esQuery } from '../../../../../../../../src/plugins/data/public'; +import { TestProviders } from '../../../../common/mock'; +import { SecurityPageName } from '../../../../app/types'; -import { AlertsHistogramPanel, buildCombinedQueries, parseCombinedQueries } from './index'; +import { AlertsHistogramPanel } from './index'; import * as helpers from './helpers'; jest.mock('react-router-dom', () => { @@ -27,8 +26,8 @@ jest.mock('react-router-dom', () => { }); const mockNavigateToApp = jest.fn(); -jest.mock('../../../common/lib/kibana/kibana_react', () => { - const original = jest.requireActual('../../../common/lib/kibana/kibana_react'); +jest.mock('../../../../common/lib/kibana/kibana_react', () => { + const original = jest.requireActual('../../../../common/lib/kibana/kibana_react'); return { ...original, @@ -46,8 +45,8 @@ jest.mock('../../../common/lib/kibana/kibana_react', () => { }; }); -jest.mock('../../../common/lib/kibana', () => { - const original = jest.requireActual('../../../common/lib/kibana'); +jest.mock('../../../../common/lib/kibana', () => { + const original = jest.requireActual('../../../../common/lib/kibana'); return { ...original, useUiSetting$: jest.fn().mockReturnValue([]), @@ -55,39 +54,65 @@ jest.mock('../../../common/lib/kibana', () => { }; }); -jest.mock('../../../common/components/navigation/use_get_url_search'); +jest.mock('../../../../common/components/navigation/use_get_url_search'); + +jest.mock('../../../containers/detection_engine/alerts/use_query', () => { + const original = jest.requireActual('../../../containers/detection_engine/alerts/use_query'); + return { + ...original, + useQueryAlerts: jest.fn().mockReturnValue({ + loading: true, + setQuery: () => undefined, + data: null, + response: '', + request: '', + refetch: null, + }), + }; +}); describe('AlertsHistogramPanel', () => { const defaultProps = { - from: '2020-07-07T08:20:18.966Z', signalIndexName: 'signalIndexName', setQuery: jest.fn(), - to: '2020-07-08T08:20:18.966Z', updateDateRange: jest.fn(), }; it('renders correctly', () => { - const wrapper = shallow(); - - expect(wrapper.find('[id="detections-histogram"]')).toBeTruthy(); + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="alerts-histogram-panel"]').exists()).toBeTruthy(); + wrapper.unmount(); }); describe('Button view alerts', () => { it('renders correctly', () => { const props = { ...defaultProps, showLinkToAlerts: true }; - const wrapper = shallow(); + const wrapper = mount( + + + + ); expect( - wrapper.find('[data-test-subj="alerts-histogram-panel-go-to-alerts-page"]') + wrapper.find('[data-test-subj="alerts-histogram-panel-go-to-alerts-page"]').exists() ).toBeTruthy(); + wrapper.unmount(); }); it('when click we call navigateToApp to make sure to navigate to right page', () => { const props = { ...defaultProps, showLinkToAlerts: true }; - const wrapper = shallow(); + const wrapper = mount( + + + + ); wrapper - .find('[data-test-subj="alerts-histogram-panel-go-to-alerts-page"]') + .find('button[data-test-subj="alerts-histogram-panel-go-to-alerts-page"]') .simulate('click', { preventDefault: jest.fn(), }); @@ -96,36 +121,36 @@ describe('AlertsHistogramPanel', () => { deepLinkId: SecurityPageName.alerts, path: '', }); + wrapper.unmount(); }); }); describe('Query', () => { it('it render with a illegal KQL', async () => { - const spyOnBuildEsQuery = jest.spyOn(esQuery, 'buildEsQuery'); - spyOnBuildEsQuery.mockImplementation(() => { - throw new Error('Something went wrong'); - }); - const props = { ...defaultProps, query: { query: 'host.name: "', language: 'kql' } }; - const wrapper = mount( - - - - ); + await act(async () => { + const spyOnBuildEsQuery = jest.spyOn(esQuery, 'buildEsQuery'); + spyOnBuildEsQuery.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + const props = { ...defaultProps, query: { query: 'host.name: "', language: 'kql' } }; + const wrapper = mount( + + + + ); - await waitFor(() => { - expect(wrapper.find('[id="detections-histogram"]')).toBeTruthy(); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="alerts-histogram-panel"]').exists()).toBeTruthy(); + }); + wrapper.unmount(); }); }); }); describe('CombinedQueries', () => { - jest.mock('./helpers'); - const mockGetAlertsHistogramQuery = jest.spyOn(helpers, 'getAlertsHistogramQuery'); - beforeEach(() => { - mockGetAlertsHistogramQuery.mockReset(); - }); - it('combinedQueries props is valid, alerts query include combinedQueries', async () => { + const mockGetAlertsHistogramQuery = jest.spyOn(helpers, 'getAlertsHistogramQuery'); + const props = { ...defaultProps, query: { query: 'host.name: "', language: 'kql' }, @@ -137,6 +162,7 @@ describe('AlertsHistogramPanel', () => { ); + await waitFor(() => { expect(mockGetAlertsHistogramQuery.mock.calls[0]).toEqual([ 'signal.rule.name', @@ -159,20 +185,20 @@ describe('AlertsHistogramPanel', () => { describe('parseCombinedQueries', () => { it('return empty object when variables is undefined', async () => { - expect(parseCombinedQueries(undefined)).toEqual({}); + expect(helpers.parseCombinedQueries(undefined)).toEqual({}); }); it('return empty object when variables is empty string', async () => { - expect(parseCombinedQueries('')).toEqual({}); + expect(helpers.parseCombinedQueries('')).toEqual({}); }); it('return empty object when variables is NOT a valid stringify json object', async () => { - expect(parseCombinedQueries('hello world')).toEqual({}); + expect(helpers.parseCombinedQueries('hello world')).toEqual({}); }); it('return a valid json object when variables is a valid json stringify', async () => { expect( - parseCombinedQueries( + helpers.parseCombinedQueries( '{"bool":{"must":[],"filter":[{"match_all":{}},{"exists":{"field":"process.name"}}],"should":[],"must_not":[]}}' ) ).toMatchInlineSnapshot(` @@ -199,20 +225,20 @@ describe('AlertsHistogramPanel', () => { describe('buildCombinedQueries', () => { it('return empty array when variables is undefined', async () => { - expect(buildCombinedQueries(undefined)).toEqual([]); + expect(helpers.buildCombinedQueries(undefined)).toEqual([]); }); it('return empty array when variables is empty string', async () => { - expect(buildCombinedQueries('')).toEqual([]); + expect(helpers.buildCombinedQueries('')).toEqual([]); }); it('return array with empty object when variables is NOT a valid stringify json object', async () => { - expect(buildCombinedQueries('hello world')).toEqual([{}]); + expect(helpers.buildCombinedQueries('hello world')).toEqual([{}]); }); it('return a valid json object when variables is a valid json stringify', async () => { expect( - buildCombinedQueries( + helpers.buildCombinedQueries( '{"bool":{"must":[],"filter":[{"match_all":{}},{"exists":{"field":"process.name"}}],"should":[],"must_not":[]}}' ) ).toMatchInlineSnapshot(` diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx similarity index 60% rename from x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx index 482032e6b4cbf..2182ed7da0c4f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx @@ -5,44 +5,44 @@ * 2.0. */ -import { Position } from '@elastic/charts'; -import { EuiFlexGroup, EuiFlexItem, EuiSelect, EuiPanel } from '@elastic/eui'; +import type { Position } from '@elastic/charts'; +import { EuiFlexGroup, EuiFlexItem, EuiTitleSize } from '@elastic/eui'; import numeral from '@elastic/numeral'; import React, { memo, useCallback, useMemo, useState, useEffect } from 'react'; import styled from 'styled-components'; import { isEmpty } from 'lodash/fp'; import uuid from 'uuid'; -import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; -import { DEFAULT_NUMBER_FORMAT, APP_ID } from '../../../../common/constants'; -import { UpdateDateRange } from '../../../common/components/charts/common'; -import { LegendItem } from '../../../common/components/charts/draggable_legend_item'; -import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers'; -import { HeaderSection } from '../../../common/components/header_section'; -import { Filter, esQuery, Query } from '../../../../../../../src/plugins/data/public'; -import { useQueryAlerts } from '../../containers/detection_engine/alerts/use_query'; -import { getDetectionEngineUrl, useFormatUrl } from '../../../common/components/link_to'; -import { defaultLegendColors } from '../../../common/components/matrix_histogram/utils'; -import { InspectButtonContainer } from '../../../common/components/inspect'; -import { MatrixLoader } from '../../../common/components/matrix_histogram/matrix_loader'; -import { MatrixHistogramOption } from '../../../common/components/matrix_histogram/types'; -import { useKibana, useUiSetting$ } from '../../../common/lib/kibana'; -import { alertsHistogramOptions } from './config'; -import { formatAlertsData, getAlertsHistogramQuery, showInitialLoadingSpinner } from './helpers'; +import { useGlobalTime } from '../../../../common/containers/use_global_time'; +import { DEFAULT_NUMBER_FORMAT, APP_ID } from '../../../../../common/constants'; +import type { UpdateDateRange } from '../../../../common/components/charts/common'; +import type { LegendItem } from '../../../../common/components/charts/draggable_legend_item'; +import { escapeDataProviderId } from '../../../../common/components/drag_and_drop/helpers'; +import { HeaderSection } from '../../../../common/components/header_section'; +import { Filter, esQuery, Query } from '../../../../../../../../src/plugins/data/public'; +import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query'; +import { getDetectionEngineUrl, useFormatUrl } from '../../../../common/components/link_to'; +import { defaultLegendColors } from '../../../../common/components/matrix_histogram/utils'; +import { InspectButtonContainer } from '../../../../common/components/inspect'; +import { MatrixLoader } from '../../../../common/components/matrix_histogram/matrix_loader'; +import { useKibana, useUiSetting$ } from '../../../../common/lib/kibana'; +import { + parseCombinedQueries, + buildCombinedQueries, + formatAlertsData, + getAlertsHistogramQuery, + showInitialLoadingSpinner, +} from './helpers'; import { AlertsHistogram } from './alerts_histogram'; import * as i18n from './translations'; -import { AlertsHistogramOption, AlertsAggregation, AlertsTotal } from './types'; -import { LinkButton } from '../../../common/components/links'; -import { SecurityPageName } from '../../../app/types'; +import type { AlertsAggregation, AlertsTotal } from './types'; +import { LinkButton } from '../../../../common/components/links'; +import { SecurityPageName } from '../../../../app/types'; +import { DEFAULT_STACK_BY_FIELD, PANEL_HEIGHT } from '../common/config'; +import type { AlertsStackByField } from '../common/types'; +import { KpiPanel, StackBySelect } from '../common/components'; -const DEFAULT_PANEL_HEIGHT = 300; - -const StyledEuiPanel = styled(EuiPanel)<{ height?: number }>` - display: flex; - flex-direction: column; - ${({ height }) => (height != null ? `height: ${height}px;` : '')} - position: relative; -`; +import { useInspectButton } from '../common/hooks'; const defaultTotalAlertsObj: AlertsTotal = { value: 0, @@ -52,89 +52,62 @@ const defaultTotalAlertsObj: AlertsTotal = { export const DETECTIONS_HISTOGRAM_ID = 'detections-histogram'; const ViewAlertsFlexItem = styled(EuiFlexItem)` - margin-left: 24px; + margin-left: ${({ theme }) => theme.eui.euiSizeL}; `; -interface AlertsHistogramPanelProps - extends Pick { +interface AlertsHistogramPanelProps { chartHeight?: number; combinedQueries?: string; - defaultStackByOption?: AlertsHistogramOption; + defaultStackByOption?: AlertsStackByField; filters?: Filter[]; headerChildren?: React.ReactNode; /** Override all defaults, and only display this field */ - onlyField?: string; + onlyField?: AlertsStackByField; + titleSize?: EuiTitleSize; query?: Query; legendPosition?: Position; - panelHeight?: number; signalIndexName: string | null; showLinkToAlerts?: boolean; showTotalAlertsCount?: boolean; - stackByOptions?: AlertsHistogramOption[]; + showStackBy?: boolean; timelineId?: string; title?: string; updateDateRange: UpdateDateRange; } -const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ - text: fieldName, - value: fieldName, -}); - const NO_LEGEND_DATA: LegendItem[] = []; -const DEFAULT_STACK_BY = 'signal.rule.name'; -const getDefaultStackByOption = (): AlertsHistogramOption => - alertsHistogramOptions.find(({ text }) => text === DEFAULT_STACK_BY) ?? alertsHistogramOptions[0]; - -export const parseCombinedQueries = (query?: string) => { - try { - return query != null && !isEmpty(query) ? JSON.parse(query) : {}; - } catch { - return {}; - } -}; - -export const buildCombinedQueries = (query?: string) => { - try { - return isEmpty(query) ? [] : [parseCombinedQueries(query)]; - } catch { - return []; - } -}; - export const AlertsHistogramPanel = memo( ({ chartHeight, combinedQueries, - defaultStackByOption = getDefaultStackByOption(), - deleteQuery, + defaultStackByOption = DEFAULT_STACK_BY_FIELD, filters, headerChildren, onlyField, query, - from, legendPosition = 'right', - panelHeight = DEFAULT_PANEL_HEIGHT, - setQuery, signalIndexName, showLinkToAlerts = false, showTotalAlertsCount = false, - stackByOptions, + showStackBy = true, timelineId, title = i18n.HISTOGRAM_HEADER, - to, updateDateRange, + titleSize = 'm', }) => { + const { to, from, deleteQuery, setQuery } = useGlobalTime(); + // create a unique, but stable (across re-renders) query id const uniqueQueryId = useMemo(() => `${DETECTIONS_HISTOGRAM_ID}-${uuid.v4()}`, []); const [isInitialLoading, setIsInitialLoading] = useState(true); const [isInspectDisabled, setIsInspectDisabled] = useState(false); const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); const [totalAlertsObj, setTotalAlertsObj] = useState(defaultTotalAlertsObj); - const [selectedStackByOption, setSelectedStackByOption] = useState( - onlyField == null ? defaultStackByOption : getHistogramOption(onlyField) + const [selectedStackByOption, setSelectedStackByOption] = useState( + onlyField == null ? defaultStackByOption : onlyField ); + const { loading: isLoadingAlerts, data: alertsData, @@ -144,13 +117,14 @@ export const AlertsHistogramPanel = memo( refetch, } = useQueryAlerts<{}, AlertsAggregation>({ query: getAlertsHistogramQuery( - selectedStackByOption.value, + selectedStackByOption, from, to, buildCombinedQueries(combinedQueries) ), indexName: signalIndexName, }); + const kibana = useKibana(); const { navigateToApp } = kibana.services.application; const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.alerts); @@ -166,13 +140,6 @@ export const AlertsHistogramPanel = memo( [totalAlertsObj] ); - const setSelectedOptionCallback = useCallback((event: React.ChangeEvent) => { - setSelectedStackByOption( - stackByOptions?.find((co) => co.value === event.target.value) ?? defaultStackByOption - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - const goToDetectionEngine = useCallback( (ev) => { ev.preventDefault(); @@ -191,14 +158,14 @@ export const AlertsHistogramPanel = memo( ? alertsData.aggregations.alertsByGrouping.buckets.map((bucket, i) => ({ color: i < defaultLegendColors.length ? defaultLegendColors[i] : undefined, dataProviderId: escapeDataProviderId( - `draggable-legend-item-${uuid.v4()}-${selectedStackByOption.value}-${bucket.key}` + `draggable-legend-item-${uuid.v4()}-${selectedStackByOption}-${bucket.key}` ), - field: selectedStackByOption.value, + field: selectedStackByOption, timelineId, value: bucket.key, })) : NO_LEGEND_DATA, - [alertsData, selectedStackByOption.value, timelineId] + [alertsData, selectedStackByOption, timelineId] ); useEffect(() => { @@ -213,29 +180,15 @@ export const AlertsHistogramPanel = memo( }; }, [isInitialLoading, isLoadingAlerts, setIsInitialLoading]); - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: uniqueQueryId }); - } - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (refetch != null && setQuery != null) { - setQuery({ - id: uniqueQueryId, - inspect: { - dsl: [request], - response: [response], - }, - loading: isLoadingAlerts, - refetch, - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [setQuery, isLoadingAlerts, alertsData, response, request, refetch]); + useInspectButton({ + setQuery, + response, + request, + refetch, + uniqueQueryId, + deleteQuery, + loading: isLoadingAlerts, + }); useEffect(() => { setTotalAlertsObj( @@ -265,7 +218,7 @@ export const AlertsHistogramPanel = memo( setIsInspectDisabled(false); setAlertsQuery( getAlertsHistogramQuery( - selectedStackByOption.value, + selectedStackByOption, from, to, !isEmpty(converted) ? [converted] : [] @@ -273,10 +226,10 @@ export const AlertsHistogramPanel = memo( ); } catch (e) { setIsInspectDisabled(true); - setAlertsQuery(getAlertsHistogramQuery(selectedStackByOption.value, from, to, [])); + setAlertsQuery(getAlertsHistogramQuery(selectedStackByOption, from, to, [])); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedStackByOption.value, from, to, query, filters, combinedQueries]); + }, [selectedStackByOption, from, to, query, filters, combinedQueries]); const linkButton = useMemo(() => { if (showLinkToAlerts) { @@ -301,22 +254,21 @@ export const AlertsHistogramPanel = memo( return ( - + - {stackByOptions && ( - )} {headerChildren != null && headerChildren} @@ -339,7 +291,7 @@ export const AlertsHistogramPanel = memo( updateDateRange={updateDateRange} /> )} - + ); } diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/translations.ts new file mode 100644 index 0000000000000..67150926621ab --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/translations.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const TOP = (fieldName: string) => + i18n.translate('xpack.securitySolution.detectionEngine.alerts.histogram.topNLabel', { + values: { fieldName }, + defaultMessage: `Top {fieldName}`, + }); + +export const HISTOGRAM_HEADER = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.headerTitle', + { + defaultMessage: 'Trend', + } +); + +export const VIEW_ALERTS = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.viewAlertsButtonLabel', + { + defaultMessage: 'View alerts', + } +); + +export const SHOWING_ALERTS = ( + totalAlertsFormatted: string, + totalAlerts: number, + modifier: string +) => + i18n.translate('xpack.securitySolution.detectionEngine.alerts.histogram.showingAlertsTitle', { + values: { totalAlertsFormatted, totalAlerts, modifier }, + defaultMessage: + 'Showing: {modifier}{totalAlertsFormatted} {totalAlerts, plural, =1 {alert} other {alerts}}', + }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/types.ts similarity index 86% rename from x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/types.ts rename to x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/types.ts index de4a770a439a5..8c2a53dc23d43 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/types.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/types.ts @@ -5,12 +5,7 @@ * 2.0. */ -import { inputsModel } from '../../../common/store'; - -export interface AlertsHistogramOption { - text: string; - value: string; -} +import type { inputsModel } from '../../../../common/store'; export interface HistogramData { x: number; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/components.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/components.tsx new file mode 100644 index 0000000000000..53d41835d6bb9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/components.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiPanel, EuiSelect } from '@elastic/eui'; +import styled from 'styled-components'; +import React, { useCallback } from 'react'; +import { PANEL_HEIGHT, MOBILE_PANEL_HEIGHT, alertsStackByOptions } from './config'; +import type { AlertsStackByField } from './types'; +import * as i18n from './translations'; + +export const KpiPanel = styled(EuiPanel)<{ height?: number }>` + display: flex; + flex-direction: column; + position: relative; + overflow: hidden; + + height: ${MOBILE_PANEL_HEIGHT}px; + + @media only screen and (min-width: ${(props) => props.theme.eui.euiBreakpoints.m}) { + height: ${PANEL_HEIGHT}px; + } +`; +interface StackedBySelectProps { + selected: AlertsStackByField; + onSelect: (selected: AlertsStackByField) => void; +} + +export const StackBySelect: React.FC = ({ selected, onSelect }) => { + const setSelectedOptionCallback = useCallback( + (event: React.ChangeEvent) => { + onSelect(event.target.value as AlertsStackByField); + }, + [onSelect] + ); + + return ( + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/config.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/config.ts similarity index 76% rename from x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/config.ts rename to x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/config.ts index 0b961cd8c7a13..cb5a23e711974 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/config.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/config.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { AlertsHistogramOption } from './types'; +import type { AlertsStackByOption } from './types'; -export const alertsHistogramOptions: AlertsHistogramOption[] = [ +export const alertsStackByOptions: AlertsStackByOption[] = [ { text: 'signal.rule.risk_score', value: 'signal.rule.risk_score' }, { text: 'signal.rule.severity', value: 'signal.rule.severity' }, { text: 'signal.rule.threat.tactic.name', value: 'signal.rule.threat.tactic.name' }, @@ -20,3 +20,9 @@ export const alertsHistogramOptions: AlertsHistogramOption[] = [ { text: 'source.ip', value: 'source.ip' }, { text: 'user.name', value: 'user.name' }, ]; + +export const DEFAULT_STACK_BY_FIELD = 'signal.rule.name'; + +export const PANEL_HEIGHT = 300; +export const MOBILE_PANEL_HEIGHT = 500; +export const CHART_HEIGHT = 200; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/helpers.ts new file mode 100644 index 0000000000000..ecc7cc0197778 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/helpers.ts @@ -0,0 +1,19 @@ +/* + * 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 { showAllOthersBucket } from '../../../../../common/constants'; +import type { AlertsStackByField } from './types'; +import * as i18n from './translations'; + +export const MISSING_IP = '0.0.0.0'; + +export const getMissingFields = (stackByField: AlertsStackByField) => + showAllOthersBucket.includes(stackByField) + ? { + missing: stackByField.endsWith('.ip') ? MISSING_IP : i18n.ALL_OTHERS, + } + : {}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.test.tsx new file mode 100644 index 0000000000000..ad0fc1fa7ac61 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.test.tsx @@ -0,0 +1,46 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { useInspectButton, UseInspectButtonParams } from './hooks'; + +describe('hooks', () => { + describe('useInspectButton', () => { + const defaultParams: UseInspectButtonParams = { + setQuery: jest.fn(), + response: '', + request: '', + refetch: jest.fn(), + uniqueQueryId: 'test-uniqueQueryId', + deleteQuery: jest.fn(), + loading: false, + }; + + it('calls setQuery when rendering', () => { + const mockSetQuery = jest.fn(); + + renderHook(() => useInspectButton({ ...defaultParams, setQuery: mockSetQuery })); + + expect(mockSetQuery).toHaveBeenCalledWith( + expect.objectContaining({ + id: defaultParams.uniqueQueryId, + }) + ); + }); + + it('calls deleteQuery when unmounting', () => { + const mockDeleteQuery = jest.fn(); + + const result = renderHook(() => + useInspectButton({ ...defaultParams, deleteQuery: mockDeleteQuery }) + ); + result.unmount(); + + expect(mockDeleteQuery).toHaveBeenCalledWith({ id: defaultParams.uniqueQueryId }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.ts new file mode 100644 index 0000000000000..6375e2b0c27fb --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.ts @@ -0,0 +1,50 @@ +/* + * 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 { useEffect } from 'react'; +import type { GlobalTimeArgs } from '../../../../common/containers/use_global_time'; + +export interface UseInspectButtonParams extends Pick { + response: string; + request: string; + refetch: (() => void) | null; + uniqueQueryId: string; + loading: boolean; +} +/** + * * Add query to inspect button utility. + * * Delete query from inspect button utility when component unmounts + */ +export const useInspectButton = ({ + setQuery, + response, + request, + refetch, + uniqueQueryId, + deleteQuery, + loading, +}: UseInspectButtonParams) => { + useEffect(() => { + if (refetch != null && setQuery != null) { + setQuery({ + id: uniqueQueryId, + inspect: { + dsl: [request], + response: [response], + }, + loading, + refetch, + }); + } + + return () => { + if (deleteQuery) { + deleteQuery({ id: uniqueQueryId }); + } + }; + }, [setQuery, loading, response, request, refetch, uniqueQueryId, deleteQuery]); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/translations.ts new file mode 100644 index 0000000000000..ef540e088877c --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const STACK_BY_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.stackByLabel', + { + defaultMessage: 'Stack by', + } +); + +export const ALL_OTHERS = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.allOthersGroupingLabel', + { + defaultMessage: 'All others', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/types.ts new file mode 100644 index 0000000000000..833c05bfc7a79 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/types.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface AlertsStackByOption { + text: AlertsStackByField; + value: AlertsStackByField; +} + +export type AlertsStackByField = + | 'signal.rule.risk_score' + | 'signal.rule.severity' + | 'signal.rule.threat.tactic.name' + | 'destination.ip' + | 'event.action' + | 'event.category' + | 'host.name' + | 'signal.rule.type' + | 'signal.rule.name' + | 'source.ip' + | 'user.name'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx index 2206960f6bcd3..55154def55b50 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx @@ -21,7 +21,7 @@ import { ActionVariables, } from '../../../../../../triggers_actions_ui/public'; import { AlertAction } from '../../../../../../alerting/common'; -import { useKibana } from '../../../../common/lib/kibana'; +import { convertArrayToCamelCase, useKibana } from '../../../../common/lib/kibana'; import { FORM_ERRORS_TITLE } from './translations'; interface Props { @@ -137,7 +137,7 @@ export const RuleActionsField: React.FC = ({ useEffect(() => { (async function () { - const actionTypes = await loadActionTypes({ http }); + const actionTypes = convertArrayToCamelCase(await loadActionTypes({ http })) as ActionType[]; const supportedTypes = getSupportedActions(actionTypes, hasErrorOnCreationCaseAction); setSupportedActionTypes(supportedTypes); })(); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx index c19e5c26bdc94..6fd90adfc0e06 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx @@ -6,11 +6,11 @@ */ import { useEffect, useRef, useState } from 'react'; -import { ACTION_URL } from '../../../../../../cases/common'; -import { KibanaServices } from '../../../../common/lib/kibana'; +import { getAllConnectorsUrl, getCreateConnectorUrl } from '../../../../../../cases/common'; +import { convertArrayToCamelCase, KibanaServices } from '../../../../common/lib/kibana'; interface CaseAction { - actionTypeId: string; + connectorTypeId: string; id: string; isPreconfigured: boolean; name: string; @@ -28,15 +28,18 @@ export const useManageCaseAction = () => { const abortCtrl = new AbortController(); const fetchActions = async () => { try { - const actions = await KibanaServices.get().http.fetch(ACTION_URL, { - method: 'GET', - signal: abortCtrl.signal, - }); - if (!actions.some((a) => a.actionTypeId === '.case' && a.name === CASE_ACTION_NAME)) { - await KibanaServices.get().http.post(`${ACTION_URL}/action`, { + const actions = convertArrayToCamelCase( + await KibanaServices.get().http.fetch(getAllConnectorsUrl(), { + method: 'GET', + signal: abortCtrl.signal, + }) + ) as CaseAction[]; + + if (!actions.some((a) => a.connectorTypeId === '.case' && a.name === CASE_ACTION_NAME)) { + await KibanaServices.get().http.post(getCreateConnectorUrl(), { method: 'POST', body: JSON.stringify({ - actionTypeId: '.case', + connector_type_id: '.case', config: {}, name: CASE_ACTION_NAME, secrets: {}, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index f52b09e2d62b4..035784b2e27a4 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiSpacer, EuiWindowEvent } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiWindowEvent } from '@elastic/eui'; import styled from 'styled-components'; import { noop } from 'lodash/fp'; import React, { useCallback, useMemo, useRef, useState } from 'react'; @@ -26,8 +26,7 @@ import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions import { useAlertInfo } from '../../components/alerts_info'; import { AlertsTable } from '../../components/alerts_table'; import { NoApiIntegrationKeyCallOut } from '../../components/callouts/no_api_integration_callout'; -import { AlertsHistogramPanel } from '../../components/alerts_histogram_panel'; -import { alertsHistogramOptions } from '../../components/alerts_histogram_panel/config'; +import { AlertsHistogramPanel } from '../../components/alerts_kpis/alerts_histogram_panel'; import { useUserData } from '../../components/user_info'; import { OverviewEmpty } from '../../../overview/components/overview_empty'; import { DetectionEngineNoIndex } from './detection_engine_no_index'; @@ -57,6 +56,8 @@ import { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { NeedAdminForUpdateRulesCallOut } from '../../components/callouts/need_admin_for_update_callout'; import { MissingPrivilegesCallOut } from '../../components/callouts/missing_privileges_callout'; import { useKibana } from '../../../common/lib/kibana'; +import { AlertsCountPanel } from '../../components/alerts_kpis/alerts_count_panel'; +import { CHART_HEIGHT } from '../../components/alerts_kpis/common/config'; /** * Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space. @@ -84,7 +85,7 @@ const DetectionEnginePageComponent = () => { // TODO: Once we are past experimental phase this code should be removed const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled'); - const { to, from, deleteQuery, setQuery } = useGlobalTime(); + const { to, from } = useGlobalTime(); const { globalFullScreen } = useGlobalFullScreen(); const [ { @@ -250,18 +251,28 @@ const DetectionEnginePageComponent = () => { {i18n.BUTTON_MANAGE_RULES} - + + + + + + + + + + diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 8770e59e0c178..233189a3e8be9 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -54,8 +54,7 @@ import { useListsConfig } from '../../../../containers/detection_engine/lists/us import { SpyRoute } from '../../../../../common/utils/route/spy_routes'; import { StepAboutRuleToggleDetails } from '../../../../components/rules/step_about_rule_details'; import { DetectionEngineHeaderPage } from '../../../../components/detection_engine_header_page'; -import { AlertsHistogramPanel } from '../../../../components/alerts_histogram_panel'; -import { AlertsHistogramOption } from '../../../../components/alerts_histogram_panel/types'; +import { AlertsHistogramPanel } from '../../../../components/alerts_kpis/alerts_histogram_panel'; import { AlertsTable } from '../../../../components/alerts_table'; import { useUserData } from '../../../../components/user_info'; import { OverviewEmpty } from '../../../../../overview/components/overview_empty'; @@ -72,7 +71,6 @@ import { RuleSwitch } from '../../../../components/rules/rule_switch'; import { StepPanel } from '../../../../components/rules/step_panel'; import { getStepsData, redirectToDetections, userHasPermissions } from '../helpers'; import { useGlobalTime } from '../../../../../common/containers/use_global_time'; -import { alertsHistogramOptions } from '../../../../components/alerts_histogram_panel/config'; import { inputsSelectors } from '../../../../../common/store/inputs'; import { setAbsoluteRangeDatePicker } from '../../../../../common/store/inputs/actions'; import { RuleActionsOverflow } from '../../../../components/rules/rule_actions_overflow'; @@ -119,6 +117,7 @@ import { getRuleStatusText } from '../../../../../../common/detection_engine/uti import { MissingPrivilegesCallOut } from '../../../../components/callouts/missing_privileges_callout'; import { useRuleWithFallback } from '../../../../containers/detection_engine/rules/use_rule_with_fallback'; import { BadgeOptions } from '../../../../../common/components/header_page/types'; +import { AlertsStackByField } from '../../../../components/alerts_kpis/common/types'; /** * Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space. @@ -173,7 +172,7 @@ const RuleDetailsPageComponent = () => { const query = useDeepEqualSelector(getGlobalQuerySelector); const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector); - const { to, from, deleteQuery, setQuery } = useGlobalTime(); + const { to, from } = useGlobalTime(); const [ { loading: userInfoLoading, @@ -572,10 +571,7 @@ const RuleDetailsPageComponent = () => { return null; } - const defaultRuleStackByOption: AlertsHistogramOption = { - text: 'event.category', - value: 'event.category', - }; + const defaultRuleStackByOption: AlertsStackByField = 'event.category'; return ( <> @@ -711,15 +707,10 @@ const RuleDetailsPageComponent = () => { <> diff --git a/x-pack/plugins/security_solution/public/network/components/direction/index.tsx b/x-pack/plugins/security_solution/public/network/components/direction/index.tsx index 7c6bb50378d9c..d87756cb9bbab 100644 --- a/x-pack/plugins/security_solution/public/network/components/direction/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/direction/index.tsx @@ -60,13 +60,15 @@ export const DirectionBadge = React.memo<{ contextId: string; direction?: string | null; eventId: string; -}>(({ contextId, eventId, direction }) => ( + isDraggable?: boolean; +}>(({ contextId, eventId, direction, isDraggable }) => ( )); diff --git a/x-pack/plugins/security_solution/public/network/components/ip/index.tsx b/x-pack/plugins/security_solution/public/network/components/ip/index.tsx index a08b8003f142c..2fa3e988784ca 100644 --- a/x-pack/plugins/security_solution/public/network/components/ip/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/ip/index.tsx @@ -22,13 +22,15 @@ export const Ip = React.memo<{ contextId: string; eventId: string; fieldName: string; + isDraggable?: boolean; value?: string | null; -}>(({ contextId, eventId, fieldName, value }) => ( +}>(({ contextId, eventId, fieldName, isDraggable, value }) => ( diff --git a/x-pack/plugins/security_solution/public/network/components/port/index.tsx b/x-pack/plugins/security_solution/public/network/components/port/index.tsx index df288c1abfb06..4afd9bc7b892a 100644 --- a/x-pack/plugins/security_solution/public/network/components/port/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/port/index.tsx @@ -29,7 +29,7 @@ export const Port = React.memo<{ contextId: string; eventId: string; fieldName: string; - isDraggable: boolean; + isDraggable?: boolean; value: string | undefined | null; }>(({ contextId, eventId, fieldName, isDraggable, value }) => isDraggable ? ( @@ -37,6 +37,7 @@ export const Port = React.memo<{ data-test-subj="port" field={fieldName} id={`port-default-draggable-${contextId}-${eventId}-${fieldName}-${value}`} + isDraggable={isDraggable} tooltipContent={fieldName} value={value} > diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/geo_fields.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/geo_fields.tsx index 1f6111cd0bb07..65bd3bf1ec154 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/geo_fields.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/geo_fields.tsx @@ -73,8 +73,9 @@ const GeoFieldValues = React.memo<{ contextId: string; eventId: string; fieldName: string; + isDraggable?: boolean; values?: string[] | null; -}>(({ contextId, eventId, fieldName, values }) => +}>(({ contextId, eventId, fieldName, isDraggable, values }) => values != null ? ( <> {uniq(values).map((value) => ( @@ -92,6 +93,7 @@ const GeoFieldValues = React.memo<{ data-test-subj={fieldName} field={fieldName} id={`geo-field-values-default-draggable-${contextId}-${eventId}-${fieldName}-${value}`} + isDraggable={isDraggable} tooltipContent={fieldName} value={value} /> @@ -114,7 +116,7 @@ GeoFieldValues.displayName = 'GeoFieldValues'; * - `source|destination.geo.city_name` */ export const GeoFields = React.memo((props) => { - const { contextId, eventId, type } = props; + const { contextId, eventId, isDraggable, type } = props; const propNameToFieldName = getGeoFieldPropNameToFieldNameMap(type); return ( @@ -124,6 +126,7 @@ export const GeoFields = React.memo((props) => { contextId={contextId} eventId={eventId} fieldName={geo.fieldName} + isDraggable={isDraggable} key={geo.fieldName} values={get(geo.prop, props)} /> diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/index.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/index.tsx index 57e302d2911fa..d7bcf9f6c5297 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/index.tsx @@ -36,6 +36,7 @@ export const SourceDestination = React.memo( destinationPackets, destinationPort, eventId, + isDraggable, networkBytes, networkCommunityId, networkDirection, @@ -59,8 +60,9 @@ export const SourceDestination = React.memo( packets={networkPackets} communityId={networkCommunityId} contextId={contextId} - eventId={eventId} direction={networkDirection} + eventId={eventId} + isDraggable={isDraggable} protocol={networkProtocol} transport={transport} /> @@ -79,6 +81,7 @@ export const SourceDestination = React.memo( destinationPackets={destinationPackets} destinationPort={destinationPort} eventId={eventId} + isDraggable={isDraggable} sourceBytes={sourceBytes} sourceGeoContinentName={sourceGeoContinentName} sourceGeoCountryName={sourceGeoCountryName} diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/ip_with_port.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/ip_with_port.tsx index 17b55c4229fcc..e99aecbc535e7 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/ip_with_port.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/ip_with_port.tsx @@ -25,9 +25,10 @@ IpPortSeparator.displayName = 'IpPortSeparator'; const PortWithSeparator = React.memo<{ contextId: string; eventId: string; + isDraggable?: boolean; port?: string | null; portFieldName: string; -}>(({ contextId, eventId, port, portFieldName }) => { +}>(({ contextId, eventId, isDraggable, port, portFieldName }) => { return port != null ? ( @@ -39,7 +40,7 @@ const PortWithSeparator = React.memo<{ data-test-subj="port" eventId={eventId} fieldName={portFieldName} - isDraggable={true} + isDraggable={isDraggable} value={port} /> @@ -58,9 +59,10 @@ export const IpWithPort = React.memo<{ eventId: string; ip?: string | null; ipFieldName: string; + isDraggable?: boolean; port?: string | null; portFieldName: string; -}>(({ contextId, eventId, ip, ipFieldName, port, portFieldName }) => ( +}>(({ contextId, eventId, ip, ipFieldName, isDraggable, port, portFieldName }) => ( @@ -75,6 +78,7 @@ export const IpWithPort = React.memo<{ diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/network.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/network.tsx index c1b454892fddf..88bfd19b7066e 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/network.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/network.tsx @@ -45,97 +45,120 @@ export const Network = React.memo<{ contextId: string; direction?: string[] | null; eventId: string; + isDraggable?: boolean; packets?: string[] | null; protocol?: string[] | null; transport?: string[] | null; -}>(({ bytes, communityId, contextId, direction, eventId, packets, protocol, transport }) => ( - - {direction != null - ? uniq(direction).map((dir) => ( - - - - )) - : null} +}>( + ({ + bytes, + communityId, + contextId, + direction, + eventId, + isDraggable, + packets, + protocol, + transport, + }) => ( + + {direction != null + ? uniq(direction).map((dir) => ( + + + + )) + : null} + + {protocol != null + ? uniq(protocol).map((proto) => ( + + + + )) + : null} - {protocol != null - ? uniq(protocol).map((proto) => ( - - - - )) - : null} + {bytes != null + ? uniq(bytes).map((b) => + !isNaN(Number(b)) ? ( + + + + + + + + + + ) : null + ) + : null} - {bytes != null - ? uniq(bytes).map((b) => - !isNaN(Number(b)) ? ( - + {packets != null + ? uniq(packets).map((p) => ( + - - - + {`${p} ${i18n.PACKETS}`} - ) : null - ) - : null} + )) + : null} - {packets != null - ? uniq(packets).map((p) => ( - - - - {`${p} ${i18n.PACKETS}`} - - - - )) - : null} - - {transport != null - ? uniq(transport).map((trans) => ( - - - - )) - : null} + {transport != null + ? uniq(transport).map((trans) => ( + + + + )) + : null} - {communityId != null - ? uniq(communityId).map((trans) => ( - - - - )) - : null} - -)); + {communityId != null + ? uniq(communityId).map((trans) => ( + + + + )) + : null} + + ) +); Network.displayName = 'Network'; diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_arrows.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_arrows.tsx index ff9edff39b3ad..6858520340aae 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_arrows.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_arrows.tsx @@ -56,10 +56,11 @@ Data.displayName = 'Data'; const SourceArrow = React.memo<{ contextId: string; eventId: string; + isDraggable?: boolean; sourceBytes: string | undefined; sourceBytesPercent: number | undefined; sourcePackets: string | undefined; -}>(({ contextId, eventId, sourceBytes, sourceBytesPercent, sourcePackets }) => { +}>(({ contextId, eventId, isDraggable, sourceBytes, sourceBytesPercent, sourcePackets }) => { const sourceArrowHeight = sourceBytesPercent != null ? getArrowHeightFromPercent(sourceBytesPercent) @@ -76,6 +77,7 @@ const SourceArrow = React.memo<{ @@ -101,6 +103,7 @@ const SourceArrow = React.memo<{ @@ -129,73 +132,85 @@ SourceArrow.displayName = 'SourceArrow'; */ const DestinationArrow = React.memo<{ contextId: string; - eventId: string; destinationBytes: string | undefined; destinationBytesPercent: number | undefined; destinationPackets: string | undefined; -}>(({ contextId, eventId, destinationBytes, destinationBytesPercent, destinationPackets }) => { - const destinationArrowHeight = - destinationBytesPercent != null - ? getArrowHeightFromPercent(destinationBytesPercent) - : DEFAULT_ARROW_HEIGHT; + eventId: string; + isDraggable?: boolean; +}>( + ({ + contextId, + destinationBytes, + destinationBytesPercent, + destinationPackets, + eventId, + isDraggable, + }) => { + const destinationArrowHeight = + destinationBytesPercent != null + ? getArrowHeightFromPercent(destinationBytesPercent) + : DEFAULT_ARROW_HEIGHT; + + return ( + + + + - return ( - - - - + + + - - - + {destinationBytes != null && !isNaN(Number(destinationBytes)) ? ( + + + + {destinationBytesPercent != null ? ( + + {`(${numeral(destinationBytesPercent).format('0.00')}%)`} + + ) : null} + + + + + + + ) : null} - {destinationBytes != null && !isNaN(Number(destinationBytes)) ? ( - - - {destinationBytesPercent != null ? ( - - {`(${numeral(destinationBytesPercent).format('0.00')}%)`} - - ) : null} - - - - - + - ) : null} - - - + {destinationPackets != null && !isNaN(Number(destinationPackets)) ? ( + + + + {`${numeral(destinationPackets).format( + '0,0' + )} ${i18n.PACKETS}`} + + + + ) : null} - {destinationPackets != null && !isNaN(Number(destinationPackets)) ? ( - - - {`${numeral(destinationPackets).format( - '0,0' - )} ${i18n.PACKETS}`} - - + - ) : null} - - - - - - ); -}); + + ); + } +); DestinationArrow.displayName = 'DestinationArrow'; @@ -208,67 +223,79 @@ export const SourceDestinationArrows = React.memo<{ destinationBytes?: string[] | null; destinationPackets?: string[] | null; eventId: string; + isDraggable?: boolean; sourceBytes?: string[] | null; sourcePackets?: string[] | null; -}>(({ contextId, destinationBytes, destinationPackets, eventId, sourceBytes, sourcePackets }) => { - const maybeSourceBytes = - sourceBytes != null && hasOneValue(sourceBytes) ? sourceBytes[0] : undefined; - - const maybeSourcePackets = - sourcePackets != null && hasOneValue(sourcePackets) ? sourcePackets[0] : undefined; - - const maybeDestinationBytes = - destinationBytes != null && hasOneValue(destinationBytes) ? destinationBytes[0] : undefined; - - const maybeDestinationPackets = - destinationPackets != null && hasOneValue(destinationPackets) - ? destinationPackets[0] - : undefined; - - const maybeSourceBytesPercent = - maybeSourceBytes != null && maybeDestinationBytes != null - ? getPercent({ - numerator: Number(maybeSourceBytes), - denominator: Number(maybeSourceBytes) + Number(maybeDestinationBytes), - }) - : undefined; - - const maybeDestinationBytesPercent = - maybeSourceBytesPercent != null ? 100 - maybeSourceBytesPercent : undefined; - - return ( - - {maybeSourceBytes != null ? ( - - - - ) : null} - - {maybeDestinationBytes != null ? ( - - - - ) : null} - - ); -}); +}>( + ({ + contextId, + destinationBytes, + destinationPackets, + eventId, + isDraggable, + sourceBytes, + sourcePackets, + }) => { + const maybeSourceBytes = + sourceBytes != null && hasOneValue(sourceBytes) ? sourceBytes[0] : undefined; + + const maybeSourcePackets = + sourcePackets != null && hasOneValue(sourcePackets) ? sourcePackets[0] : undefined; + + const maybeDestinationBytes = + destinationBytes != null && hasOneValue(destinationBytes) ? destinationBytes[0] : undefined; + + const maybeDestinationPackets = + destinationPackets != null && hasOneValue(destinationPackets) + ? destinationPackets[0] + : undefined; + + const maybeSourceBytesPercent = + maybeSourceBytes != null && maybeDestinationBytes != null + ? getPercent({ + numerator: Number(maybeSourceBytes), + denominator: Number(maybeSourceBytes) + Number(maybeDestinationBytes), + }) + : undefined; + + const maybeDestinationBytesPercent = + maybeSourceBytesPercent != null ? 100 - maybeSourceBytesPercent : undefined; + + return ( + + {maybeSourceBytes != null ? ( + + + + ) : null} + {maybeDestinationBytes != null ? ( + + + + ) : null} + + ); + } +); SourceDestinationArrows.displayName = 'SourceDestinationArrows'; diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx index 91f7ea3d7ac7a..824b9fd11f242 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx @@ -958,6 +958,7 @@ describe('SourceDestinationIp', () => { destinationIp={asArrayIfExists(get(DESTINATION_IP_FIELD_NAME, getMockNetflowData()))} destinationPort={asArrayIfExists(get(DESTINATION_PORT_FIELD_NAME, getMockNetflowData()))} eventId={get(ID_FIELD_NAME, getMockNetflowData())} + isDraggable={true} sourceGeoContinentName={asArrayIfExists( get(SOURCE_GEO_CONTINENT_NAME_FIELD_NAME, getMockNetflowData()) )} @@ -979,7 +980,6 @@ describe('SourceDestinationIp', () => { /> ); - expect( removeExternalLinkText( wrapper.find('[data-test-subj="draggable-content-source.port"]').first().text() @@ -1011,6 +1011,7 @@ describe('SourceDestinationIp', () => { destinationIp={asArrayIfExists(get(DESTINATION_IP_FIELD_NAME, getMockNetflowData()))} destinationPort={asArrayIfExists(get(DESTINATION_PORT_FIELD_NAME, getMockNetflowData()))} eventId={get(ID_FIELD_NAME, getMockNetflowData())} + isDraggable={true} sourceGeoContinentName={asArrayIfExists( get(SOURCE_GEO_CONTINENT_NAME_FIELD_NAME, getMockNetflowData()) )} @@ -1064,6 +1065,7 @@ describe('SourceDestinationIp', () => { destinationIp={asArrayIfExists(get(DESTINATION_IP_FIELD_NAME, getMockNetflowData()))} destinationPort={asArrayIfExists(get(DESTINATION_PORT_FIELD_NAME, getMockNetflowData()))} eventId={get(ID_FIELD_NAME, getMockNetflowData())} + isDraggable={true} sourceGeoContinentName={asArrayIfExists( get(SOURCE_GEO_CONTINENT_NAME_FIELD_NAME, getMockNetflowData()) )} @@ -1118,6 +1120,7 @@ describe('SourceDestinationIp', () => { destinationIp={undefined} destinationPort={asArrayIfExists(get(DESTINATION_PORT_FIELD_NAME, getMockNetflowData()))} eventId={get(ID_FIELD_NAME, getMockNetflowData())} + isDraggable={true} sourceGeoContinentName={asArrayIfExists( get(SOURCE_GEO_CONTINENT_NAME_FIELD_NAME, getMockNetflowData()) )} @@ -1271,6 +1274,7 @@ describe('SourceDestinationIp', () => { destinationIp={asArrayIfExists(get(DESTINATION_IP_FIELD_NAME, getMockNetflowData()))} destinationPort={asArrayIfExists(get(DESTINATION_PORT_FIELD_NAME, getMockNetflowData()))} eventId={get(ID_FIELD_NAME, getMockNetflowData())} + isDraggable={true} sourceGeoContinentName={asArrayIfExists( get(SOURCE_GEO_CONTINENT_NAME_FIELD_NAME, getMockNetflowData()) )} diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.tsx index db9773789bf54..31bae6880fcbe 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.tsx @@ -88,54 +88,67 @@ const IpAdressesWithPorts = React.memo<{ destinationIp?: string[] | null; destinationPort?: Array | null; eventId: string; + isDraggable?: boolean; sourceIp?: string[] | null; sourcePort?: Array | null; type: SourceDestinationType; -}>(({ contextId, destinationIp, destinationPort, eventId, sourceIp, sourcePort, type }) => { - const ip = type === 'source' ? sourceIp : destinationIp; - const ipFieldName = type === 'source' ? SOURCE_IP_FIELD_NAME : DESTINATION_IP_FIELD_NAME; - const port = type === 'source' ? sourcePort : destinationPort; - const portFieldName = type === 'source' ? SOURCE_PORT_FIELD_NAME : DESTINATION_PORT_FIELD_NAME; - - if (ip == null) { - return null; // if ip is not populated as an array, ports will be ignored +}>( + ({ + contextId, + destinationIp, + destinationPort, + eventId, + isDraggable, + sourceIp, + sourcePort, + type, + }) => { + const ip = type === 'source' ? sourceIp : destinationIp; + const ipFieldName = type === 'source' ? SOURCE_IP_FIELD_NAME : DESTINATION_IP_FIELD_NAME; + const port = type === 'source' ? sourcePort : destinationPort; + const portFieldName = type === 'source' ? SOURCE_PORT_FIELD_NAME : DESTINATION_PORT_FIELD_NAME; + + if (ip == null) { + return null; // if ip is not populated as an array, ports will be ignored + } + + // IMPORTANT: The ip and port arrays are parallel arrays; the port at + // index `i` corresponds with the ip address at index `i`. We must + // preserve the relationships between the parallel arrays: + const ipPortPairs: IpPortPair[] = + port != null && ip.length === port.length + ? ip.map((address, i) => ({ + ip: address, + port: port[i] != null ? `${port[i]}` : null, // use the corresponding port in the parallel array + })) + : ip.map((address) => ({ + ip: address, + port: null, // drop the port, because the length of the parallel ip and port arrays is different + })); + + return ( + + {uniqWith(deepEqual, ipPortPairs).map( + (ipPortPair) => + ipPortPair.ip != null && ( + + + + ) + )} + + ); } - - // IMPORTANT: The ip and port arrays are parallel arrays; the port at - // index `i` corresponds with the ip address at index `i`. We must - // preserve the relationships between the parallel arrays: - const ipPortPairs: IpPortPair[] = - port != null && ip.length === port.length - ? ip.map((address, i) => ({ - ip: address, - port: port[i] != null ? `${port[i]}` : null, // use the corresponding port in the parallel array - })) - : ip.map((address) => ({ - ip: address, - port: null, // drop the port, because the length of the parallel ip and port arrays is different - })); - - return ( - - {uniqWith(deepEqual, ipPortPairs).map( - (ipPortPair) => - ipPortPair.ip != null && ( - - - - ) - )} - - ); -}); +); IpAdressesWithPorts.displayName = 'IpAdressesWithPorts'; @@ -159,6 +172,7 @@ export const SourceDestinationIp = React.memo( destinationIp, destinationPort, eventId, + isDraggable, sourceGeoContinentName, sourceGeoCountryName, sourceGeoCountryIsoCode, @@ -189,6 +203,7 @@ export const SourceDestinationIp = React.memo( destinationIp={destinationIp} destinationPort={destinationPort} eventId={eventId} + isDraggable={isDraggable} sourceIp={sourceIp} sourcePort={sourcePort} type={type} @@ -202,7 +217,7 @@ export const SourceDestinationIp = React.memo( data-test-subj="port" eventId={eventId} fieldName={`${type}.port`} - isDraggable={true} + isDraggable={isDraggable} value={port} /> @@ -219,6 +234,7 @@ export const SourceDestinationIp = React.memo( destinationGeoRegionName={destinationGeoRegionName} destinationGeoCityName={destinationGeoCityName} eventId={eventId} + isDraggable={isDraggable} sourceGeoContinentName={sourceGeoContinentName} sourceGeoCountryName={sourceGeoCountryName} sourceGeoCountryIsoCode={sourceGeoCountryIsoCode} diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_with_arrows.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_with_arrows.tsx index 3d6189118ecb0..a010d674291ba 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_with_arrows.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_with_arrows.tsx @@ -32,6 +32,7 @@ export const SourceDestinationWithArrows = React.memo @@ -85,6 +88,7 @@ export const SourceDestinationWithArrows = React.memo | null; eventId: string; + isDraggable?: boolean; sourceGeoContinentName?: string[] | null; sourceGeoCountryName?: string[] | null; sourceGeoCountryIsoCode?: string[] | null; @@ -85,6 +88,7 @@ export interface SourceDestinationWithArrowsProps { destinationPackets?: string[] | null; destinationPort?: string[] | null; eventId: string; + isDraggable?: boolean; sourceBytes?: string[] | null; sourceGeoContinentName?: string[] | null; sourceGeoCountryName?: string[] | null; diff --git a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx index a7823a1a6b98d..76116f2261118 100644 --- a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx @@ -8,22 +8,21 @@ import React, { useCallback } from 'react'; import { useDispatch } from 'react-redux'; -import { AlertsHistogramPanel } from '../../../detections/components/alerts_histogram_panel'; -import { alertsHistogramOptions } from '../../../detections/components/alerts_histogram_panel/config'; +import { AlertsHistogramPanel } from '../../../detections/components/alerts_kpis/alerts_histogram_panel'; import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index'; import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; import { Filter, Query } from '../../../../../../../src/plugins/data/public'; import { InputsModelId } from '../../../common/store/inputs/constants'; import * as i18n from '../../pages/translations'; import { UpdateDateRange } from '../../../common/components/charts/common'; -import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; +import { AlertsStackByField } from '../../../detections/components/alerts_kpis/common/types'; -interface Props extends Pick { +interface Props { combinedQueries?: string; filters?: Filter[]; headerChildren?: React.ReactNode; /** Override all defaults, and only display this field */ - onlyField?: string; + onlyField?: AlertsStackByField; query?: Query; setAbsoluteRangeDatePickerTarget?: InputsModelId; timelineId?: string; @@ -31,16 +30,12 @@ interface Props extends Pick = ({ combinedQueries, - deleteQuery, filters, - from, headerChildren, onlyField, query, setAbsoluteRangeDatePickerTarget = 'global', - setQuery, timelineId, - to, }) => { const dispatch = useDispatch(); const { signalIndexName } = useSignalIndex(); @@ -64,20 +59,17 @@ const SignalsByCategoryComponent: React.FC = ({ return ( diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index ed12dce6db482..9c0b1ea87e1f9 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -96,13 +96,7 @@ const OverviewComponent = () => { - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.tsx index 29775067478a5..296faf208ac91 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.tsx @@ -40,8 +40,9 @@ export const CertificateFingerprint = React.memo<{ certificateType: CertificateType; contextId: string; fieldName: string; + isDraggable?: boolean; value?: string | null; -}>(({ eventId, certificateType, contextId, fieldName, value }) => { +}>(({ eventId, certificateType, contextId, fieldName, isDraggable, value }) => { return ( {fieldName} diff --git a/x-pack/plugins/security_solution/public/timelines/components/duration/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/duration/index.tsx index 421ba5941eaef..7500fdb122fae 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/duration/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/duration/index.tsx @@ -26,6 +26,7 @@ export const Duration = React.memo<{ isDraggable ? ( @@ -24,6 +25,7 @@ exports[`Field Renderers #autonomousSystemRenderer it renders correctly against @@ -58,6 +60,7 @@ exports[`Field Renderers #hostIdRenderer it renders correctly against snapshot 1 }, } } + isDraggable={false} render={[Function]} /> `; @@ -79,6 +82,7 @@ exports[`Field Renderers #hostNameRenderer it renders correctly against snapshot }, } } + isDraggable={false} render={[Function]} /> `; @@ -94,6 +98,7 @@ exports[`Field Renderers #locationRenderer it renders correctly against snapshot @@ -84,6 +85,7 @@ export const autonomousSystemRenderer = ( id={`autonomous-system-renderer-default-draggable-${IpOverviewId}-${ contextID ? `${contextID}-` : '' }${flowTarget}.as.organization.name`} + isDraggable={false} field={`${flowTarget}.as.organization.name`} value={as.organization.name} /> @@ -94,6 +96,7 @@ export const autonomousSystemRenderer = ( id={`autonomous-system-renderer-default-draggable-${IpOverviewId}-${ contextID ? `${contextID}-` : '' }${flowTarget}.as.number`} + isDraggable={false} field={`${flowTarget}.as.number`} value={`${as.number}`} /> @@ -123,6 +126,7 @@ export const hostIdRenderer = ({ id={`host-id-renderer-default-draggable-${IpOverviewId}-${ contextID ? `${contextID}-` : '' }host-id`} + isDraggable={false} field="host.id" value={host.id[0]} > @@ -154,6 +158,7 @@ export const hostNameRenderer = ( id={`host-name-renderer-default-draggable-${IpOverviewId}-${ contextID ? `${contextID}-` : '' }host-name`} + isDraggable={false} field={'host.name'} value={host.name[0]} > @@ -204,7 +209,7 @@ export const DefaultFieldRendererComponent: React.FC )} {typeof rowItem === 'string' && ( - + {render ? render(rowItem) : rowItem} )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx index 5014a198e8bd5..5acc0ef9aa46b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx @@ -59,11 +59,11 @@ describe('FieldName', () => { ); await waitFor(() => { - wrapper.find('[data-test-subj="withHoverActionsButton"]').at(0).simulate('mouseenter'); + wrapper.find('[data-test-subj="withHoverActionsButton"]').simulate('mouseenter'); wrapper.update(); jest.runAllTimers(); wrapper.update(); - expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(true); + expect(wrapper.find('[data-test-subj="hover-actions-copy-button"]').exists()).toBe(true); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx index 2e76e43227506..1e081d249cc00 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx @@ -11,11 +11,9 @@ import styled from 'styled-components'; import { OnUpdateColumns } from '../timeline/events'; import { WithHoverActions } from '../../../common/components/with_hover_actions'; -import { - DraggableWrapperHoverContent, - useGetTimelineId, -} from '../../../common/components/drag_and_drop/draggable_wrapper_hover_content'; +import { useGetTimelineId } from '../../../common/components/drag_and_drop/use_get_timeline_id_from_dom'; import { ColumnHeaderOptions } from '../../../../common'; +import { HoverActions } from '../../../common/components/hover_actions'; /** * The name of a (draggable) field @@ -112,9 +110,10 @@ export const FieldName = React.memo<{ const hoverContent = useMemo( () => ( - = ({ return ( (({ contextId, eventId, fieldName, value }) => ( +}>(({ contextId, eventId, fieldName, isDraggable, value }) => ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/fingerprints/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/netflow/fingerprints/index.tsx index 16ea48890778e..328d310524070 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/netflow/fingerprints/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/fingerprints/index.tsx @@ -23,6 +23,7 @@ import { JA3_HASH_FIELD_NAME, Ja3Fingerprint } from '../../ja3_fingerprint'; export const Fingerprints = React.memo<{ contextId: string; eventId: string; + isDraggable?: boolean; tlsClientCertificateFingerprintSha1?: string[] | null; tlsFingerprintsJa3Hash?: string[] | null; tlsServerCertificateFingerprintSha1?: string[] | null; @@ -30,6 +31,7 @@ export const Fingerprints = React.memo<{ ({ contextId, eventId, + isDraggable, tlsClientCertificateFingerprintSha1, tlsFingerprintsJa3Hash, tlsServerCertificateFingerprintSha1, @@ -48,6 +50,7 @@ export const Fingerprints = React.memo<{ eventId={eventId} fieldName={JA3_HASH_FIELD_NAME} contextId={contextId} + isDraggable={isDraggable} value={ja3} /> @@ -61,6 +64,7 @@ export const Fingerprints = React.memo<{ certificateType="client" contextId={contextId} fieldName={TLS_CLIENT_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME} + isDraggable={isDraggable} value={clientCert} /> @@ -74,6 +78,7 @@ export const Fingerprints = React.memo<{ certificateType="server" contextId={contextId} fieldName={TLS_SERVER_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME} + isDraggable={isDraggable} value={serverCert} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.tsx index 05bfe56d1df42..a755aa54fca7b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.tsx @@ -37,6 +37,7 @@ export const Netflow = React.memo( eventId, eventEnd, eventStart, + isDraggable, networkBytes, networkCommunityId, networkDirection, @@ -82,6 +83,7 @@ export const Netflow = React.memo( eventId={eventId} eventEnd={eventEnd} eventStart={eventStart} + isDraggable={isDraggable} networkBytes={networkBytes} networkCommunityId={networkCommunityId} networkDirection={networkDirection} @@ -105,6 +107,7 @@ export const Netflow = React.memo( (({ contextId, eventDuration, eventId, eventEnd, eventStart }) => ( + isDraggable?: boolean; +}>(({ contextId, eventDuration, eventId, eventEnd, eventStart, isDraggable }) => ( @@ -94,6 +97,7 @@ export const DurationEventStartEnd = React.memo<{ data-test-subj="event-end" field={EVENT_END_FIELD_NAME} id={`duration-event-start-end-default-draggable-${contextId}-${eventId}-${EVENT_END_FIELD_NAME}-${end}`} + isDraggable={isDraggable} tooltipContent={null} value={end} > diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/index.tsx index 4714b561f036b..e319e803e63fe 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/index.tsx @@ -48,6 +48,7 @@ export const NetflowColumns = React.memo( eventId, eventEnd, eventStart, + isDraggable, networkBytes, networkCommunityId, networkDirection, @@ -76,6 +77,7 @@ export const NetflowColumns = React.memo( @@ -88,6 +90,7 @@ export const NetflowColumns = React.memo( eventId={eventId} eventEnd={eventEnd} eventStart={eventStart} + isDraggable={isDraggable} /> @@ -104,6 +107,7 @@ export const NetflowColumns = React.memo( destinationPackets={destinationPackets} destinationPort={destinationPort} eventId={eventId} + isDraggable={isDraggable} networkBytes={networkBytes} networkCommunityId={networkCommunityId} networkDirection={networkDirection} diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/types.ts b/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/types.ts index 532b35f4cffd0..801df93bfcf37 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/types.ts @@ -21,6 +21,7 @@ export interface NetflowColumnsProps { eventId: string; eventEnd?: string[] | null; eventStart?: string[] | null; + isDraggable?: boolean; networkBytes?: string[] | null; networkCommunityId?: string[] | null; networkDirection?: string[] | null; diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/user_process.tsx b/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/user_process.tsx index e6931baeb7017..72de537fee588 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/user_process.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/user_process.tsx @@ -22,9 +22,10 @@ export const USER_NAME_FIELD_NAME = 'user.name'; export const UserProcess = React.memo<{ contextId: string; eventId: string; + isDraggable?: boolean; processName?: string[] | null; userName?: string[] | null; -}>(({ contextId, eventId, processName, userName }) => ( +}>(({ contextId, eventId, isDraggable, processName, userName }) => ( @@ -55,6 +57,7 @@ export const UserProcess = React.memo<{ data-test-subj="process-name" eventId={eventId} field={PROCESS_NAME_FIELD_NAME} + isDraggable={isDraggable} value={process} iconType="console" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/types.ts b/x-pack/plugins/security_solution/public/timelines/components/netflow/types.ts index a28334e2d45fb..0798345c61da9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/netflow/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/types.ts @@ -20,6 +20,7 @@ export interface NetflowProps { eventId: string; eventEnd?: string[] | null; eventStart?: string[] | null; + isDraggable?: boolean; networkBytes?: string[] | null; networkCommunityId?: string[] | null; networkDirection?: string[] | null; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/alerts.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/alerts.tsx index b0384155c5c10..d6aa34f2528e5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/alerts.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/alerts.tsx @@ -26,6 +26,7 @@ const AlertsExampleComponent: React.FC = () => { {alertsRowRenderer.renderRow({ browserFields: {}, data: mockEndpointProcessExecutionMalwarePreventionAlert, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd.tsx index 703621bc4c666..2c6ce5886462b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd.tsx @@ -23,6 +23,7 @@ const AuditdExampleComponent: React.FC = () => { {auditdRowRenderer.renderRow({ browserFields: {}, data: mockTimelineData[26].ecs, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx index 265a71ef264d1..a525b26571dc8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx @@ -23,6 +23,7 @@ const AuditdFileExampleComponent: React.FC = () => { {auditdFileRowRenderer.renderRow({ browserFields: {}, data: mockTimelineData[27].ecs, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/library.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/library.tsx index 6198225fcb87d..f8704b63fe47e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/library.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/library.tsx @@ -23,6 +23,7 @@ const LibraryExampleComponent: React.FC = () => { {libraryRowRenderer.renderRow({ browserFields: {}, data: mockEndpointLibraryLoadEvent, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/netflow.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/netflow.tsx index cd20b28203246..c5a0f09440899 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/netflow.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/netflow.tsx @@ -16,6 +16,7 @@ const NetflowExampleComponent: React.FC = () => ( {netflowRowRenderer.renderRow({ browserFields: {}, data: getMockNetflowData(), + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/registry.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/registry.tsx index f00db0d94eed8..67859db1a5ea4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/registry.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/registry.tsx @@ -23,6 +23,7 @@ const RegistryExampleComponent: React.FC = () => { {registryRowRenderer.renderRow({ browserFields: {}, data: mockEndpointRegistryModificationEvent, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/suricata.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/suricata.tsx index f22ac0dca6f9d..1e6caca2effa9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/suricata.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/suricata.tsx @@ -16,6 +16,7 @@ const SuricataExampleComponent: React.FC = () => ( {suricataRowRenderer.renderRow({ browserFields: {}, data: mockTimelineData[2].ecs, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx index 909d5224fb351..7d38f8feaace6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx @@ -23,6 +23,7 @@ const SystemExampleComponent: React.FC = () => { {systemRowRenderer.renderRow({ browserFields: {}, data: mockEndgameTerminationEvent, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_dns.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_dns.tsx index 0f413eed811be..72c5060b27701 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_dns.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_dns.tsx @@ -19,6 +19,7 @@ const SystemDnsExampleComponent: React.FC = () => { {systemDnsRowRenderer.renderRow({ browserFields: {}, data: mockEndgameDnsRequest, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_endgame_process.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_endgame_process.tsx index 0e5fe1768c787..6103746b3238b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_endgame_process.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_endgame_process.tsx @@ -23,6 +23,7 @@ const SystemEndgameProcessExampleComponent: React.FC = () => { {systemEndgameProcessRowRenderer.renderRow({ browserFields: {}, data: mockEndgameCreationEvent, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx index 3db9a93fc37c9..cb8668536f8d2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx @@ -23,6 +23,7 @@ const SystemFileExampleComponent: React.FC = () => { {systemFileRowRenderer.renderRow({ browserFields: {}, data: mockEndgameFileDeleteEvent, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_fim.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_fim.tsx index 08ff6a5ddc7c9..12ad132131d1b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_fim.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_fim.tsx @@ -23,6 +23,7 @@ const SystemFimExampleComponent: React.FC = () => { {systemFimRowRenderer.renderRow({ browserFields: {}, data: mockEndgameFileCreateEvent, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_security_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_security_event.tsx index 59b5fedbc82fa..8dfb0bf998738 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_security_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_security_event.tsx @@ -21,6 +21,7 @@ const SystemSecurityEventExampleComponent: React.FC = () => { {systemSecurityEventRowRenderer.renderRow({ browserFields: {}, data: mockEndgameUserLogon, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_socket.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_socket.tsx index 5175145bae9d9..7fa430e812625 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_socket.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_socket.tsx @@ -22,6 +22,7 @@ const SystemSocketExampleComponent: React.FC = () => { {systemSocketRowRenderer.renderRow({ browserFields: {}, data: mockEndgameIpv4ConnectionAcceptEvent, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/threat_match.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/threat_match.tsx index 9d7e5d48315e3..73d458a23ca17 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/threat_match.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/threat_match.tsx @@ -16,6 +16,7 @@ const ThreatMatchExampleComponent: React.FC = () => ( {threatMatchRowRenderer.renderRow({ browserFields: {}, data: mockTimelineData[31].ecs, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/zeek.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/zeek.tsx index b84942ea8b2a8..83d9e0122e971 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/zeek.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/zeek.tsx @@ -16,6 +16,7 @@ const ZeekExampleComponent: React.FC = () => ( {zeekRowRenderer.renderRow({ browserFields: {}, data: mockTimelineData[13].ecs, + isDraggable: false, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx index fb48319057788..de190c7df5e3f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx @@ -51,6 +51,24 @@ const SortingColumnsContainer = styled.div` } `; +const FieldBrowserContainer = styled.div` + .euiToolTipAnchor { + .euiButtonContent { + padding: ${({ theme }) => `0 ${theme.eui.paddingSizes.xs}`}; + } + button { + color: ${({ theme }) => theme.eui.euiColorPrimary}; + } + .euiButtonContent__icon { + width: 16px; + height: 16px; + } + .euiButtonEmpty__text { + display: none; + } + } +`; + const ActionsContainer = styled.div` align-items: center; display: flex; @@ -160,11 +178,13 @@ const HeaderActionsComponent: React.FC = ({ )} - {timelinesUi.getFieldBrowser({ - browserFields, - columnHeaders, - timelineId, - })} + + {timelinesUi.getFieldBrowser({ + browserFields, + columnHeaders, + timelineId, + })} + diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx index c5d39dd80c7ca..66deeddaf03f2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx @@ -93,4 +93,36 @@ describe('Actions', () => { expect(wrapper.find('[data-test-subj="select-event"]').exists()).toBe(false); }); + + test('it does NOT render a checkbox for selecting the event when `tGridEnabled` is `true`', () => { + useIsExperimentalFeatureEnabledMock.mockReturnValue(true); + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="select-event"]').exists()).toBe(false); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index 93039e6fd44e5..ab34ea37efeac 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -11,6 +11,7 @@ import { EuiButtonIcon, EuiCheckbox, EuiLoadingSpinner, EuiToolTip } from '@elas import { noop } from 'lodash/fp'; import styled from 'styled-components'; +import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import { eventHasNotes, getEventType, @@ -52,13 +53,14 @@ const ActionsComponent: React.FC = ({ onEventDetailsPanelOpened, onRowSelected, refetch, - onRuleChange, showCheckboxes, + onRuleChange, showNotes, timelineId, toggleShowNotes, }) => { const dispatch = useDispatch(); + const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); const emptyNotes: string[] = []; const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const { timelines: timelinesUi } = useKibana().services; @@ -81,6 +83,7 @@ const ActionsComponent: React.FC = ({ }), [eventId, onRowSelected] ); + const handlePinClicked = useCallback( () => getPinOnClick({ @@ -113,7 +116,7 @@ const ActionsComponent: React.FC = ({ }, [ariaRowindex, ecsData, casePermissions, insertTimelineHook, columnValues]); return ( - {showCheckboxes && ( + {showCheckboxes && !tGridEnabled && (
{loadingEventIds.includes(eventId) ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx index 19abd6841e7e8..cf1f4a26c709d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx @@ -80,6 +80,7 @@ export const StatefulRowRenderer = ({ {rowRenderer.renderRow({ browserFields, data: event.ecs, + isDraggable: true, timelineId, })}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/empty_column_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/empty_column_renderer.test.tsx.snap index 92816d499b029..722c7a7aebb00 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/empty_column_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/empty_column_renderer.test.tsx.snap @@ -19,6 +19,7 @@ exports[`empty_column_renderer renders correctly against snapshot 1`] = ` }, } } + isDraggable={true} key="empty-column-renderer-draggable-wrapper-test-source.ip-1-source.ip" render={[Function]} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx index 417cf0ceee184..edec2d0d823fa 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx @@ -42,6 +42,7 @@ export const AgentStatuses = React.memo( @@ -60,6 +61,7 @@ export const AgentStatuses = React.memo( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/args.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/args.tsx index bdb650585bdb0..fbb2c2edf8ae3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/args.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/args.tsx @@ -15,9 +15,10 @@ interface Props { contextId: string; eventId: string; processTitle: string | null | undefined; + isDraggable?: boolean; } -export const ArgsComponent = ({ args, contextId, eventId, processTitle }: Props) => { +export const ArgsComponent = ({ args, contextId, eventId, processTitle, isDraggable }: Props) => { if (isNillEmptyOrNotFinite(args) && isNillEmptyOrNotFinite(processTitle)) { return null; } @@ -31,6 +32,7 @@ export const ArgsComponent = ({ args, contextId, eventId, processTitle }: Props) contextId={`${contextId}-args-${i}-${arg}`} eventId={eventId} field="process.args" + isDraggable={isDraggable} value={arg} /> @@ -42,6 +44,7 @@ export const ArgsComponent = ({ args, contextId, eventId, processTitle }: Props) contextId={contextId} eventId={eventId} field="process.title" + isDraggable={isDraggable} value={processTitle} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap index 63b65d3cf36be..684764e39848f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap @@ -98,6 +98,7 @@ exports[`GenericRowRenderer #createGenericAuditRowRenderer renders correctly aga }, } } + isDraggable={true} text="connected using" timelineId="test" /> @@ -222,6 +223,7 @@ exports[`GenericRowRenderer #createGenericFileRowRenderer renders correctly agai } } fileIcon="document" + isDraggable={true} text="opened file using" timelineId="test" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_details.tsx index 737d0b74bfbf9..fb14d44995c95 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_details.tsx @@ -36,6 +36,7 @@ interface Props { workingDirectory: string | null | undefined; args: string[] | null | undefined; session: string | null | undefined; + isDraggable?: boolean; } export const AuditdGenericLine = React.memo( @@ -55,6 +56,7 @@ export const AuditdGenericLine = React.memo( result, session, text, + isDraggable, }) => ( ( secondary={secondary} workingDirectory={workingDirectory} session={session} + isDraggable={isDraggable} /> {processExecutable != null && ( @@ -81,9 +84,16 @@ export const AuditdGenericLine = React.memo( processPid={processPid} processName={processName} processExecutable={processExecutable} + isDraggable={isDraggable} /> - + {result != null && ( {i18n.WITH_RESULT} @@ -94,6 +104,7 @@ export const AuditdGenericLine = React.memo( contextId={contextId} eventId={id} field="auditd.result" + isDraggable={isDraggable} queryValue={result} value={result} /> @@ -107,13 +118,14 @@ AuditdGenericLine.displayName = 'AuditdGenericLine'; interface GenericDetailsProps { browserFields: BrowserFields; data: Ecs; + isDraggable?: boolean; contextId: string; text: string; timelineId: string; } export const AuditdGenericDetails = React.memo( - ({ data, contextId, text, timelineId }) => { + ({ data, contextId, isDraggable, text, timelineId }) => { const id = data._id; const session: string | null | undefined = get('auditd.session[0]', data); const hostName: string | null | undefined = get('host.name[0]', data); @@ -146,9 +158,10 @@ export const AuditdGenericDetails = React.memo( primary={primary} result={result} secondary={secondary} + isDraggable={isDraggable} /> - + ); } else { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.tsx index efab1a433c0bb..89fbbf751b0ee 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.tsx @@ -38,6 +38,7 @@ interface Props { workingDirectory: string | null | undefined; args: string[] | null | undefined; session: string | null | undefined; + isDraggable?: boolean; } export const AuditdGenericFileLine = React.memo( @@ -59,6 +60,7 @@ export const AuditdGenericFileLine = React.memo( session, text, fileIcon, + isDraggable, }) => ( ( secondary={secondary} workingDirectory={workingDirectory} session={session} + isDraggable={isDraggable} /> {(filePath != null || processExecutable != null) && ( @@ -81,6 +84,7 @@ export const AuditdGenericFileLine = React.memo( contextId={contextId} eventId={id} field="file.path" + isDraggable={isDraggable} value={filePath} iconType={fileIcon} /> @@ -96,12 +100,19 @@ export const AuditdGenericFileLine = React.memo( endgamePid={undefined} endgameProcessName={undefined} eventId={id} + isDraggable={isDraggable} processPid={processPid} processName={processName} processExecutable={processExecutable} /> - + {result != null && ( {i18n.WITH_RESULT} @@ -112,6 +123,7 @@ export const AuditdGenericFileLine = React.memo( contextId={contextId} eventId={id} field="auditd.result" + isDraggable={isDraggable} queryValue={result} value={result} /> @@ -124,15 +136,16 @@ AuditdGenericFileLine.displayName = 'AuditdGenericFileLine'; interface GenericDetailsProps { browserFields: BrowserFields; - data: Ecs; contextId: string; + data: Ecs; text: string; fileIcon: IconType; timelineId: string; + isDraggable?: boolean; } export const AuditdGenericFileDetails = React.memo( - ({ data, contextId, text, fileIcon = 'document', timelineId }) => { + ({ data, contextId, text, fileIcon = 'document', timelineId, isDraggable }) => { const id = data._id; const session: string | null | undefined = get('auditd.session[0]', data); const hostName: string | null | undefined = get('host.name[0]', data); @@ -169,9 +182,10 @@ export const AuditdGenericFileDetails = React.memo( secondary={secondary} fileIcon={fileIcon} result={result} + isDraggable={isDraggable} /> - + ); } else { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx index 74a5ff472b581..1f44feb3b394f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx @@ -55,6 +55,7 @@ describe('GenericRowRenderer', () => { const children = connectedToRenderer.renderRow({ browserFields, data: auditd, + isDraggable: true, timelineId: 'test', }); @@ -84,6 +85,7 @@ describe('GenericRowRenderer', () => { const children = connectedToRenderer.renderRow({ browserFields: mockBrowserFields, data: auditd, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( @@ -117,6 +119,7 @@ describe('GenericRowRenderer', () => { const children = fileToRenderer.renderRow({ browserFields, data: auditdFile, + isDraggable: true, timelineId: 'test', }); @@ -146,6 +149,7 @@ describe('GenericRowRenderer', () => { const children = fileToRenderer.renderRow({ browserFields: mockBrowserFields, data: auditdFile, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx index 765bfd3d21351..d0522e97157ab 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx @@ -36,11 +36,12 @@ export const createGenericAuditRowRenderer = ({ action.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, timelineId }) => ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx index 8fd8cfd5af9da..5857dc1e30182 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx @@ -21,69 +21,77 @@ interface Props { eventId: string; primary: string | null | undefined; secondary: string | null | undefined; + isDraggable?: boolean; } -export const PrimarySecondary = React.memo(({ contextId, eventId, primary, secondary }) => { - if (nilOrUnSet(primary) && nilOrUnSet(secondary)) { - return null; - } else if (!nilOrUnSet(primary) && nilOrUnSet(secondary)) { - return ( - - ); - } else if (nilOrUnSet(primary) && !nilOrUnSet(secondary)) { - return ( - - ); - } else if (primary === secondary) { - return ( - - ); - } else { - return ( - - - - - - {i18n.AS} - - - - - - ); +export const PrimarySecondary = React.memo( + ({ contextId, eventId, primary, secondary, isDraggable }) => { + if (nilOrUnSet(primary) && nilOrUnSet(secondary)) { + return null; + } else if (!nilOrUnSet(primary) && nilOrUnSet(secondary)) { + return ( + + ); + } else if (nilOrUnSet(primary) && !nilOrUnSet(secondary)) { + return ( + + ); + } else if (primary === secondary) { + return ( + + ); + } else { + return ( + + + + + + {i18n.AS} + + + + + + ); + } } -}); +); PrimarySecondary.displayName = 'PrimarySecondary'; @@ -93,10 +101,11 @@ interface PrimarySecondaryUserInfoProps { userName: string | null | undefined; primary: string | null | undefined; secondary: string | null | undefined; + isDraggable?: boolean; } export const PrimarySecondaryUserInfo = React.memo( - ({ contextId, eventId, userName, primary, secondary }) => { + ({ contextId, eventId, userName, primary, secondary, isDraggable }) => { if (nilOrUnSet(userName) && nilOrUnSet(primary) && nilOrUnSet(secondary)) { return null; } else if ( @@ -111,6 +120,7 @@ export const PrimarySecondaryUserInfo = React.memo @@ -121,6 +131,7 @@ export const PrimarySecondaryUserInfo = React.memo @@ -130,6 +141,7 @@ export const PrimarySecondaryUserInfo = React.memo diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx index a7252064d9774..f90407b882fdf 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx @@ -23,10 +23,21 @@ interface Props { secondary: string | null | undefined; workingDirectory: string | null | undefined; session: string | null | undefined; + isDraggable?: boolean; } export const SessionUserHostWorkingDir = React.memo( - ({ eventId, contextId, hostName, userName, primary, secondary, workingDirectory, session }) => ( + ({ + eventId, + contextId, + hostName, + userName, + primary, + secondary, + workingDirectory, + session, + isDraggable, + }) => ( <> {i18n.SESSION} @@ -38,6 +49,7 @@ export const SessionUserHostWorkingDir = React.memo( field="auditd.session" value={session} iconType="number" + isDraggable={isDraggable} /> @@ -47,6 +59,7 @@ export const SessionUserHostWorkingDir = React.memo( userName={userName} primary={primary} secondary={secondary} + isDraggable={isDraggable} /> {hostName != null && ( @@ -59,6 +72,7 @@ export const SessionUserHostWorkingDir = React.memo( eventId={eventId} workingDirectory={workingDirectory} hostName={hostName} + isDraggable={isDraggable} /> ) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.tsx index e2418334dfc80..8859c601ad56d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.tsx @@ -26,6 +26,7 @@ export const Bytes = React.memo<{ isDraggable ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/indicator_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/indicator_details.tsx index 11846632f740e..f2e4555147c50 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/indicator_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/indicator_details.tsx @@ -26,6 +26,7 @@ interface IndicatorDetailsProps { indicatorProvider: string | undefined; indicatorReference: string | undefined; indicatorType: string | undefined; + isDraggable?: boolean; } export const IndicatorDetails: React.FC = ({ @@ -35,6 +36,7 @@ export const IndicatorDetails: React.FC = ({ indicatorProvider, indicatorReference, indicatorType, + isDraggable, }) => ( = ({ data-test-subj="threat-match-indicator-details-indicator-type" eventId={eventId} field={INDICATOR_MATCHED_TYPE} + isDraggable={isDraggable} value={indicatorType} /> @@ -71,6 +74,7 @@ export const IndicatorDetails: React.FC = ({ data-test-subj="threat-match-indicator-details-indicator-dataset" eventId={eventId} field={INDICATOR_DATASET} + isDraggable={isDraggable} value={indicatorDataset} /> @@ -92,6 +96,7 @@ export const IndicatorDetails: React.FC = ({ data-test-subj="threat-match-indicator-details-indicator-provider" eventId={eventId} field={INDICATOR_PROVIDER} + isDraggable={isDraggable} value={indicatorProvider} /> @@ -108,6 +113,7 @@ export const IndicatorDetails: React.FC = ({ data-test-subj="threat-match-indicator-details-indicator-reference" eventId={eventId} fieldName={INDICATOR_REFERENCE} + isDraggable={isDraggable} value={indicatorReference} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/match_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/match_details.tsx index 2195421301d31..31c5065cde59a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/match_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/match_details.tsx @@ -16,6 +16,7 @@ import { HorizontalSpacer } from './helpers'; interface MatchDetailsProps { contextId: string; eventId: string; + isDraggable?: boolean; sourceField: string; sourceValue: string; } @@ -23,6 +24,7 @@ interface MatchDetailsProps { export const MatchDetails: React.FC = ({ contextId, eventId, + isDraggable, sourceField, sourceValue, }) => ( @@ -40,6 +42,7 @@ export const MatchDetails: React.FC = ({ data-test-subj="threat-match-details-source-field" eventId={eventId} field={INDICATOR_MATCHED_FIELD} + isDraggable={isDraggable} value={sourceField} /> @@ -57,6 +60,7 @@ export const MatchDetails: React.FC = ({ data-test-subj="threat-match-details-source-value" eventId={eventId} field={sourceField} + isDraggable={isDraggable} value={sourceValue} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx index ba5b0127df526..94ed19e218d74 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx @@ -28,6 +28,7 @@ export interface ThreatMatchRowProps { indicatorProvider: string | undefined; indicatorReference: string | undefined; indicatorType: string | undefined; + isDraggable?: boolean; sourceField: string; sourceValue: string; } @@ -36,10 +37,12 @@ export const ThreatMatchRow = ({ contextId, data, eventId, + isDraggable, }: { contextId: string; data: Fields; eventId: string; + isDraggable?: boolean; }) => { const props = { contextId, @@ -48,6 +51,7 @@ export const ThreatMatchRow = ({ indicatorReference: get(data, EVENT_REFERENCE)[0] as string | undefined, indicatorProvider: get(data, PROVIDER)[0] as string | undefined, indicatorType: get(data, MATCHED_TYPE)[0] as string | undefined, + isDraggable, sourceField: get(data, MATCHED_FIELD)[0] as string, sourceValue: get(data, MATCHED_ATOMIC)[0] as string, }; @@ -62,6 +66,7 @@ export const ThreatMatchRowView = ({ indicatorProvider, indicatorReference, indicatorType, + isDraggable, sourceField, sourceValue, }: ThreatMatchRowProps) => { @@ -76,6 +81,7 @@ export const ThreatMatchRowView = ({ @@ -88,6 +94,7 @@ export const ThreatMatchRowView = ({ indicatorProvider={indicatorProvider} indicatorReference={indicatorReference} indicatorType={indicatorType} + isDraggable={isDraggable} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.test.tsx index 6687179e5b887..78972442f5018 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.test.tsx @@ -56,6 +56,7 @@ describe('threatMatchRowRenderer', () => { const children = threatMatchRowRenderer.renderRow({ browserFields: {}, data: threatMatchData, + isDraggable: true, timelineId: 'test', }); const wrapper = shallow({children}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx index f6feb6dd1b126..d2c1f09d903c1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx @@ -20,7 +20,7 @@ const SpacedContainer = styled.div` margin: ${({ theme }) => theme.eui.paddingSizes.s} 0; `; -export const ThreatMatchRows: RowRenderer['renderRow'] = ({ data, timelineId }) => { +export const ThreatMatchRows: RowRenderer['renderRow'] = ({ data, isDraggable, timelineId }) => { const indicators = get(data, 'threat.indicator') as Fields[]; const eventId = get(data, ID_FIELD_NAME); @@ -31,7 +31,12 @@ export const ThreatMatchRows: RowRenderer['renderRow'] = ({ data, timelineId }) const contextId = `threat-match-row-${timelineId}-${eventId}-${index}`; return ( - + {index < indicators.length - 1 && } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.tsx index 3a53db2196d8c..90d68eceb7fb3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.tsx @@ -20,46 +20,50 @@ interface Props { browserFields: BrowserFields; contextId: string; data: Ecs; + isDraggable?: boolean; timelineId: string; } -export const DnsRequestEventDetails = React.memo(({ data, contextId, timelineId }) => { - const dnsQuestionName: string | null | undefined = get('dns.question.name[0]', data); - const dnsQuestionType: string | null | undefined = get('dns.question.type[0]', data); - const dnsResolvedIp: string | null | undefined = get('dns.resolved_ip[0]', data); - const dnsResponseCode: string | null | undefined = get('dns.response_code[0]', data); - const eventCode: string | null | undefined = get('event.code[0]', data); - const hostName: string | null | undefined = get('host.name[0]', data); - const id = data._id; - const processExecutable: string | null | undefined = get('process.executable[0]', data); - const processName: string | null | undefined = get('process.name[0]', data); - const processPid: number | null | undefined = get('process.pid[0]', data); - const userDomain: string | null | undefined = get('user.domain[0]', data); - const userName: string | null | undefined = get('user.name[0]', data); - const winlogEventId: string | null | undefined = get('winlog.event_id[0]', data); +export const DnsRequestEventDetails = React.memo( + ({ data, contextId, isDraggable, timelineId }) => { + const dnsQuestionName: string | null | undefined = get('dns.question.name[0]', data); + const dnsQuestionType: string | null | undefined = get('dns.question.type[0]', data); + const dnsResolvedIp: string | null | undefined = get('dns.resolved_ip[0]', data); + const dnsResponseCode: string | null | undefined = get('dns.response_code[0]', data); + const eventCode: string | null | undefined = get('event.code[0]', data); + const hostName: string | null | undefined = get('host.name[0]', data); + const id = data._id; + const processExecutable: string | null | undefined = get('process.executable[0]', data); + const processName: string | null | undefined = get('process.name[0]', data); + const processPid: number | null | undefined = get('process.pid[0]', data); + const userDomain: string | null | undefined = get('user.domain[0]', data); + const userName: string | null | undefined = get('user.name[0]', data); + const winlogEventId: string | null | undefined = get('winlog.event_id[0]', data); - return ( -
- - - -
- ); -}); + return ( +
+ + + +
+ ); + } +); DnsRequestEventDetails.displayName = 'DnsRequestEventDetails'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx index 549abcf6a6d35..ff85336bd47f8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx @@ -24,6 +24,7 @@ interface Props { eventCode: string | null | undefined; hostName: string | null | undefined; id: string; + isDraggable?: boolean; processExecutable: string | null | undefined; processName: string | null | undefined; processPid: number | null | undefined; @@ -42,6 +43,7 @@ export const DnsRequestEventDetailsLine = React.memo( eventCode, hostName, id, + isDraggable, processExecutable, processName, processPid, @@ -56,6 +58,7 @@ export const DnsRequestEventDetailsLine = React.memo( contextId={contextId} eventId={id} hostName={hostName} + isDraggable={isDraggable} userDomain={userDomain} userName={userName} workingDirectory={undefined} @@ -71,6 +74,7 @@ export const DnsRequestEventDetailsLine = React.memo( contextId={contextId} eventId={id} field="dns.question.name" + isDraggable={isDraggable} value={dnsQuestionName} />
@@ -87,6 +91,7 @@ export const DnsRequestEventDetailsLine = React.memo( contextId={contextId} eventId={id} field="dns.question.type" + isDraggable={isDraggable} value={dnsQuestionType} />
@@ -103,6 +108,7 @@ export const DnsRequestEventDetailsLine = React.memo( contextId={contextId} eventId={id} field="dns.resolved_ip" + isDraggable={isDraggable} value={dnsResolvedIp} /> @@ -122,6 +128,7 @@ export const DnsRequestEventDetailsLine = React.memo( contextId={contextId} eventId={id} field="dns.response_code" + isDraggable={isDraggable} value={dnsResponseCode} /> @@ -141,6 +148,7 @@ export const DnsRequestEventDetailsLine = React.memo( endgamePid={undefined} endgameProcessName={undefined} eventId={id} + isDraggable={isDraggable} processPid={processPid} processName={processName} processExecutable={processExecutable} @@ -155,6 +163,7 @@ export const DnsRequestEventDetailsLine = React.memo( contextId={contextId} eventId={id} field="event.code" + isDraggable={isDraggable} value={eventCode} /> @@ -165,6 +174,7 @@ export const DnsRequestEventDetailsLine = React.memo( eventId={id} iconType="logoWindows" field="winlog.event_id" + isDraggable={isDraggable} value={winlogEventId} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx index 8e2335a2f149b..db568726f1b20 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx @@ -60,6 +60,7 @@ export const emptyColumnRenderer: ColumnRenderer = { kqlQuery: '', and: [], }} + isDraggable={isDraggable} key={`empty-column-renderer-draggable-wrapper-${timelineId}-${columnName}-${eventId}-${field.id}`} render={(dataProvider, _, snapshot) => snapshot.isDragging ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx index 515db45e9fcd4..8f39cf933570f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx @@ -20,65 +20,75 @@ interface Props { browserFields: BrowserFields; contextId: string; data: Ecs; + isDraggable?: boolean; timelineId: string; } -export const EndgameSecurityEventDetails = React.memo(({ data, contextId, timelineId }) => { - const endgameLogonType: number | null | undefined = get('endgame.logon_type[0]', data); - const endgameSubjectDomainName: string | null | undefined = get( - 'endgame.subject_domain_name[0]', - data - ); - const endgameSubjectLogonId: string | null | undefined = get('endgame.subject_logon_id[0]', data); - const endgameSubjectUserName: string | null | undefined = get( - 'endgame.subject_user_name[0]', - data - ); - const endgameTargetLogonId: string | null | undefined = get('endgame.target_logon_id[0]', data); - const endgameTargetDomainName: string | null | undefined = get( - 'endgame.target_domain_name[0]', - data - ); - const endgameTargetUserName: string | null | undefined = get('endgame.target_user_name[0]', data); - const eventAction: string | null | undefined = get('event.action[0]', data); - const eventCode: string | null | undefined = get('event.code[0]', data); - const eventOutcome: string | null | undefined = get('event.outcome[0]', data); - const hostName: string | null | undefined = get('host.name[0]', data); - const id = data._id; - const processExecutable: string | null | undefined = get('process.executable[0]', data); - const processName: string | null | undefined = get('process.name[0]', data); - const processPid: number | null | undefined = get('process.pid[0]', data); - const userDomain: string | null | undefined = get('user.domain[0]', data); - const userName: string | null | undefined = get('user.name[0]', data); - const winlogEventId: string | null | undefined = get('winlog.event_id[0]', data); +export const EndgameSecurityEventDetails = React.memo( + ({ data, contextId, isDraggable, timelineId }) => { + const endgameLogonType: number | null | undefined = get('endgame.logon_type[0]', data); + const endgameSubjectDomainName: string | null | undefined = get( + 'endgame.subject_domain_name[0]', + data + ); + const endgameSubjectLogonId: string | null | undefined = get( + 'endgame.subject_logon_id[0]', + data + ); + const endgameSubjectUserName: string | null | undefined = get( + 'endgame.subject_user_name[0]', + data + ); + const endgameTargetLogonId: string | null | undefined = get('endgame.target_logon_id[0]', data); + const endgameTargetDomainName: string | null | undefined = get( + 'endgame.target_domain_name[0]', + data + ); + const endgameTargetUserName: string | null | undefined = get( + 'endgame.target_user_name[0]', + data + ); + const eventAction: string | null | undefined = get('event.action[0]', data); + const eventCode: string | null | undefined = get('event.code[0]', data); + const eventOutcome: string | null | undefined = get('event.outcome[0]', data); + const hostName: string | null | undefined = get('host.name[0]', data); + const id = data._id; + const processExecutable: string | null | undefined = get('process.executable[0]', data); + const processName: string | null | undefined = get('process.name[0]', data); + const processPid: number | null | undefined = get('process.pid[0]', data); + const userDomain: string | null | undefined = get('user.domain[0]', data); + const userName: string | null | undefined = get('user.name[0]', data); + const winlogEventId: string | null | undefined = get('winlog.event_id[0]', data); - return ( -
- - - -
- ); -}); + return ( +
+ + + +
+ ); + } +); EndgameSecurityEventDetails.displayName = 'EndgameSecurityEventDetails'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx index aba6f7346271d..7e5a6dd08765b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx @@ -38,6 +38,7 @@ interface Props { eventOutcome: string | null | undefined; hostName: string | null | undefined; id: string; + isDraggable?: boolean; processExecutable: string | null | undefined; processName: string | null | undefined; processPid: number | null | undefined; @@ -61,6 +62,7 @@ export const EndgameSecurityEventDetailsLine = React.memo( eventOutcome, hostName, id, + isDraggable, processExecutable, processName, processPid, @@ -95,6 +97,7 @@ export const EndgameSecurityEventDetailsLine = React.memo( eventId={id} hostName={hostName} hostNameSeparator={hostNameSeparator} + isDraggable={isDraggable} userDomain={domain} userDomainField={userDomainField} userName={user} @@ -116,6 +119,7 @@ export const EndgameSecurityEventDetailsLine = React.memo( contextId={contextId} eventId={id} field="endgame.logon_type" + isDraggable={isDraggable} queryValue={String(endgameLogonType)} value={`${endgameLogonType} - ${getHumanReadableLogonType(endgameLogonType)}`} /> @@ -136,6 +140,7 @@ export const EndgameSecurityEventDetailsLine = React.memo( contextId={contextId} eventId={id} field="endgame.target_logon_id" + isDraggable={isDraggable} value={endgameTargetLogonId} /> @@ -155,6 +160,7 @@ export const EndgameSecurityEventDetailsLine = React.memo( endgamePid={undefined} endgameProcessName={undefined} eventId={id} + isDraggable={isDraggable} processPid={processPid} processName={processName} processExecutable={processExecutable} @@ -176,6 +182,7 @@ export const EndgameSecurityEventDetailsLine = React.memo( contextId={contextId} eventId={id} field="endgame.subject_user_name" + isDraggable={isDraggable} iconType="user" value={endgameSubjectUserName} /> @@ -197,6 +204,7 @@ export const EndgameSecurityEventDetailsLine = React.memo( contextId={contextId} eventId={id} field="endgame.subject_domain_name" + isDraggable={isDraggable} value={endgameSubjectDomainName} /> @@ -216,6 +224,7 @@ export const EndgameSecurityEventDetailsLine = React.memo( contextId={contextId} eventId={id} field="endgame.subject_logon_id" + isDraggable={isDraggable} value={endgameSubjectLogonId} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.tsx index 7ac9fe290893f..1f1862daa4e55 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.tsx @@ -15,12 +15,13 @@ interface Props { contextId: string; endgameExitCode: string | null | undefined; eventId: string; + isDraggable?: boolean; processExitCode: number | null | undefined; text: string | null | undefined; } export const ExitCodeDraggable = React.memo( - ({ contextId, endgameExitCode, eventId, processExitCode, text }) => { + ({ contextId, endgameExitCode, eventId, isDraggable, processExitCode, text }) => { if (isNillEmptyOrNotFinite(processExitCode) && isNillEmptyOrNotFinite(endgameExitCode)) { return null; } @@ -39,6 +40,7 @@ export const ExitCodeDraggable = React.memo( contextId={contextId} eventId={eventId} field="process.exit_code" + isDraggable={isDraggable} value={`${processExitCode}`} /> @@ -50,6 +52,7 @@ export const ExitCodeDraggable = React.memo( contextId={contextId} eventId={eventId} field="endgame.exit_code" + isDraggable={isDraggable} value={endgameExitCode} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_draggable.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_draggable.tsx index 703b38e627e55..7ff5a0f73ab30 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_draggable.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_draggable.tsx @@ -20,6 +20,7 @@ interface Props { fileName: string | null | undefined; filePath: string | null | undefined; fileExtOriginalPath: string | null | undefined; + isDraggable?: boolean; } export const FileDraggable = React.memo( @@ -31,6 +32,7 @@ export const FileDraggable = React.memo( fileExtOriginalPath, fileName, filePath, + isDraggable, }) => { if ( isNillEmptyOrNotFinite(fileName) && @@ -52,6 +54,7 @@ export const FileDraggable = React.memo( contextId={contextId} eventId={eventId} field="file.name" + isDraggable={isDraggable} value={fileName} iconType="document" /> @@ -62,6 +65,7 @@ export const FileDraggable = React.memo( contextId={contextId} eventId={eventId} field="endgame.file_name" + isDraggable={isDraggable} value={endgameFileName} iconType="document" /> @@ -80,6 +84,7 @@ export const FileDraggable = React.memo( contextId={contextId} eventId={eventId} field="file.path" + isDraggable={isDraggable} value={filePath} iconType="document" /> @@ -90,6 +95,7 @@ export const FileDraggable = React.memo( contextId={contextId} eventId={eventId} field="endgame.file_path" + isDraggable={isDraggable} value={endgameFilePath} iconType="document" /> @@ -106,6 +112,7 @@ export const FileDraggable = React.memo( contextId={contextId} eventId={eventId} field="file.Ext.original.path" + isDraggable={isDraggable} value={fileExtOriginalPath} iconType="document" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_hash.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_hash.tsx index 9e624ba17c921..13b024e0a5359 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_hash.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_hash.tsx @@ -21,9 +21,10 @@ interface Props { contextId: string; eventId: string; fileHashSha256: string | null | undefined; + isDraggable?: boolean; } -export const FileHash = React.memo(({ contextId, eventId, fileHashSha256 }) => { +export const FileHash = React.memo(({ contextId, eventId, fileHashSha256, isDraggable }) => { if (isNillEmptyOrNotFinite(fileHashSha256)) { return null; } @@ -35,6 +36,7 @@ export const FileHash = React.memo(({ contextId, eventId, fileHashSha256 contextId={contextId} eventId={eventId} field="file.hash.sha256" + isDraggable={isDraggable} iconType="number" value={fileHashSha256} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx index aa6c7beb9139e..06ed901110962 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx @@ -86,6 +86,7 @@ const FormattedFieldValueComponent: React.FC<{ @@ -214,6 +215,7 @@ const FormattedFieldValueComponent: React.FC<{ = ({ @@ -95,6 +96,7 @@ export const RenderRuleName: React.FC = ({ @@ -150,6 +152,7 @@ export const renderEventModule = ({ @@ -218,6 +221,7 @@ export const renderUrl = ({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx index 104550f138f16..6b76aba92678d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx @@ -54,6 +54,7 @@ describe('get_column_renderer', () => { const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: nonSuricata, + isDraggable: true, timelineId: 'test', }); @@ -66,6 +67,7 @@ describe('get_column_renderer', () => { const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: nonSuricata, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( @@ -81,6 +83,7 @@ describe('get_column_renderer', () => { const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: suricata, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( @@ -99,6 +102,7 @@ describe('get_column_renderer', () => { const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: suricata, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( @@ -117,6 +121,7 @@ describe('get_column_renderer', () => { const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: zeek, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( @@ -135,6 +140,7 @@ describe('get_column_renderer', () => { const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: system, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( @@ -153,6 +159,7 @@ describe('get_column_renderer', () => { const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: auditd, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx index abd4731ec4b66..060b539950d83 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx @@ -93,6 +93,7 @@ const HostNameComponent: React.FC = ({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_working_dir.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_working_dir.tsx index de307d1af7f93..fef9a5d5c0201 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_working_dir.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_working_dir.tsx @@ -17,10 +17,11 @@ interface Props { eventId: string; hostName: string | null | undefined; workingDirectory: string | null | undefined; + isDraggable?: boolean; } export const HostWorkingDir = React.memo( - ({ contextId, eventId, hostName, workingDirectory }) => ( + ({ contextId, eventId, hostName, workingDirectory, isDraggable }) => ( <> ( eventId={eventId} field="host.name" value={hostName} + isDraggable={isDraggable} /> {workingDirectory != null && ( @@ -42,6 +44,7 @@ export const HostWorkingDir = React.memo( field="process.working_directory" value={workingDirectory} iconType="folderOpen" + isDraggable={isDraggable} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow.tsx index 18f56d8b03066..d6ea939c966ac 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow.tsx @@ -60,52 +60,58 @@ import { interface NetflowRendererProps { data: Ecs; timelineId: string; + isDraggable?: boolean; } -export const NetflowRenderer = React.memo(({ data, timelineId }) => ( - -)); +export const NetflowRenderer = React.memo( + ({ data, timelineId, isDraggable }) => ( + + ) +); NetflowRenderer.displayName = 'NetflowRenderer'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap index d7bdacbcc61ef..a9ecbe8428aee 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap @@ -67,6 +67,7 @@ exports[`netflowRowRenderer renders correctly against snapshot 1`] = ` "2018-11-12T19:03:25.836Z", ] } + isDraggable={true} networkBytes={ Array [ 100, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx index fc97624dbfc96..01e05bbc365e9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx @@ -38,6 +38,7 @@ describe('netflowRowRenderer', () => { const children = netflowRowRenderer.renderRow({ browserFields, data: getMockNetflowData(), + isDraggable: true, timelineId: 'test', }); @@ -107,6 +108,7 @@ describe('netflowRowRenderer', () => { const children = netflowRowRenderer.renderRow({ browserFields: mockBrowserFields, data: getMockNetflowData(), + isDraggable: true, timelineId: 'test', }); const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx index 35406dce6ff72..272912b855af0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx @@ -90,7 +90,7 @@ export const netflowRowRenderer: RowRenderer = { isInstance: (ecs) => eventCategoryMatches(get(EVENT_CATEGORY_FIELD, ecs)) || eventActionMatches(get(EVENT_ACTION_FIELD, ecs)), - renderRow: ({ data, timelineId }) => ( + renderRow: ({ data, isDraggable, timelineId }) => (
( contextId, endgameParentProcessName, eventId, + isDraggable, processParentName, processParentPid, processPpid, @@ -56,6 +58,7 @@ export const ParentProcessDraggable = React.memo( contextId={contextId} eventId={eventId} field="process.parent.name" + isDraggable={isDraggable} value={processParentName} /> @@ -67,6 +70,7 @@ export const ParentProcessDraggable = React.memo( contextId={contextId} eventId={eventId} field="endgame.parent_process_name" + isDraggable={isDraggable} value={endgameParentProcessName} /> @@ -78,6 +82,7 @@ export const ParentProcessDraggable = React.memo( contextId={contextId} eventId={eventId} field="process.parent.pid" + isDraggable={isDraggable} queryValue={String(processParentPid)} value={`(${String(processParentPid)})`} /> @@ -90,6 +95,7 @@ export const ParentProcessDraggable = React.memo( contextId={contextId} eventId={eventId} field="process.ppid" + isDraggable={isDraggable} queryValue={String(processPpid)} value={`(${String(processPpid)})`} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.test.tsx index 666fb254aaa2c..f28c72253a4e7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.test.tsx @@ -24,6 +24,7 @@ describe('plain_row_renderer', () => { const children = plainRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockDatum, + isDraggable: true, timelineId: 'test', }); const wrapper = shallow({children}); @@ -38,6 +39,7 @@ describe('plain_row_renderer', () => { const children = plainRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockDatum, + isDraggable: true, timelineId: 'test', }); const wrapper = mount({children}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_draggable.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_draggable.tsx index 705eff8873204..db7e3ae6f06c9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_draggable.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_draggable.tsx @@ -21,6 +21,7 @@ interface Props { processExecutable: string | undefined | null; processPid: number | undefined | null; processName: string | undefined | null; + isDraggable?: boolean; } export const ProcessDraggable = React.memo( @@ -32,6 +33,7 @@ export const ProcessDraggable = React.memo( processExecutable, processName, processPid, + isDraggable, }) => { if ( isNillEmptyOrNotFinite(processName) && @@ -53,6 +55,7 @@ export const ProcessDraggable = React.memo( field="process.name" value={processName} iconType="console" + isDraggable={isDraggable} /> ) : !isNillEmptyOrNotFinite(processExecutable) ? ( @@ -63,6 +66,7 @@ export const ProcessDraggable = React.memo( field="process.executable" value={processExecutable} iconType="console" + isDraggable={isDraggable} /> ) : !isNillEmptyOrNotFinite(endgameProcessName) ? ( @@ -73,6 +77,7 @@ export const ProcessDraggable = React.memo( field="endgame.process_name" value={endgameProcessName} iconType="console" + isDraggable={isDraggable} /> ) : null} @@ -85,6 +90,7 @@ export const ProcessDraggable = React.memo( field="process.pid" queryValue={String(processPid)} value={`(${String(processPid)})`} + isDraggable={isDraggable} /> ) : !isNillEmptyOrNotFinite(endgamePid) ? ( @@ -95,6 +101,7 @@ export const ProcessDraggable = React.memo( field="endgame.pid" queryValue={String(endgamePid)} value={`(${String(endgamePid)})`} + isDraggable={isDraggable} /> ) : null} @@ -114,6 +121,7 @@ export const ProcessDraggableWithNonExistentProcess = React.memo( processExecutable, processName, processPid, + isDraggable, }) => { if ( endgamePid == null && @@ -133,6 +141,7 @@ export const ProcessDraggableWithNonExistentProcess = React.memo( processExecutable={processExecutable} processName={processName} processPid={processPid} + isDraggable={isDraggable} /> ); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_hash.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_hash.tsx index 32432afbf205c..dd4f588a14bb7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_hash.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_hash.tsx @@ -20,27 +20,31 @@ const HashFlexGroup = styled(EuiFlexGroup)` interface Props { contextId: string; eventId: string; + isDraggable?: boolean; processHashSha256: string | null | undefined; } -export const ProcessHash = React.memo(({ contextId, eventId, processHashSha256 }) => { - if (isNillEmptyOrNotFinite(processHashSha256)) { - return null; +export const ProcessHash = React.memo( + ({ contextId, eventId, isDraggable, processHashSha256 }) => { + if (isNillEmptyOrNotFinite(processHashSha256)) { + return null; + } + + return ( + + + + + + ); } - - return ( - - - - - - ); -}); +); ProcessHash.displayName = 'ProcessHash'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details.tsx index 0bfb03168019a..da31f75e2fa10 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details.tsx @@ -18,10 +18,11 @@ interface Props { browserFields: BrowserFields; contextId: string; data: Ecs; + isDraggable?: boolean; text: string; } -const RegistryEventDetailsComponent: React.FC = ({ contextId, data, text }) => { +const RegistryEventDetailsComponent: React.FC = ({ contextId, data, isDraggable, text }) => { const hostName: string | null | undefined = get('host.name[0]', data); const id = data._id; const processName: string | null | undefined = get('process.name[0]', data); @@ -41,6 +42,7 @@ const RegistryEventDetailsComponent: React.FC = ({ contextId, data, text contextId={contextId} hostName={hostName} id={id} + isDraggable={isDraggable} processName={processName} processPid={processPid} registryKey={registryKey} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details_line.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details_line.tsx index b85ae25ed2509..8d9f52da88fdd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details_line.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details_line.tsx @@ -19,6 +19,7 @@ interface Props { contextId: string; hostName: string | null | undefined; id: string; + isDraggable?: boolean; processName: string | null | undefined; processPid: number | null | undefined; registryKey: string | null | undefined; @@ -32,6 +33,7 @@ const RegistryEventDetailsLineComponent: React.FC = ({ contextId, hostName, id, + isDraggable, processName, processPid, registryKey, @@ -71,6 +73,7 @@ const RegistryEventDetailsLineComponent: React.FC = ({ contextId={contextId} eventId={id} hostName={hostName} + isDraggable={isDraggable} userDomain={userDomain} userName={userName} workingDirectory={undefined} @@ -86,6 +89,7 @@ const RegistryEventDetailsLineComponent: React.FC = ({ contextId={contextId} eventId={id} field="registry.key" + isDraggable={isDraggable} tooltipContent={registryKeyTooltipContent} value={registryKey} /> @@ -103,6 +107,7 @@ const RegistryEventDetailsLineComponent: React.FC = ({ contextId={contextId} eventId={id} field="registry.path" + isDraggable={isDraggable} tooltipContent={registryPathTooltipContent} value={registryPath} /> @@ -120,6 +125,7 @@ const RegistryEventDetailsLineComponent: React.FC = ({ endgamePid={undefined} endgameProcessName={undefined} eventId={id} + isDraggable={isDraggable} processPid={processPid} processName={processName} processExecutable={undefined} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx index 126bfae996ef7..09248b832490a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx @@ -42,6 +42,7 @@ const RuleStatusComponent: React.FC = ({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap index 2934d35dc184d..eeb8786b2cfa3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap @@ -525,6 +525,7 @@ exports[`suricata_row_renderer renders correctly against snapshot 1`] = ` }, } } + isDraggable={true} timelineId="test" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.tsx index 82eb11d455543..f096cd906f619 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.tsx @@ -26,8 +26,9 @@ Details.displayName = 'Details'; export const SuricataDetails = React.memo<{ browserFields: BrowserFields; data: Ecs; + isDraggable?: boolean; timelineId: string; -}>(({ data, timelineId }) => { +}>(({ data, isDraggable, timelineId }) => { const signature: string | null | undefined = get('suricata.eve.alert.signature[0]', data); const signatureId: number | null | undefined = get('suricata.eve.alert.signature_id[0]', data); @@ -37,12 +38,13 @@ export const SuricataDetails = React.memo<{ - +
); } else { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx index 998233b2278c9..661fc562cc34c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx @@ -45,6 +45,7 @@ describe('suricata_row_renderer', () => { const children = suricataRowRenderer.renderRow({ browserFields: mockBrowserFields, data: nonSuricata, + isDraggable: true, timelineId: 'test', }); @@ -64,6 +65,7 @@ describe('suricata_row_renderer', () => { const children = suricataRowRenderer.renderRow({ browserFields: mockBrowserFields, data: suricata, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( @@ -81,6 +83,7 @@ describe('suricata_row_renderer', () => { const children = suricataRowRenderer.renderRow({ browserFields: mockBrowserFields, data: suricata, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx index aa482926bf007..0faa6a4fbba74 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx @@ -21,9 +21,14 @@ export const suricataRowRenderer: RowRenderer = { const module: string | null | undefined = get('event.module[0]', ecs); return module != null && module.toLowerCase() === 'suricata'; }, - renderRow: ({ browserFields, data, timelineId }) => ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( - + ), }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx index a4e16c66f4fef..2a5b57d77498f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx @@ -57,65 +57,69 @@ export const Tokens = React.memo<{ tokens: string[] }>(({ tokens }) => ( Tokens.displayName = 'Tokens'; -export const DraggableSignatureId = React.memo<{ id: string; signatureId: number }>( - ({ id, signatureId }) => { - const dataProviderProp = useMemo( - () => ({ - and: [], - enabled: true, - id: escapeDataProviderId(`suricata-draggable-signature-id-${id}-sig-${signatureId}`), - name: String(signatureId), - excluded: false, - kqlQuery: '', - queryMatch: { - field: SURICATA_SIGNATURE_ID_FIELD_NAME, - value: signatureId, - operator: IS_OPERATOR as QueryOperator, - }, - }), - [id, signatureId] - ); - - const render = useCallback( - (dataProvider, _, snapshot) => - snapshot.isDragging ? ( - - - - ) : ( - - - {signatureId} - - - ), - [signatureId] - ); - - return ( - - - - ); - } -); +export const DraggableSignatureId = React.memo<{ + id: string; + isDraggable?: boolean; + signatureId: number; +}>(({ id, isDraggable, signatureId }) => { + const dataProviderProp = useMemo( + () => ({ + and: [], + enabled: true, + id: escapeDataProviderId(`suricata-draggable-signature-id-${id}-sig-${signatureId}`), + name: String(signatureId), + excluded: false, + kqlQuery: '', + queryMatch: { + field: SURICATA_SIGNATURE_ID_FIELD_NAME, + value: signatureId, + operator: IS_OPERATOR as QueryOperator, + }, + }), + [id, signatureId] + ); + + const render = useCallback( + (dataProvider, _, snapshot) => + snapshot.isDragging ? ( + + + + ) : ( + + + {signatureId} + + + ), + [signatureId] + ); + + return ( + + + + ); +}); DraggableSignatureId.displayName = 'DraggableSignatureId'; export const SuricataSignature = React.memo<{ contextId: string; id: string; + isDraggable?: boolean; signature: string; signatureId: number; -}>(({ contextId, id, signature, signatureId }) => { +}>(({ contextId, id, isDraggable, signature, signatureId }) => { const tokens = getBeginningTokens(signature); return ( @@ -124,6 +128,7 @@ export const SuricataSignature = React.memo<{ data-test-subj="draggable-signature-link" field={SURICATA_SIGNATURE_FIELD_NAME} id={`suricata-signature-default-draggable-${contextId}-${id}-${SURICATA_SIGNATURE_FIELD_NAME}`} + isDraggable={isDraggable} value={signature} >
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap index d2c405a46acf8..15443058f434e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap @@ -56,6 +56,7 @@ exports[`GenericRowRenderer #createGenericFileRowRenderer renders correctly agai }, } } + isDraggable={true} text="some text" timelineId="test" /> @@ -119,6 +120,7 @@ exports[`GenericRowRenderer #createGenericSystemRowRenderer renders correctly ag }, } } + isDraggable={true} text="some text" timelineId="test" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/auth_ssh.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/auth_ssh.tsx index 4dcb90637a817..7de03a2ae2356 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/auth_ssh.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/auth_ssh.tsx @@ -13,35 +13,40 @@ import { TokensFlexItem } from '../helpers'; interface Props { contextId: string; eventId: string; + isDraggable?: boolean; sshSignature: string | null | undefined; sshMethod: string | null | undefined; } -export const AuthSsh = React.memo(({ contextId, eventId, sshSignature, sshMethod }) => ( - <> - {sshSignature != null && ( - - - - )} - {sshMethod != null && ( - - - - )} - -)); +export const AuthSsh = React.memo( + ({ contextId, eventId, isDraggable, sshSignature, sshMethod }) => ( + <> + {sshSignature != null && ( + + + + )} + {sshMethod != null && ( + + + + )} + + ) +); AuthSsh.displayName = 'AuthSsh'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.tsx index 19d5fd2a0dab1..2cf42ecc9c670 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.tsx @@ -27,6 +27,7 @@ interface Props { contextId: string; hostName: string | null | undefined; id: string; + isDraggable?: boolean; message: string | null | undefined; outcome: string | null | undefined; packageName: string | null | undefined; @@ -48,6 +49,7 @@ export const SystemGenericLine = React.memo( contextId, hostName, id, + isDraggable, message, outcome, packageName, @@ -68,9 +70,10 @@ export const SystemGenericLine = React.memo( @@ -82,6 +85,7 @@ export const SystemGenericLine = React.memo( endgamePid={undefined} endgameProcessName={undefined} eventId={id} + isDraggable={isDraggable} processPid={processPid} processName={processName} processExecutable={processExecutable} @@ -97,6 +101,7 @@ export const SystemGenericLine = React.memo( contextId={contextId} eventId={id} field="event.outcome" + isDraggable={isDraggable} queryValue={outcome} value={outcome} /> @@ -104,12 +109,14 @@ export const SystemGenericLine = React.memo( ( - ({ data, contextId, text, timelineId }) => { + ({ contextId, data, isDraggable, text, timelineId }) => { const id = data._id; const message: string | null = data.message != null ? data.message[0] : null; const hostName: string | null | undefined = get('host.name[0]', data); @@ -165,6 +173,7 @@ export const SystemGenericDetails = React.memo( contextId={contextId} hostName={hostName} id={id} + isDraggable={isDraggable} message={message} outcome={outcome} packageName={packageName} @@ -181,7 +190,7 @@ export const SystemGenericDetails = React.memo( workingDirectory={workingDirectory} /> - + ); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.tsx index 6df583656ff2d..ae31dbff7f063 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.tsx @@ -45,6 +45,7 @@ interface Props { filePath: string | null | undefined; hostName: string | null | undefined; id: string; + isDraggable?: boolean; message: string | null | undefined; outcome: string | null | undefined; packageName: string | null | undefined; @@ -87,6 +88,7 @@ export const SystemGenericFileLine = React.memo( filePath, hostName, id, + isDraggable, message, outcome, packageName, @@ -116,6 +118,7 @@ export const SystemGenericFileLine = React.memo( ( fileExtOriginalPath={fileExtOriginalPath} fileName={fileName} filePath={filePath} + isDraggable={isDraggable} /> )} {showVia(eventAction) && ( @@ -147,6 +151,7 @@ export const SystemGenericFileLine = React.memo( endgamePid={endgamePid} endgameProcessName={endgameProcessName} eventId={id} + isDraggable={isDraggable} processPid={processPid} processName={processName} processExecutable={processExecutable} @@ -157,6 +162,7 @@ export const SystemGenericFileLine = React.memo( contextId={contextId} endgameExitCode={endgameExitCode} eventId={id} + isDraggable={isDraggable} processExitCode={processExitCode} text={i18n.WITH_EXIT_CODE} /> @@ -165,6 +171,7 @@ export const SystemGenericFileLine = React.memo( contextId={contextId} endgameParentProcessName={endgameParentProcessName} eventId={id} + isDraggable={isDraggable} processParentName={processParentName} processParentPid={processParentPid} processPpid={processPpid} @@ -181,6 +188,7 @@ export const SystemGenericFileLine = React.memo( contextId={contextId} eventId={id} field="event.outcome" + isDraggable={isDraggable} queryValue={outcome} value={outcome} /> @@ -188,22 +196,34 @@ export const SystemGenericFileLine = React.memo( {!skipRedundantFileDetails && ( - + )} {!skipRedundantProcessDetails && ( - + )} {message != null && showMessage && ( @@ -226,8 +246,9 @@ SystemGenericFileLine.displayName = 'SystemGenericFileLine'; interface GenericDetailsProps { browserFields: BrowserFields; - data: Ecs; contextId: string; + data: Ecs; + isDraggable?: boolean; showMessage?: boolean; skipRedundantFileDetails?: boolean; skipRedundantProcessDetails?: boolean; @@ -237,8 +258,9 @@ interface GenericDetailsProps { export const SystemGenericFileDetails = React.memo( ({ - data, contextId, + data, + isDraggable, showMessage = true, skipRedundantFileDetails = false, skipRedundantProcessDetails = false, @@ -323,9 +345,10 @@ export const SystemGenericFileDetails = React.memo( sshSignature={sshSignature} sshMethod={sshMethod} outcome={outcome} + isDraggable={isDraggable} /> - + ); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx index 6f5b225f0690b..516d279765904 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx @@ -118,6 +118,7 @@ describe('GenericRowRenderer', () => { const children = connectedToRenderer.renderRow({ browserFields, data: system, + isDraggable: true, timelineId: 'test', }); @@ -147,6 +148,7 @@ describe('GenericRowRenderer', () => { const children = connectedToRenderer.renderRow({ browserFields: mockBrowserFields, data: system, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( @@ -180,6 +182,7 @@ describe('GenericRowRenderer', () => { const children = fileToRenderer.renderRow({ browserFields, data: systemFile, + isDraggable: true, timelineId: 'test', }); @@ -208,6 +211,7 @@ describe('GenericRowRenderer', () => { const children = fileToRenderer.renderRow({ browserFields: mockBrowserFields, data: systemFile, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( @@ -239,6 +243,7 @@ describe('GenericRowRenderer', () => { endpointAlertsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFileCreationMalwarePreventionAlert, + isDraggable: true, timelineId: 'test', })} @@ -266,6 +271,7 @@ describe('GenericRowRenderer', () => { endpointAlertsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFileCreationMalwareDetectionAlert, + isDraggable: true, timelineId: 'test', })} @@ -295,6 +301,7 @@ describe('GenericRowRenderer', () => { endpointAlertsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFilesEncryptedRansomwarePreventionAlert, + isDraggable: true, timelineId: 'test', })} @@ -324,6 +331,7 @@ describe('GenericRowRenderer', () => { endpointAlertsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFilesEncryptedRansomwareDetectionAlert, + isDraggable: true, timelineId: 'test', })} @@ -353,6 +361,7 @@ describe('GenericRowRenderer', () => { endpointAlertsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFileModificationMalwarePreventionAlert, + isDraggable: true, timelineId: 'test', })} @@ -382,6 +391,7 @@ describe('GenericRowRenderer', () => { endpointAlertsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFileModificationMalwareDetectionAlert, + isDraggable: true, timelineId: 'test', })} @@ -409,6 +419,7 @@ describe('GenericRowRenderer', () => { endpointAlertsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFileRenameMalwarePreventionAlert, + isDraggable: true, timelineId: 'test', })} @@ -436,6 +447,7 @@ describe('GenericRowRenderer', () => { endpointAlertsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFileRenameMalwareDetectionAlert, + isDraggable: true, timelineId: 'test', })} @@ -465,6 +477,7 @@ describe('GenericRowRenderer', () => { endpointAlertsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointProcessExecutionMalwarePreventionAlert, + isDraggable: true, timelineId: 'test', })} @@ -494,6 +507,7 @@ describe('GenericRowRenderer', () => { endpointAlertsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointProcessExecutionMalwareDetectionAlert, + isDraggable: true, timelineId: 'test', })} @@ -521,6 +535,7 @@ describe('GenericRowRenderer', () => { endpointProcessStartRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointProcessExecEvent, + isDraggable: true, timelineId: 'test', })} @@ -546,6 +561,7 @@ describe('GenericRowRenderer', () => { endpointProcessStartRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointProcessForkEvent, + isDraggable: true, timelineId: 'test', })} @@ -571,6 +587,7 @@ describe('GenericRowRenderer', () => { endpointProcessStartRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointProcessStartEvent, + isDraggable: true, timelineId: 'test', })} @@ -599,6 +616,7 @@ describe('GenericRowRenderer', () => { endgameProcessCreationEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameCreationEvent, + isDraggable: true, timelineId: 'test', })} @@ -624,6 +642,7 @@ describe('GenericRowRenderer', () => { endpointProcessEndRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointProcessEndEvent, + isDraggable: true, timelineId: 'test', })} @@ -652,6 +671,7 @@ describe('GenericRowRenderer', () => { endgameProcessTerminationEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameTerminationEvent, + isDraggable: true, timelineId: 'test', })} @@ -680,6 +700,7 @@ describe('GenericRowRenderer', () => { endgameProcessCreationEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameCreationEvent, + isDraggable: true, timelineId: 'test', })} @@ -710,6 +731,7 @@ describe('GenericRowRenderer', () => { endgameProcessCreationEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameCreationEvent, + isDraggable: true, timelineId: 'test', })} @@ -740,6 +762,7 @@ describe('GenericRowRenderer', () => { endgameProcessCreationEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameCreationEvent, + isDraggable: true, timelineId: 'test', })} @@ -765,6 +788,7 @@ describe('GenericRowRenderer', () => { endpointFileCreationRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFileCreationEvent, + isDraggable: true, timelineId: 'test', })} @@ -793,6 +817,7 @@ describe('GenericRowRenderer', () => { endgameFileCreateEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameFileCreateEvent, + isDraggable: true, timelineId: 'test', })} @@ -818,6 +843,7 @@ describe('GenericRowRenderer', () => { endpointFileDeletionRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFileDeletionEvent, + isDraggable: true, timelineId: 'test', })} @@ -843,6 +869,7 @@ describe('GenericRowRenderer', () => { endpointFileModificationRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFileModificationEvent, + isDraggable: true, timelineId: 'test', })} @@ -868,6 +895,7 @@ describe('GenericRowRenderer', () => { endpointFileOverwriteRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFileOverwriteEvent, + isDraggable: true, timelineId: 'test', })} @@ -893,6 +921,7 @@ describe('GenericRowRenderer', () => { endpointFileRenameRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointFileRenameEvent, + isDraggable: true, timelineId: 'test', })} @@ -921,6 +950,7 @@ describe('GenericRowRenderer', () => { endgameFileDeleteEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameFileDeleteEvent, + isDraggable: true, timelineId: 'test', })} @@ -949,6 +979,7 @@ describe('GenericRowRenderer', () => { fileCreatedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: fimFileCreatedEvent, + isDraggable: true, timelineId: 'test', })} @@ -975,6 +1006,7 @@ describe('GenericRowRenderer', () => { fileDeletedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: fimFileDeletedEvent, + isDraggable: true, timelineId: 'test', })} @@ -1003,6 +1035,7 @@ describe('GenericRowRenderer', () => { endgameFileCreateEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameFileCreateEvent, + isDraggable: true, timelineId: 'test', })} @@ -1033,6 +1066,7 @@ describe('GenericRowRenderer', () => { endgameFileCreateEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: endgameFileCreateEvent, + isDraggable: true, timelineId: 'test', })} @@ -1063,6 +1097,7 @@ describe('GenericRowRenderer', () => { fileCreatedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: fimFileCreatedEvent, + isDraggable: true, timelineId: 'test', })} @@ -1090,6 +1125,7 @@ describe('GenericRowRenderer', () => { endpointConnectionAcceptedRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointNetworkConnectionAcceptedEvent, + isDraggable: true, timelineId: 'test', })} @@ -1118,6 +1154,7 @@ describe('GenericRowRenderer', () => { endpointRegistryModificationRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointRegistryModificationEvent, + isDraggable: true, timelineId: 'test', })} @@ -1145,6 +1182,7 @@ describe('GenericRowRenderer', () => { endpointLibraryLoadRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointLibraryLoadEvent, + isDraggable: true, timelineId: 'test', })} @@ -1171,6 +1209,7 @@ describe('GenericRowRenderer', () => { endpointHttpRequestEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointNetworkHttpRequestEvent, + isDraggable: true, timelineId: 'test', })} @@ -1199,6 +1238,7 @@ describe('GenericRowRenderer', () => { endgameIpv4ConnectionAcceptEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: ipv4ConnectionAcceptEvent, + isDraggable: true, timelineId: 'test', })} @@ -1227,6 +1267,7 @@ describe('GenericRowRenderer', () => { endgameIpv6ConnectionAcceptEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: ipv6ConnectionAcceptEvent, + isDraggable: true, timelineId: 'test', })} @@ -1252,6 +1293,7 @@ describe('GenericRowRenderer', () => { endpointDisconnectReceivedRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointDisconnectReceivedEvent, + isDraggable: true, timelineId: 'test', })} @@ -1280,6 +1322,7 @@ describe('GenericRowRenderer', () => { endgameIpv4DisconnectReceivedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: ipv4DisconnectReceivedEvent, + isDraggable: true, timelineId: 'test', })} @@ -1308,6 +1351,7 @@ describe('GenericRowRenderer', () => { endgameIpv6DisconnectReceivedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: ipv6DisconnectReceivedEvent, + isDraggable: true, timelineId: 'test', })} @@ -1336,6 +1380,7 @@ describe('GenericRowRenderer', () => { socketOpenedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: socketOpenedEvent, + isDraggable: true, timelineId: 'test', })} @@ -1364,6 +1409,7 @@ describe('GenericRowRenderer', () => { socketClosedEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: socketClosedEvent, + isDraggable: true, timelineId: 'test', })} @@ -1392,6 +1438,7 @@ describe('GenericRowRenderer', () => { endgameIpv4ConnectionAcceptEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: ipv4ConnectionAcceptEvent, + isDraggable: true, timelineId: 'test', })} @@ -1413,6 +1460,7 @@ describe('GenericRowRenderer', () => { securityLogOnRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointSecurityLogOnSuccessEvent, + isDraggable: true, timelineId: 'test', })} @@ -1434,6 +1482,7 @@ describe('GenericRowRenderer', () => { securityLogOnRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointSecurityLogOnFailureEvent, + isDraggable: true, timelineId: 'test', })} @@ -1458,6 +1507,7 @@ describe('GenericRowRenderer', () => { userLogonEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: userLogonEvent, + isDraggable: true, timelineId: 'test', })} @@ -1482,6 +1532,7 @@ describe('GenericRowRenderer', () => { adminLogonEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: adminLogonEvent, + isDraggable: true, timelineId: 'test', })} @@ -1506,6 +1557,7 @@ describe('GenericRowRenderer', () => { explicitUserLogonEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: explicitUserLogonEvent, + isDraggable: true, timelineId: 'test', })} @@ -1527,6 +1579,7 @@ describe('GenericRowRenderer', () => { securityLogOffRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointSecurityLogOffEvent, + isDraggable: true, timelineId: 'test', })} @@ -1551,6 +1604,7 @@ describe('GenericRowRenderer', () => { userLogoffEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: userLogoffEvent, + isDraggable: true, timelineId: 'test', })} @@ -1575,6 +1629,7 @@ describe('GenericRowRenderer', () => { userLogonEventRowRenderer.renderRow({ browserFields: mockBrowserFields, data: userLogonEvent, + isDraggable: true, timelineId: 'test', })} @@ -1594,6 +1649,7 @@ describe('GenericRowRenderer', () => { dnsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointNetworkLookupRequestedEvent, + isDraggable: true, timelineId: 'test', })} @@ -1613,6 +1669,7 @@ describe('GenericRowRenderer', () => { dnsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockEndpointNetworkLookupResultEvent, + isDraggable: true, timelineId: 'test', })} @@ -1636,6 +1693,7 @@ describe('GenericRowRenderer', () => { dnsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: requestEvent, + isDraggable: true, timelineId: 'test', })} @@ -1659,6 +1717,7 @@ describe('GenericRowRenderer', () => { dnsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: dnsEvent, + isDraggable: true, timelineId: 'test', })} @@ -1688,6 +1747,7 @@ describe('GenericRowRenderer', () => { dnsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: requestEvent, + isDraggable: true, timelineId: 'test', })} @@ -1715,6 +1775,7 @@ describe('GenericRowRenderer', () => { dnsRowRenderer.renderRow({ browserFields: mockBrowserFields, data: requestEvent, + isDraggable: true, timelineId: 'test', })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx index c6845d7d672d2..b1027bf12b7d2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx @@ -40,12 +40,13 @@ export const createGenericSystemRowRenderer = ({ action.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, timelineId }) => ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( @@ -71,12 +72,13 @@ export const createEndgameProcessRowRenderer = ({ action?.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, timelineId }) => ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( @@ -239,12 +245,13 @@ export const createSocketRowRenderer = ({ const action: string | null | undefined = get('event.action[0]', ecs); return action != null && action.toLowerCase() === actionName; }, - renderRow: ({ browserFields, data, timelineId }) => ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( @@ -268,12 +275,13 @@ export const createSecurityEventRowRenderer = ({ action?.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, timelineId }) => ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( @@ -287,12 +295,13 @@ export const createDnsRowRenderer = (): RowRenderer => ({ const dnsQuestionName: string | null | undefined = get('dns.question.name[0]', ecs); return !isNillEmptyOrNotFinite(dnsQuestionType) && !isNillEmptyOrNotFinite(dnsQuestionName); }, - renderRow: ({ browserFields, data, timelineId }) => ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( @@ -315,12 +324,13 @@ export const createEndpointRegistryRowRenderer = ({ dataset?.toLowerCase() === 'endpoint.events.registry' && action?.toLowerCase() === actionName ); }, - renderRow: ({ browserFields, data, timelineId }) => ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/package.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/package.tsx index 7952154da1293..296c099da22a4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/package.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/package.tsx @@ -13,13 +13,14 @@ import { TokensFlexItem } from '../helpers'; interface Props { contextId: string; eventId: string; + isDraggable?: boolean; packageName: string | null | undefined; packageSummary: string | null | undefined; packageVersion: string | null | undefined; } export const Package = React.memo( - ({ contextId, eventId, packageName, packageSummary, packageVersion }) => { + ({ contextId, eventId, isDraggable, packageName, packageSummary, packageVersion }) => { if (packageName != null || packageSummary != null || packageVersion != null) { return ( <> @@ -28,6 +29,7 @@ export const Package = React.memo( contextId={contextId} eventId={eventId} field="system.audit.package.name" + isDraggable={isDraggable} value={packageName} iconType="document" /> @@ -37,6 +39,7 @@ export const Package = React.memo( contextId={contextId} eventId={eventId} field="system.audit.package.version" + isDraggable={isDraggable} value={packageVersion} iconType="document" /> @@ -46,6 +49,7 @@ export const Package = React.memo( contextId={contextId} eventId={eventId} field="system.audit.package.summary" + isDraggable={isDraggable} value={packageSummary} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx index 7cff1166cd0de..b61e00f1752b8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx @@ -242,6 +242,7 @@ describe('UserHostWorkingDir', () => { { ); - expect(wrapper.find('[data-test-subj="draggable-content-user.domain"]').exists()).toBe(true); + expect(wrapper.find('[data-test-subj="render-content-user.domain"]').exists()).toBe(true); }); test('it renders a draggable with an overridden field name when userDomain is provided, and userDomainField is also specified as a prop', () => { @@ -261,6 +262,7 @@ describe('UserHostWorkingDir', () => { { ); - expect( - wrapper.find('[data-test-subj="draggable-content-overridden.field.name"]').exists() - ).toBe(true); + expect(wrapper.find('[data-test-subj="render-content-overridden.field.name"]').exists()).toBe( + true + ); }); test('it renders a draggable `user.name` field (by default) when userName is provided, and userNameField is NOT specified as a prop', () => { @@ -283,6 +285,7 @@ describe('UserHostWorkingDir', () => { { ); - expect(wrapper.find('[data-test-subj="draggable-content-user.name"]').exists()).toBe(true); + expect(wrapper.find('[data-test-subj="render-content-user.name"]').exists()).toBe(true); }); test('it renders a draggable with an overridden field name when userName is provided, and userNameField is also specified as a prop', () => { @@ -302,6 +305,7 @@ describe('UserHostWorkingDir', () => { { ); - expect( - wrapper.find('[data-test-subj="draggable-content-overridden.field.name"]').exists() - ).toBe(true); + expect(wrapper.find('[data-test-subj="render-content-overridden.field.name"]').exists()).toBe( + true + ); }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.tsx index 0ab3624970c28..9e789cbd7aba2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.tsx @@ -14,12 +14,13 @@ import { HostWorkingDir } from './host_working_dir'; interface Props { contextId: string; eventId: string; + isDraggable?: boolean; + hostName: string | null | undefined; + hostNameSeparator?: string; userDomain: string | null | undefined; userDomainField?: string; userName: string | null | undefined; userNameField?: string; - hostName: string | null | undefined; - hostNameSeparator?: string; workingDirectory: string | null | undefined; } @@ -29,6 +30,7 @@ export const UserHostWorkingDir = React.memo( eventId, hostName, hostNameSeparator = '@', + isDraggable, userDomain, userDomainField = 'user.domain', userName, @@ -42,6 +44,7 @@ export const UserHostWorkingDir = React.memo( contextId={contextId} eventId={eventId} field={userNameField} + isDraggable={isDraggable} value={userName} iconType="user" /> @@ -61,6 +64,7 @@ export const UserHostWorkingDir = React.memo( contextId={contextId} eventId={eventId} field={userDomainField} + isDraggable={isDraggable} value={userDomain} /> @@ -76,6 +80,7 @@ export const UserHostWorkingDir = React.memo( contextId={contextId} eventId={eventId} hostName={hostName} + isDraggable={isDraggable} workingDirectory={workingDirectory} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap index 6c59df606cd36..94cbe43e93d2d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap @@ -525,6 +525,7 @@ exports[`zeek_row_renderer renders correctly against snapshot 1`] = ` }, } } + isDraggable={true} timelineId="test" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.tsx index 7b44862040f2d..a4dbde1a5626d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.tsx @@ -24,15 +24,16 @@ Details.displayName = 'Details'; interface ZeekDetailsProps { browserFields: BrowserFields; data: Ecs; + isDraggable?: boolean; timelineId: string; } -export const ZeekDetails = React.memo(({ data, timelineId }) => +export const ZeekDetails = React.memo(({ data, isDraggable, timelineId }) => data.zeek != null ? (
- + - +
) : null ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx index 6b154d4d32707..12f2fd08163ba 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx @@ -44,6 +44,7 @@ describe('zeek_row_renderer', () => { const children = zeekRowRenderer.renderRow({ browserFields: mockBrowserFields, data: nonZeek, + isDraggable: true, timelineId: 'test', }); @@ -63,6 +64,7 @@ describe('zeek_row_renderer', () => { const children = zeekRowRenderer.renderRow({ browserFields: mockBrowserFields, data: zeek, + isDraggable: true, timelineId: 'test', }); const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx index 2b6311b8cae83..0a265fa7522b1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx @@ -21,9 +21,14 @@ export const zeekRowRenderer: RowRenderer = { const module: string | null | undefined = get('event.module[0]', ecs); return module != null && module.toLowerCase() === 'zeek'; }, - renderRow: ({ browserFields, data, timelineId }) => ( + renderRow: ({ browserFields, data, isDraggable, timelineId }) => ( - + ), }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.tsx index e4d5a6a86682d..412fd9d04fe7c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.tsx @@ -67,9 +67,10 @@ export const sha1StringRenderer: StringRenderer = (value: string) => export const DraggableZeekElement = React.memo<{ id: string; field: string; + isDraggable?: boolean; value: string | null | undefined; stringRenderer?: StringRenderer; -}>(({ id, field, value, stringRenderer = defaultStringRenderer }) => { +}>(({ id, field, isDraggable, value, stringRenderer = defaultStringRenderer }) => { const dataProviderProp = useMemo( () => ({ and: [], @@ -105,7 +106,7 @@ export const DraggableZeekElement = React.memo<{ return value != null ? ( - + ) : null; }); @@ -203,10 +204,11 @@ export const constructDroppedValue = (dropped: boolean | null | undefined): stri interface ZeekSignatureProps { data: Ecs; + isDraggable?: boolean; timelineId: string; } -export const ZeekSignature = React.memo(({ data, timelineId }) => { +export const ZeekSignature = React.memo(({ data, isDraggable, timelineId }) => { const id = `zeek-signature-draggable-zeek-element-${timelineId}-${data._id}`; const sessionId: string | null | undefined = get('zeek.session_id[0]', data); const dataSet: string | null | undefined = get('event.dataset[0]', data); @@ -234,42 +236,92 @@ export const ZeekSignature = React.memo(({ data, timelineId return ( <> - + - - - - - - - - + + + + + + + + diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts index a68617536c6af..ef47b474350c7 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { DataProvider } from '../../components/timeline/data_providers/data_provider'; import { EqlOptionsSelected } from '../../../../common/search_strategy/timeline'; import type { TimelineEventsType, @@ -26,8 +25,6 @@ export type TimelineModel = TGridModelForTimeline & { prevActiveTab: TimelineTabs; /** Timeline saved object owner */ createdBy?: string; - /** The sources of the event data shown in the timeline */ - dataProviders: DataProvider[]; /** A summary of the events and notes in this timeline */ description: string; eqlOptions: EqlOptionsSelected; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_action_types.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_action_types.sh index 6c4047552ecd7..0aa6eeb04c28e 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_action_types.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_action_types.sh @@ -14,5 +14,5 @@ set -e # https://github.com/elastic/kibana/blob/master/x-pack/plugins/actions/README.md curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}${SPACE_URL}/api/actions/list_action_types \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/actions/connector_types \ | jq . diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/mocks.ts b/x-pack/plugins/security_solution/server/lib/telemetry/mocks.ts index 6e98dcd59e3ec..642be5fc737f7 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/mocks.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/mocks.ts @@ -9,6 +9,7 @@ import { TelemetryEventsSender } from './sender'; import { TelemetryDiagTask } from './diagnostic_task'; import { TelemetryEndpointTask } from './endpoint_task'; +import { TelemetryTrustedAppsTask } from './trusted_apps_task'; import { PackagePolicy } from '../../../../fleet/common/types/models/package_policy'; /** @@ -24,6 +25,7 @@ export const createMockTelemetryEventsSender = ( fetchDiagnosticAlerts: jest.fn(), fetchEndpointMetrics: jest.fn(), fetchEndpointPolicyResponses: jest.fn(), + fetchTrustedApplications: jest.fn(), queueTelemetryEvents: jest.fn(), processEvents: jest.fn(), isTelemetryOptedIn: jest.fn().mockReturnValue(enableTelemtry ?? jest.fn()), @@ -65,3 +67,10 @@ export class MockTelemetryDiagnosticTask extends TelemetryDiagTask { export class MockTelemetryEndpointTask extends TelemetryEndpointTask { public runTask = jest.fn(); } + +/** + * Creates a mocked Telemetry trusted app Task + */ +export class MockTelemetryTrustedAppTask extends TelemetryTrustedAppsTask { + public runTask = jest.fn(); +} diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts index bdd301d9fea1d..e8ef18ec798ae 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -19,8 +19,11 @@ import { } from '../../../../task_manager/server'; import { TelemetryDiagTask } from './diagnostic_task'; import { TelemetryEndpointTask } from './endpoint_task'; +import { TelemetryTrustedAppsTask } from './trusted_apps_task'; import { EndpointAppContextService } from '../../endpoint/endpoint_app_context_services'; import { AgentService, AgentPolicyServiceInterface } from '../../../../fleet/server'; +import { ExceptionListClient } from '../../../../lists/server'; +import { getTrustedAppsList } from '../../endpoint/routes/trusted_apps/service'; type BaseSearchTypes = string | number | boolean | object; export type SearchTypes = BaseSearchTypes | BaseSearchTypes[] | undefined; @@ -57,10 +60,12 @@ export class TelemetryEventsSender { private isOptedIn?: boolean = true; // Assume true until the first check private diagTask?: TelemetryDiagTask; private epMetricsTask?: TelemetryEndpointTask; + private trustedAppsTask?: TelemetryTrustedAppsTask; private agentService?: AgentService; private agentPolicyService?: AgentPolicyServiceInterface; private esClient?: ElasticsearchClient; - private savedObjectClient?: SavedObjectsClientContract; + private savedObjectsClient?: SavedObjectsClientContract; + private exceptionListClient?: ExceptionListClient; constructor(logger: Logger) { this.logger = logger.get('telemetry_events'); @@ -72,6 +77,7 @@ export class TelemetryEventsSender { if (taskManager) { this.diagTask = new TelemetryDiagTask(this.logger, taskManager, this); this.epMetricsTask = new TelemetryEndpointTask(this.logger, taskManager, this); + this.trustedAppsTask = new TelemetryTrustedAppsTask(this.logger, taskManager, this); } } @@ -79,18 +85,21 @@ export class TelemetryEventsSender { core?: CoreStart, telemetryStart?: TelemetryPluginStart, taskManager?: TaskManagerStartContract, - endpointContextService?: EndpointAppContextService + endpointContextService?: EndpointAppContextService, + exceptionListClient?: ExceptionListClient ) { this.telemetryStart = telemetryStart; this.esClient = core?.elasticsearch.client.asInternalUser; this.agentService = endpointContextService?.getAgentService(); this.agentPolicyService = endpointContextService?.getAgentPolicyService(); - this.savedObjectClient = (core?.savedObjects.createInternalRepository() as unknown) as SavedObjectsClientContract; + this.savedObjectsClient = (core?.savedObjects.createInternalRepository() as unknown) as SavedObjectsClientContract; + this.exceptionListClient = exceptionListClient; if (taskManager && this.diagTask && this.epMetricsTask) { this.logger.debug(`Starting diagnostic and endpoint telemetry tasks`); this.diagTask.start(taskManager); this.epMetricsTask.start(taskManager); + this.trustedAppsTask?.start(taskManager); } this.logger.debug(`Starting local task`); @@ -139,7 +148,7 @@ export class TelemetryEventsSender { } public async fetchEndpointMetrics(executeFrom: string, executeTo: string) { - if (this.esClient === undefined) { + if (this.esClient === undefined || this.esClient === null) { throw Error('could not fetch policy responses. es client is not available'); } @@ -186,7 +195,7 @@ export class TelemetryEventsSender { } public async fetchFleetAgents() { - if (this.esClient === undefined) { + if (this.esClient === undefined || this.esClient === null) { throw Error('could not fetch policy responses. es client is not available'); } @@ -199,15 +208,15 @@ export class TelemetryEventsSender { } public async fetchPolicyConfigs(id: string) { - if (this.savedObjectClient === undefined) { + if (this.savedObjectsClient === undefined || this.savedObjectsClient === null) { throw Error('could not fetch endpoint policy configs. saved object client is not available'); } - return this.agentPolicyService?.get(this.savedObjectClient, id); + return this.agentPolicyService?.get(this.savedObjectsClient, id); } public async fetchEndpointPolicyResponses(executeFrom: string, executeTo: string) { - if (this.esClient === undefined) { + if (this.esClient === undefined || this.esClient === null) { throw Error('could not fetch policy responses. es client is not available'); } @@ -253,6 +262,14 @@ export class TelemetryEventsSender { return this.esClient.search(query); } + public async fetchTrustedApplications() { + if (this?.exceptionListClient === undefined || this?.exceptionListClient === null) { + throw Error('could not fetch trusted applications. exception list client not available.'); + } + + return getTrustedAppsList(this.exceptionListClient, { page: 1, per_page: 10_000 }); + } + public queueTelemetryEvents(events: TelemetryEvent[]) { const qlength = this.queue.length; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/trusted_apps_task.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/trusted_apps_task.test.ts new file mode 100644 index 0000000000000..5cd67a9c9c215 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/telemetry/trusted_apps_task.test.ts @@ -0,0 +1,109 @@ +/* + * 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 { loggingSystemMock } from 'src/core/server/mocks'; +import { TaskStatus } from '../../../../task_manager/server'; +import { taskManagerMock } from '../../../../task_manager/server/mocks'; + +import { TelemetryTrustedAppsTask, TelemetryTrustedAppsTaskConstants } from './trusted_apps_task'; +import { createMockTelemetryEventsSender, MockTelemetryTrustedAppTask } from './mocks'; + +describe('test trusted apps telemetry task functionality', () => { + let logger: ReturnType; + + beforeEach(() => { + logger = loggingSystemMock.createLogger(); + }); + + test('the trusted apps task can register', () => { + const telemetryTrustedAppsTask = new TelemetryTrustedAppsTask( + logger, + taskManagerMock.createSetup(), + createMockTelemetryEventsSender(true) + ); + + expect(telemetryTrustedAppsTask).toBeInstanceOf(TelemetryTrustedAppsTask); + }); + + test('the trusted apps task should be registered', () => { + const mockTaskManager = taskManagerMock.createSetup(); + new TelemetryTrustedAppsTask(logger, mockTaskManager, createMockTelemetryEventsSender(true)); + + expect(mockTaskManager.registerTaskDefinitions).toHaveBeenCalled(); + }); + + test('the trusted apps task should be scheduled', async () => { + const mockTaskManagerSetup = taskManagerMock.createSetup(); + const telemetryTrustedAppsTask = new TelemetryTrustedAppsTask( + logger, + mockTaskManagerSetup, + createMockTelemetryEventsSender(true) + ); + + const mockTaskManagerStart = taskManagerMock.createStart(); + await telemetryTrustedAppsTask.start(mockTaskManagerStart); + expect(mockTaskManagerStart.ensureScheduled).toHaveBeenCalled(); + }); + + test('the trusted apps task should not query elastic if telemetry is not opted in', async () => { + const mockSender = createMockTelemetryEventsSender(false); + const mockTaskManager = taskManagerMock.createSetup(); + new MockTelemetryTrustedAppTask(logger, mockTaskManager, mockSender); + + const mockTaskInstance = { + id: TelemetryTrustedAppsTaskConstants.TYPE, + runAt: new Date(), + attempts: 0, + ownerId: '', + status: TaskStatus.Running, + startedAt: new Date(), + scheduledAt: new Date(), + retryAt: new Date(), + params: {}, + state: {}, + taskType: TelemetryTrustedAppsTaskConstants.TYPE, + }; + const createTaskRunner = + mockTaskManager.registerTaskDefinitions.mock.calls[0][0][ + TelemetryTrustedAppsTaskConstants.TYPE + ].createTaskRunner; + const taskRunner = createTaskRunner({ taskInstance: mockTaskInstance }); + await taskRunner.run(); + expect(mockSender.fetchTrustedApplications).not.toHaveBeenCalled(); + }); + + test('the trusted apps task should query elastic if telemetry opted in', async () => { + const mockSender = createMockTelemetryEventsSender(true); + const mockTaskManager = taskManagerMock.createSetup(); + const telemetryTrustedAppsTask = new MockTelemetryTrustedAppTask( + logger, + mockTaskManager, + mockSender + ); + + const mockTaskInstance = { + id: TelemetryTrustedAppsTaskConstants.TYPE, + runAt: new Date(), + attempts: 0, + ownerId: '', + status: TaskStatus.Running, + startedAt: new Date(), + scheduledAt: new Date(), + retryAt: new Date(), + params: {}, + state: {}, + taskType: TelemetryTrustedAppsTaskConstants.TYPE, + }; + const createTaskRunner = + mockTaskManager.registerTaskDefinitions.mock.calls[0][0][ + TelemetryTrustedAppsTaskConstants.TYPE + ].createTaskRunner; + const taskRunner = createTaskRunner({ taskInstance: mockTaskInstance }); + await taskRunner.run(); + expect(telemetryTrustedAppsTask.runTask).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/trusted_apps_task.ts b/x-pack/plugins/security_solution/server/lib/telemetry/trusted_apps_task.ts new file mode 100644 index 0000000000000..f91f3e8428d04 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/telemetry/trusted_apps_task.ts @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; +import { Logger } from 'src/core/server'; +import { + ConcreteTaskInstance, + TaskManagerSetupContract, + TaskManagerStartContract, +} from '../../../../task_manager/server'; + +import { getPreviousEpMetaTaskTimestamp, batchTelemetryRecords } from './helpers'; +import { TelemetryEventsSender } from './sender'; + +export const TelemetryTrustedAppsTaskConstants = { + TIMEOUT: '1m', + TYPE: 'security:trusted-apps-telemetry', + INTERVAL: '24h', + VERSION: '1.0.0', +}; + +/** Telemetry Trusted Apps Task + * + * The Trusted Apps task is a daily batch job that collects and transmits non-sensitive + * trusted apps hashes + file paths for supported operating systems. This helps test + * efficacy of our protections. + */ +export class TelemetryTrustedAppsTask { + private readonly logger: Logger; + private readonly sender: TelemetryEventsSender; + + constructor( + logger: Logger, + taskManager: TaskManagerSetupContract, + sender: TelemetryEventsSender + ) { + this.logger = logger; + this.sender = sender; + + taskManager.registerTaskDefinitions({ + [TelemetryTrustedAppsTaskConstants.TYPE]: { + title: 'Security Solution Telemetry Endpoint Metrics and Info task', + timeout: TelemetryTrustedAppsTaskConstants.TIMEOUT, + createTaskRunner: ({ taskInstance }: { taskInstance: ConcreteTaskInstance }) => { + const { state } = taskInstance; + + return { + run: async () => { + const taskExecutionTime = moment().utc().toISOString(); + const lastExecutionTimestamp = getPreviousEpMetaTaskTimestamp( + taskExecutionTime, + taskInstance.state?.lastExecutionTimestamp + ); + + const hits = await this.runTask( + taskInstance.id, + lastExecutionTimestamp, + taskExecutionTime + ); + + return { + state: { + lastExecutionTimestamp: taskExecutionTime, + runs: (state.runs || 0) + 1, + hits, + }, + }; + }, + cancel: async () => {}, + }; + }, + }, + }); + } + + public start = async (taskManager: TaskManagerStartContract) => { + try { + await taskManager.ensureScheduled({ + id: this.getTaskId(), + taskType: TelemetryTrustedAppsTaskConstants.TYPE, + scope: ['securitySolution'], + schedule: { + interval: TelemetryTrustedAppsTaskConstants.INTERVAL, + }, + state: { runs: 0 }, + params: { version: TelemetryTrustedAppsTaskConstants.VERSION }, + }); + } catch (e) { + this.logger.error(`Error scheduling task, received ${e.message}`); + } + }; + + private getTaskId = (): string => { + return `${TelemetryTrustedAppsTaskConstants.TYPE}:${TelemetryTrustedAppsTaskConstants.VERSION}`; + }; + + public runTask = async (taskId: string, executeFrom: string, executeTo: string) => { + if (taskId !== this.getTaskId()) { + this.logger.debug(`Outdated task running: ${taskId}`); + return 0; + } + + const isOptedIn = await this.sender.isTelemetryOptedIn(); + if (!isOptedIn) { + this.logger.debug(`Telemetry is not opted-in.`); + return 0; + } + + const response = await this.sender.fetchTrustedApplications(); + this.logger.debug(`Trusted Apps: ${response}`); + + batchTelemetryRecords(response.data, 1_000).forEach((telemetryBatch) => + this.sender.sendOnDemand('lists-trustedapps', telemetryBatch) + ); + + return response.data.length; + }; +} diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 2c0be2ac93321..a68280379fad3 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -391,6 +391,8 @@ export class Plugin implements IPlugin ({ logHealthMetrics: jest.fn(), })); -// FLAKY: https://github.com/elastic/kibana/issues/106388 -describe.skip('healthRoute', () => { +describe('healthRoute', () => { beforeEach(() => { jest.resetAllMocks(); }); @@ -161,7 +160,7 @@ describe.skip('healthRoute', () => { summarizeMonitoringStats(warnWorkloadStat, getTaskManagerConfig({})) ), }); - expect(logHealthMetrics.mock.calls[2][0]).toMatchObject({ + expect(logHealthMetrics.mock.calls[3][0]).toMatchObject({ id, timestamp: expect.any(String), status: expect.any(String), @@ -234,7 +233,7 @@ describe.skip('healthRoute', () => { summarizeMonitoringStats(errorWorkloadStat, getTaskManagerConfig({})) ), }); - expect(logHealthMetrics.mock.calls[2][0]).toMatchObject({ + expect(logHealthMetrics.mock.calls[3][0]).toMatchObject({ id, timestamp: expect.any(String), status: expect.any(String), diff --git a/x-pack/plugins/timelines/common/types/timeline/rows/index.ts b/x-pack/plugins/timelines/common/types/timeline/rows/index.ts index b598d13273798..089f886e1c221 100644 --- a/x-pack/plugins/timelines/common/types/timeline/rows/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/rows/index.ts @@ -15,10 +15,12 @@ export interface RowRenderer { renderRow: ({ browserFields, data, + isDraggable, timelineId, }: { browserFields: BrowserFields; data: Ecs; + isDraggable: boolean; timelineId: string; }) => React.ReactNode; } diff --git a/x-pack/plugins/timelines/public/components/fields_browser/index.tsx b/x-pack/plugins/timelines/public/components/fields_browser/index.tsx index ac121f9afdd58..3b67d530bc63f 100644 --- a/x-pack/plugins/timelines/public/components/fields_browser/index.tsx +++ b/x-pack/plugins/timelines/public/components/fields_browser/index.tsx @@ -11,25 +11,17 @@ import { Provider } from 'react-redux'; import { I18nProvider } from '@kbn/i18n/react'; import type { FieldBrowserProps } from '../t_grid/toolbar/fields_browser/types'; import { StatefulFieldsBrowser } from '../t_grid/toolbar/fields_browser'; -import { - FIELD_BROWSER_WIDTH, - FIELD_BROWSER_HEIGHT, -} from '../t_grid/toolbar/fields_browser/helpers'; +export type { FieldBrowserProps } from '../t_grid/toolbar/fields_browser/types'; const EMPTY_BROWSER_FIELDS = {}; -export type FieldBrowserWrappedProps = Omit & { - width?: FieldBrowserProps['width']; - height?: FieldBrowserProps['height']; -}; -export type FieldBrowserWrappedComponentProps = FieldBrowserWrappedProps & { + +export type FieldBrowserWrappedComponentProps = FieldBrowserProps & { store: Store; }; export const FieldBrowserWrappedComponent = (props: FieldBrowserWrappedComponentProps) => { const { store, ...restProps } = props; const fieldsBrowseProps = { - width: FIELD_BROWSER_WIDTH, - height: FIELD_BROWSER_HEIGHT, ...restProps, browserFields: restProps.browserFields ?? EMPTY_BROWSER_FIELDS, }; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx index eb9c95f0998c6..dd5ef27c32a89 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx @@ -5,18 +5,20 @@ * 2.0. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { DraggableId } from 'react-beautiful-dnd'; +import { useDispatch } from 'react-redux'; + +import { isEmpty } from 'lodash'; +import { DataProvider, stopPropagationAndPreventDefault, TimelineId } from '../../../../common'; import { TooltipWithKeyboardShortcut } from '../../tooltip_with_keyboard_shortcut'; import { getAdditionalScreenReaderOnlyContext } from '../utils'; import { useAddToTimeline } from '../../../hooks/use_add_to_timeline'; import { HoverActionComponentProps } from './types'; - -const ADD_TO_TIMELINE = i18n.translate('xpack.timelines.hoverActions.addToTimeline', { - defaultMessage: 'Add to timeline investigation', -}); +import { tGridActions } from '../../..'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; +import * as i18n from './translations'; export const ADD_TO_TIMELINE_KEYBOARD_SHORTCUT = 'a'; @@ -25,7 +27,7 @@ export interface UseGetHandleStartDragToTimelineArgs { draggableId: DraggableId | undefined; } -export const useGetHandleStartDragToTimeline = ({ +const useGetHandleStartDragToTimeline = ({ field, draggableId, }: UseGetHandleStartDragToTimelineArgs): (() => void) => { @@ -41,8 +43,59 @@ export const useGetHandleStartDragToTimeline = ({ return handleStartDragToTimeline; }; -export const AddToTimelineButton: React.FC = React.memo( - ({ field, onClick, ownFocus, showTooltip = false, value }) => { +export interface AddToTimelineButtonProps extends HoverActionComponentProps { + draggableId?: DraggableId; + dataProvider?: DataProvider[] | DataProvider; +} + +const AddToTimelineButton: React.FC = React.memo( + ({ + closePopOver, + dataProvider, + defaultFocusedButtonRef, + draggableId, + field, + keyboardEvent, + ownFocus, + showTooltip = false, + value, + }) => { + const dispatch = useDispatch(); + const { addSuccess } = useAppToasts(); + const startDragToTimeline = useGetHandleStartDragToTimeline({ draggableId, field }); + const handleStartDragToTimeline = useCallback(() => { + if (draggableId != null) { + startDragToTimeline(); + } else if (!isEmpty(dataProvider)) { + const addDataProvider = Array.isArray(dataProvider) ? dataProvider : [dataProvider]; + addDataProvider.forEach((provider) => { + if (provider) { + dispatch( + tGridActions.addProviderToTimeline({ + id: TimelineId.active, + dataProvider: provider, + }) + ); + addSuccess(i18n.ADDED_TO_TIMELINE_MESSAGE(provider.name)); + } + }); + } + + if (closePopOver != null) { + closePopOver(); + } + }, [addSuccess, closePopOver, dataProvider, dispatch, draggableId, startDragToTimeline]); + + useEffect(() => { + if (!ownFocus) { + return; + } + if (keyboardEvent?.key === ADD_TO_TIMELINE_KEYBOARD_SHORTCUT) { + stopPropagationAndPreventDefault(keyboardEvent); + handleStartDragToTimeline(); + } + }, [handleStartDragToTimeline, keyboardEvent, ownFocus]); + return showTooltip ? ( = React.me field, value, })} - content={ADD_TO_TIMELINE} + content={i18n.ADD_TO_TIMELINE} shortcut={ADD_TO_TIMELINE_KEYBOARD_SHORTCUT} showShortcut={ownFocus} /> } > ) : ( ); } ); AddToTimelineButton.displayName = 'AddToTimelineButton'; + +// eslint-disable-next-line import/no-default-export +export { AddToTimelineButton as default }; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/column_toggle.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/column_toggle.tsx index 52d8fb439526f..d59383b8553ea 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/column_toggle.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/column_toggle.tsx @@ -5,9 +5,11 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback, useEffect } from 'react'; import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; + +import { stopPropagationAndPreventDefault } from '../../../../common'; import { TooltipWithKeyboardShortcut } from '../../tooltip_with_keyboard_shortcut'; import { getAdditionalScreenReaderOnlyContext } from '../utils'; import { defaultColumnHeaderType } from '../../t_grid/body/column_headers/default_headers'; @@ -30,28 +32,48 @@ export const NESTED_COLUMN = (field: string) => export const COLUMN_TOGGLE_KEYBOARD_SHORTCUT = 'i'; -export interface ColumnToggleFnArgs { - toggleColumn: (column: ColumnHeaderOptions) => void; - field: string; -} - -export const columnToggleFn = ({ toggleColumn, field }: ColumnToggleFnArgs): void => { - return toggleColumn({ - columnHeaderType: defaultColumnHeaderType, - id: field, - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }); -}; - export interface ColumnToggleProps extends HoverActionComponentProps { isDisabled: boolean; isObjectArray: boolean; + toggleColumn: (column: ColumnHeaderOptions) => void; } -export const ColumnToggleButton: React.FC = React.memo( - ({ field, isDisabled, isObjectArray, onClick, ownFocus, showTooltip = false, value }) => { +const ColumnToggleButton: React.FC = React.memo( + ({ + closePopOver, + defaultFocusedButtonRef, + field, + isDisabled, + isObjectArray, + keyboardEvent, + ownFocus, + showTooltip = false, + toggleColumn, + value, + }) => { const label = isObjectArray ? NESTED_COLUMN(field) : COLUMN_TOGGLE(field); + const handleToggleColumn = useCallback(() => { + toggleColumn({ + columnHeaderType: defaultColumnHeaderType, + id: field, + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }); + if (closePopOver != null) { + closePopOver(); + } + }, [closePopOver, field, toggleColumn]); + + useEffect(() => { + if (!ownFocus) { + return; + } + if (keyboardEvent?.key === COLUMN_TOGGLE_KEYBOARD_SHORTCUT) { + stopPropagationAndPreventDefault(keyboardEvent); + handleToggleColumn(); + } + }, [handleToggleColumn, keyboardEvent, ownFocus]); + return showTooltip ? ( = React.memo( > = React.memo( id={field} iconSize="s" iconType="listAdd" - onClick={onClick} + onClick={handleToggleColumn} /> ) : ( = React.memo( id={field} iconSize="s" iconType="listAdd" - onClick={onClick} + onClick={handleToggleColumn} /> ); } ); ColumnToggleButton.displayName = 'ColumnToggleButton'; + +// eslint-disable-next-line import/no-default-export +export { ColumnToggleButton as default }; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx index 33cc71e12c46f..1b567dee50683 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx @@ -5,10 +5,12 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import { i18n } from '@kbn/i18n'; +import { stopPropagationAndPreventDefault } from '../../../../common'; import { WithCopyToClipboard } from '../../clipboard/with_copy_to_clipboard'; import { HoverActionComponentProps } from './types'; +import { COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME } from '../../clipboard'; export const FIELD = i18n.translate('xpack.timelines.hoverActions.fieldLabel', { defaultMessage: 'Field', @@ -16,22 +18,45 @@ export const FIELD = i18n.translate('xpack.timelines.hoverActions.fieldLabel', { export const COPY_TO_CLIPBOARD_KEYBOARD_SHORTCUT = 'c'; -export type CopyProps = Omit & { +export interface CopyProps extends HoverActionComponentProps { isHoverAction?: boolean; -}; +} -export const CopyButton: React.FC = React.memo( - ({ field, isHoverAction, ownFocus, value }) => { +const CopyButton: React.FC = React.memo( + ({ closePopOver, field, isHoverAction, keyboardEvent, ownFocus, value }) => { + const panelRef = useRef(null); + useEffect(() => { + if (!ownFocus) { + return; + } + if (keyboardEvent?.key === COPY_TO_CLIPBOARD_KEYBOARD_SHORTCUT) { + stopPropagationAndPreventDefault(keyboardEvent); + const copyToClipboardButton = panelRef.current?.querySelector( + `.${COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME}` + ); + if (copyToClipboardButton != null) { + copyToClipboardButton.click(); + } + if (closePopOver != null) { + closePopOver(); + } + } + }, [closePopOver, keyboardEvent, ownFocus]); return ( - +
+ +
); } ); CopyButton.displayName = 'CopyButton'; + +// eslint-disable-next-line import/no-default-export +export { CopyButton as default }; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_for_value.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_for_value.tsx index 421cd0089c1b7..58f7b4a831e51 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_for_value.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_for_value.tsx @@ -5,9 +5,11 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiButtonIcon, EuiButtonIconPropsForButton, EuiToolTip } from '@elastic/eui'; +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; + +import { stopPropagationAndPreventDefault } from '../../../../common'; import { TooltipWithKeyboardShortcut } from '../../tooltip_with_keyboard_shortcut'; import { createFilter, getAdditionalScreenReaderOnlyContext } from '../utils'; import { HoverActionComponentProps, FilterValueFnArgs } from './types'; @@ -17,34 +19,50 @@ export const FILTER_FOR_VALUE = i18n.translate('xpack.timelines.hoverActions.fil }); export const FILTER_FOR_VALUE_KEYBOARD_SHORTCUT = 'f'; -export const filterForValueFn = ({ - field, - value, - filterManager, - onFilterAdded, -}: FilterValueFnArgs): void => { - const makeFilter = (currentVal: string | null | undefined) => - currentVal?.length === 0 ? createFilter(field, undefined) : createFilter(field, currentVal); - const filters = Array.isArray(value) - ? value.map((currentVal: string | null | undefined) => makeFilter(currentVal)) - : makeFilter(value); +export type FilterForValueProps = HoverActionComponentProps & FilterValueFnArgs; - const activeFilterManager = filterManager; +const FilterForValueButton: React.FC = React.memo( + ({ + closePopOver, + defaultFocusedButtonRef, + field, + filterManager, + keyboardEvent, + onFilterAdded, + ownFocus, + showTooltip = false, + value, + }) => { + const filterForValueFn = useCallback(() => { + const makeFilter = (currentVal: string | null | undefined) => + currentVal?.length === 0 ? createFilter(field, undefined) : createFilter(field, currentVal); + const filters = Array.isArray(value) + ? value.map((currentVal: string | null | undefined) => makeFilter(currentVal)) + : makeFilter(value); - if (activeFilterManager != null) { - activeFilterManager.addFilters(filters); - if (onFilterAdded != null) { - onFilterAdded(); - } - } -}; + const activeFilterManager = filterManager; -export interface FilterForValueProps extends HoverActionComponentProps { - defaultFocusedButtonRef: EuiButtonIconPropsForButton['buttonRef']; -} + if (activeFilterManager != null) { + activeFilterManager.addFilters(filters); + if (onFilterAdded != null) { + onFilterAdded(); + } + } + if (closePopOver != null) { + closePopOver(); + } + }, [closePopOver, field, filterManager, onFilterAdded, value]); + + useEffect(() => { + if (!ownFocus) { + return; + } + if (keyboardEvent?.key === FILTER_FOR_VALUE_KEYBOARD_SHORTCUT) { + stopPropagationAndPreventDefault(keyboardEvent); + filterForValueFn(); + } + }, [filterForValueFn, keyboardEvent, ownFocus]); -export const FilterForValueButton: React.FC = React.memo( - ({ defaultFocusedButtonRef, field, onClick, ownFocus, showTooltip = false, value }) => { return showTooltip ? ( = React.memo( data-test-subj="filter-for-value" iconSize="s" iconType="plusInCircle" - onClick={onClick} + onClick={filterForValueFn} /> ) : ( @@ -77,10 +95,13 @@ export const FilterForValueButton: React.FC = React.memo( data-test-subj="filter-for-value" iconSize="s" iconType="plusInCircle" - onClick={onClick} + onClick={filterForValueFn} /> ); } ); FilterForValueButton.displayName = 'FilterForValueButton'; + +// eslint-disable-next-line import/no-default-export +export { FilterForValueButton as default }; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_out_value.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_out_value.tsx index bfa7848025bf4..03150d6371397 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_out_value.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_out_value.tsx @@ -5,9 +5,11 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; + +import { stopPropagationAndPreventDefault } from '../../../../common'; import { TooltipWithKeyboardShortcut } from '../../tooltip_with_keyboard_shortcut'; import { createFilter, getAdditionalScreenReaderOnlyContext } from '../utils'; import { HoverActionComponentProps, FilterValueFnArgs } from './types'; @@ -18,32 +20,50 @@ export const FILTER_OUT_VALUE = i18n.translate('xpack.timelines.hoverActions.fil export const FILTER_OUT_VALUE_KEYBOARD_SHORTCUT = 'o'; -export const filterOutValueFn = ({ - field, - value, - filterManager, - onFilterAdded, -}: FilterValueFnArgs) => { - const makeFilter = (currentVal: string | null | undefined) => - currentVal?.length === 0 - ? createFilter(field, null, false) - : createFilter(field, currentVal, true); - const filters = Array.isArray(value) - ? value.map((currentVal: string | null | undefined) => makeFilter(currentVal)) - : makeFilter(value); +const FilterOutValueButton: React.FC = React.memo( + ({ + closePopOver, + defaultFocusedButtonRef, + field, + filterManager, + keyboardEvent, + onFilterAdded, + ownFocus, + showTooltip = false, + value, + }) => { + const filterOutValueFn = useCallback(() => { + const makeFilter = (currentVal: string | null | undefined) => + currentVal?.length === 0 + ? createFilter(field, null, false) + : createFilter(field, currentVal, true); + const filters = Array.isArray(value) + ? value.map((currentVal: string | null | undefined) => makeFilter(currentVal)) + : makeFilter(value); - const activeFilterManager = filterManager; + const activeFilterManager = filterManager; - if (activeFilterManager != null) { - activeFilterManager.addFilters(filters); - if (onFilterAdded != null) { - onFilterAdded(); - } - } -}; + if (activeFilterManager != null) { + activeFilterManager.addFilters(filters); + if (onFilterAdded != null) { + onFilterAdded(); + } + } + if (closePopOver != null) { + closePopOver(); + } + }, [closePopOver, field, filterManager, onFilterAdded, value]); + + useEffect(() => { + if (!ownFocus) { + return; + } + if (keyboardEvent?.key === FILTER_OUT_VALUE_KEYBOARD_SHORTCUT) { + stopPropagationAndPreventDefault(keyboardEvent); + filterOutValueFn(); + } + }, [filterOutValueFn, keyboardEvent, ownFocus]); -export const FilterOutValueButton: React.FC = React.memo( - ({ field, onClick, ownFocus, showTooltip = false, value }) => { return showTooltip ? ( = React.m > ) : ( ); } ); FilterOutValueButton.displayName = 'FilterOutValueButton'; + +// eslint-disable-next-line import/no-default-export +export { FilterOutValueButton as default }; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/translations.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/translations.tsx new file mode 100644 index 0000000000000..2f8587ddfab49 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/translations.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ADD_TO_TIMELINE = i18n.translate('xpack.timelines.hoverActions.addToTimeline', { + defaultMessage: 'Add to timeline investigation', +}); + +export const ADDED_TO_TIMELINE_MESSAGE = (fieldOrValue: string) => + i18n.translate('xpack.timelines.hoverActions.addToTimeline.addedFieldMessage', { + values: { fieldOrValue }, + defaultMessage: `Added {fieldOrValue} to timeline`, + }); diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/types.ts b/x-pack/plugins/timelines/public/components/hover_actions/actions/types.ts index 4999638e0fe81..fdef1403e3dc2 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/types.ts +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/types.ts @@ -4,6 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + +import { EuiButtonIconPropsForButton } from '@elastic/eui'; import { FilterManager } from '../../../../../../../src/plugins/data/public'; export interface FilterValueFnArgs { @@ -14,8 +16,10 @@ export interface FilterValueFnArgs { } export interface HoverActionComponentProps { + closePopOver?: () => void; + defaultFocusedButtonRef?: EuiButtonIconPropsForButton['buttonRef']; field: string; - onClick?: () => void; + keyboardEvent?: React.KeyboardEvent; ownFocus: boolean; showTooltip?: boolean; value?: string[] | string | null; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/index.tsx b/x-pack/plugins/timelines/public/components/hover_actions/index.tsx index 2329134d85626..fc8fcfa488a76 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/index.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/index.tsx @@ -4,86 +4,83 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; -import { - AddToTimelineButton, - ADD_TO_TIMELINE_KEYBOARD_SHORTCUT, - UseGetHandleStartDragToTimelineArgs, - useGetHandleStartDragToTimeline, -} from './actions/add_to_timeline'; -import { - ColumnToggleButton, - columnToggleFn, - ColumnToggleFnArgs, - ColumnToggleProps, - COLUMN_TOGGLE_KEYBOARD_SHORTCUT, -} from './actions/column_toggle'; -import { CopyButton, CopyProps, COPY_TO_CLIPBOARD_KEYBOARD_SHORTCUT } from './actions/copy'; -import { - FilterForValueButton, - filterForValueFn, - FilterForValueProps, - FILTER_FOR_VALUE_KEYBOARD_SHORTCUT, -} from './actions/filter_for_value'; -import { - FilterOutValueButton, - filterOutValueFn, - FILTER_OUT_VALUE_KEYBOARD_SHORTCUT, -} from './actions/filter_out_value'; -import { HoverActionComponentProps, FilterValueFnArgs } from './actions/types'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { I18nProvider } from '@kbn/i18n/react'; +import React, { ReactElement } from 'react'; +import { Provider } from 'react-redux'; +import { Store } from 'redux'; +import type { AddToTimelineButtonProps } from './actions/add_to_timeline'; +import type { ColumnToggleProps } from './actions/column_toggle'; +import type { CopyProps } from './actions/copy'; +import type { HoverActionComponentProps, FilterValueFnArgs } from './actions/types'; export interface HoverActionsConfig { - addToTimeline: { - AddToTimelineButton: React.FC; - keyboardShortcut: string; - useGetHandleStartDragToTimeline: (args: UseGetHandleStartDragToTimelineArgs) => () => void; - }; - columnToggle: { - ColumnToggleButton: React.FC; - columnToggleFn: (args: ColumnToggleFnArgs) => void; - keyboardShortcut: string; - }; - copy: { - CopyButton: React.FC; - keyboardShortcut: string; - }; - filterForValue: { - FilterForValueButton: React.FC; - filterForValueFn: (args: FilterValueFnArgs) => void; - keyboardShortcut: string; - }; - filterOutValue: { - FilterOutValueButton: React.FC; - filterOutValueFn: (args: FilterValueFnArgs) => void; - keyboardShortcut: string; - }; + getAddToTimelineButton: ( + props: AddToTimelineButtonProps + ) => ReactElement; + getColumnToggleButton: (props: ColumnToggleProps) => ReactElement; + getCopyButton: (props: CopyProps) => ReactElement; + getFilterForValueButton: ( + props: HoverActionComponentProps & FilterValueFnArgs + ) => ReactElement; + getFilterOutValueButton: ( + props: HoverActionComponentProps & FilterValueFnArgs + ) => ReactElement; } -export const addToTimeline = { - AddToTimelineButton, - keyboardShortcut: ADD_TO_TIMELINE_KEYBOARD_SHORTCUT, - useGetHandleStartDragToTimeline, +const AddToTimelineButtonLazy = React.lazy(() => import('./actions/add_to_timeline')); +const getAddToTimelineButtonLazy = (store: Store, props: AddToTimelineButtonProps) => { + return ( + }> + + + + + + + ); }; -export const columnToggle = { - ColumnToggleButton, - columnToggleFn, - keyboardShortcut: COLUMN_TOGGLE_KEYBOARD_SHORTCUT, +const ColumnToggleButtonLazy = React.lazy(() => import('./actions/column_toggle')); +const getColumnToggleButtonLazy = (props: ColumnToggleProps) => { + return ( + }> + + + ); }; -export const copy = { - CopyButton, - keyboardShortcut: COPY_TO_CLIPBOARD_KEYBOARD_SHORTCUT, +const CopyButtonLazy = React.lazy(() => import('./actions/copy')); +const getCopyButtonLazy = (props: CopyProps) => { + return ( + }> + + + ); }; -export const filterForValue = { - FilterForValueButton, - filterForValueFn, - keyboardShortcut: FILTER_FOR_VALUE_KEYBOARD_SHORTCUT, +const FilterForValueButtonLazy = React.lazy(() => import('./actions/filter_for_value')); +const getFilterForValueButtonLazy = (props: HoverActionComponentProps & FilterValueFnArgs) => { + return ( + }> + + + ); }; -export const filterOutValue = { - FilterOutValueButton, - filterOutValueFn, - keyboardShortcut: FILTER_OUT_VALUE_KEYBOARD_SHORTCUT, +const FilterOutValueButtonLazy = React.lazy(() => import('./actions/filter_out_value')); +const getFilterOutValueButtonLazy = (props: HoverActionComponentProps & FilterValueFnArgs) => { + return ( + }> + + + ); }; + +export const getHoverActions = (store?: Store): HoverActionsConfig => ({ + getAddToTimelineButton: getAddToTimelineButtonLazy.bind(null, store!), + getColumnToggleButton: getColumnToggleButtonLazy, + getCopyButton: getCopyButtonLazy, + getFilterForValueButton: getFilterForValueButtonLazy, + getFilterOutValueButton: getFilterOutValueButtonLazy, +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.test.tsx new file mode 100644 index 0000000000000..e263be11c0dcc --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.test.tsx @@ -0,0 +1,76 @@ +/* + * 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 { render, fireEvent } from '@testing-library/react'; +import { ActionProps, HeaderActionProps, TimelineTabs } from '../../../../../common'; +import { HeaderCheckBox, RowCheckBox } from './checkbox'; +import React from 'react'; + +describe('checkbox control column', () => { + describe('RowCheckBox', () => { + const defaultProps: ActionProps = { + ariaRowindex: 1, + columnId: 'test-columnId', + columnValues: 'test-columnValues', + checked: false, + onRowSelected: jest.fn(), + eventId: 'test-event-id', + loadingEventIds: [], + onEventDetailsPanelOpened: jest.fn(), + showCheckboxes: true, + data: [], + ecsData: { + _id: 'test-ecsData-id', + }, + index: 1, + rowIndex: 1, + showNotes: true, + timelineId: 'test-timelineId', + }; + test('displays loader when id is included on loadingEventIds', () => { + const { getByTestId } = render( + + ); + expect(getByTestId('event-loader')).not.toBeNull(); + }); + + test('calls onRowSelected when checked', () => { + const onRowSelected = jest.fn(); + const { getByTestId } = render( + + ); + + fireEvent.click(getByTestId('select-event')); + + expect(onRowSelected).toHaveBeenCalled(); + }); + }); + describe('HeaderCheckBox', () => { + const defaultProps: HeaderActionProps = { + width: 99999, + browserFields: {}, + columnHeaders: [], + isSelectAllChecked: true, + onSelectAll: jest.fn(), + showEventsSelect: true, + showSelectAllCheckbox: true, + sort: [], + tabType: TimelineTabs.query, + timelineId: 'test-timelineId', + }; + + test('calls onSelectAll when checked', () => { + const onSelectAll = jest.fn(); + const { getByTestId } = render( + + ); + fireEvent.click(getByTestId('select-all-events')); + + expect(onSelectAll).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.tsx new file mode 100644 index 0000000000000..cc8ec06d18dbd --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.tsx @@ -0,0 +1,59 @@ +/* + * 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 { EuiCheckbox, EuiLoadingSpinner } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { ActionProps, HeaderActionProps } from '../../../../../common'; +import * as i18n from './translations'; + +export const RowCheckBox = ({ + eventId, + onRowSelected, + checked, + ariaRowindex, + columnValues, + loadingEventIds, +}: ActionProps) => { + const handleSelectEvent = useCallback( + (event: React.ChangeEvent) => + onRowSelected({ + eventIds: [eventId], + isSelected: event.currentTarget.checked, + }), + [eventId, onRowSelected] + ); + + return loadingEventIds.includes(eventId) ? ( + + ) : ( + + ); +}; + +export const HeaderCheckBox = ({ onSelectAll, isSelectAllChecked }: HeaderActionProps) => { + const handleSelectPageChange = useCallback( + (event: React.ChangeEvent) => { + onSelectAll({ isSelected: event.currentTarget.checked }); + }, + [onSelectAll] + ); + + return ( + + ); +}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/index.tsx new file mode 100644 index 0000000000000..dbf7fc9b99cff --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/index.tsx @@ -0,0 +1,16 @@ +/* + * 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 { ControlColumnProps } from '../../../../../common'; +import { HeaderCheckBox, RowCheckBox } from './checkbox'; + +export const checkBoxControlColumn: ControlColumnProps = { + id: 'checkbox-control-column', + width: 32, + headerCellRender: HeaderCheckBox, + rowCellRender: RowCheckBox, +}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/translations.ts new file mode 100644 index 0000000000000..9cc4bfd58357c --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { i18n } from '@kbn/i18n'; + +export const CHECKBOX_FOR_ROW = ({ + ariaRowindex, + columnValues, + checked, +}: { + ariaRowindex: number; + columnValues: string; + checked: boolean; +}) => + i18n.translate('xpack.timelines.timeline.body.actions.checkboxForRowAriaLabel', { + values: { ariaRowindex, checked, columnValues }, + defaultMessage: + '{checked, select, false {unchecked} true {checked}} checkbox for the alert or event in row {ariaRowindex}, with columns {columnValues}', + }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_row_renderer/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_row_renderer/index.tsx index 65762b93cd43f..0d606ad28eff2 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_row_renderer/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_row_renderer/index.tsx @@ -80,6 +80,7 @@ export const StatefulRowRenderer = ({ {rowRenderer.renderRow({ browserFields, data: event.ecs, + isDraggable: false, timelineId, })}
diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx index 6be6db292fc96..1efee943c6456 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx @@ -17,7 +17,8 @@ import memoizeOne from 'memoize-one'; import React, { ComponentType, useCallback, useEffect, useMemo, useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { TimelineId, TimelineTabs } from '../../../../common/types/timeline'; +import { SortColumnTimeline, TimelineId, TimelineTabs } from '../../../../common/types/timeline'; + import type { CellValueElementProps, ColumnHeaderOptions, @@ -37,8 +38,8 @@ import { StatefulFieldsBrowser, tGridActions } from '../../../'; import { TGridModel, tGridSelectors, TimelineState } from '../../../store/t_grid'; import { useDeepEqualSelector } from '../../../hooks/use_selector'; import { RowAction } from './row_action'; -import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from '../toolbar/fields_browser/helpers'; import * as i18n from './translations'; +import { checkBoxControlColumn } from './control_columns'; interface OwnProps { activePage: number; @@ -87,6 +88,10 @@ const transformControlColumns = ({ showCheckboxes, tabType, timelineId, + isSelectAllChecked, + onSelectPage, + browserFields, + sort, }: { actionColumnsWidth: number; columnHeaders: ColumnHeaderOptions[]; @@ -100,11 +105,38 @@ const transformControlColumns = ({ showCheckboxes: boolean; tabType: TimelineTabs; timelineId: string; + isSelectAllChecked: boolean; + browserFields: BrowserFields; + onSelectPage: OnSelectAll; + sort: SortColumnTimeline[]; }): EuiDataGridControlColumn[] => controlColumns.map( ({ id: columnId, headerCellRender = EmptyHeaderCellRender, rowCellRender, width }, i) => ({ id: `${columnId}`, - headerCellRender: headerCellRender as ComponentType, + // eslint-disable-next-line react/display-name + headerCellRender: () => { + const HeaderActions = headerCellRender; + return ( + <> + {HeaderActions && ( + + )} + + ); + }, + // eslint-disable-next-line react/display-name rowCellRender: ({ isDetails, @@ -135,7 +167,7 @@ const transformControlColumns = ({ width={width ?? MIN_ACTION_COLUMN_WIDTH} /> ), - width: actionColumnsWidth, + width: width ?? actionColumnsWidth, }) ); @@ -189,7 +221,7 @@ export const BodyComponent = React.memo( [setSelected, id, data, selectedEventIds, queryFields] ); - const onSelectAll: OnSelectAll = useCallback( + const onSelectPage: OnSelectAll = useCallback( ({ isSelected }: { isSelected: boolean }) => isSelected ? setSelected!({ @@ -209,9 +241,9 @@ export const BodyComponent = React.memo( // Sync to selectAll so parent components can select all events useEffect(() => { if (selectAll && !isSelectAllChecked) { - onSelectAll({ isSelected: true }); + onSelectPage({ isSelected: true }); } - }, [isSelectAllChecked, onSelectAll, selectAll]); + }, [isSelectAllChecked, onSelectPage, selectAll]); const toolbarVisibility: EuiDataGridToolBarVisibilityOptions = useMemo( () => ({ @@ -221,8 +253,6 @@ export const BodyComponent = React.memo( { ( setVisibleColumns(columnHeaders.map(({ id: cid }) => cid)); }, [columnHeaders]); - const [leadingTGridControlColumns, trailingTGridControlColumns] = useMemo( - () => - [leadingControlColumns, trailingControlColumns].map((controlColumns) => - transformControlColumns({ - columnHeaders, - controlColumns, - data, - isEventViewer, - actionColumnsWidth: hasAdditionalActions(id as TimelineId) - ? getActionsColumnWidth( - isEventViewer, - showCheckboxes, - DEFAULT_ICON_BUTTON_WIDTH * NUM_OF_ICON_IN_TIMELINE_ROW + EXTRA_WIDTH - ) - : controlColumns.reduce((acc, c) => acc + (c.width ?? MIN_ACTION_COLUMN_WIDTH), 0), - loadingEventIds, - onRowSelected, - onRuleChange, - selectedEventIds, - showCheckboxes, - tabType, - timelineId: id, - }) - ), - [ - columnHeaders, - data, - id, - isEventViewer, - leadingControlColumns, - loadingEventIds, - onRowSelected, - onRuleChange, - selectedEventIds, - showCheckboxes, - tabType, + const [leadingTGridControlColumns, trailingTGridControlColumns] = useMemo(() => { + return [ + showCheckboxes ? [checkBoxControlColumn, ...leadingControlColumns] : leadingControlColumns, trailingControlColumns, - ] - ); + ].map((controlColumns) => + transformControlColumns({ + columnHeaders, + controlColumns, + data, + isEventViewer, + actionColumnsWidth: hasAdditionalActions(id as TimelineId) + ? getActionsColumnWidth( + isEventViewer, + showCheckboxes, + DEFAULT_ICON_BUTTON_WIDTH * NUM_OF_ICON_IN_TIMELINE_ROW + EXTRA_WIDTH + ) + : controlColumns.reduce((acc, c) => acc + (c.width ?? MIN_ACTION_COLUMN_WIDTH), 0), + loadingEventIds, + onRowSelected, + onRuleChange, + selectedEventIds, + showCheckboxes, + tabType, + timelineId: id, + isSelectAllChecked, + sort, + browserFields, + onSelectPage, + }) + ); + }, [ + columnHeaders, + data, + id, + isEventViewer, + leadingControlColumns, + loadingEventIds, + onRowSelected, + onRuleChange, + selectedEventIds, + showCheckboxes, + tabType, + trailingControlColumns, + isSelectAllChecked, + browserFields, + onSelectPage, + sort, + ]); const renderTGridCellValue: (x: EuiDataGridCellValueElementProps) => React.ReactNode = ({ columnId, diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/renderers/plain_row_renderer.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/renderers/plain_row_renderer.test.tsx index 5cd709d2de3c7..1a4bfcb0e4ab5 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/renderers/plain_row_renderer.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/renderers/plain_row_renderer.test.tsx @@ -23,6 +23,7 @@ describe('plain_row_renderer', () => { const children = plainRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockDatum, + isDraggable: false, timelineId: 'test', }); const wrapper = shallow({children}); @@ -37,6 +38,7 @@ describe('plain_row_renderer', () => { const children = plainRowRenderer.renderRow({ browserFields: mockBrowserFields, data: mockDatum, + isDraggable: false, timelineId: 'test', }); const wrapper = mount({children}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/body/translations.ts index c45a00a0516f4..e2d13fe49f2b6 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/translations.ts +++ b/x-pack/plugins/timelines/public/components/t_grid/body/translations.ts @@ -120,21 +120,6 @@ export const ACTION_INVESTIGATE_IN_RESOLVER = i18n.translate( } ); -export const CHECKBOX_FOR_ROW = ({ - ariaRowindex, - columnValues, - checked, -}: { - ariaRowindex: number; - columnValues: string; - checked: boolean; -}) => - i18n.translate('xpack.timelines.timeline.body.actions.checkboxForRowAriaLabel', { - values: { ariaRowindex, checked, columnValues }, - defaultMessage: - '{checked, select, false {unchecked} true {checked}} checkbox for the alert or event in row {ariaRowindex}, with columns {columnValues}', - }); - export const ACTION_INVESTIGATE_IN_RESOLVER_FOR_ROW = ({ ariaRowindex, columnValues, diff --git a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx index cabedd84d270d..94ae06dc9a558 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx @@ -279,7 +279,7 @@ const TGridStandaloneComponent: React.FC = ({ sort, itemsPerPage, itemsPerPageOptions, - showCheckboxes: false, + showCheckboxes: true, }) ); dispatch( diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.tsx index e308996d0d45b..bac0a2eceda0c 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.tsx @@ -21,10 +21,10 @@ import { CATEGORIES_PANE_CLASS_NAME, TABLE_HEIGHT } from './helpers'; import * as i18n from './translations'; const CategoryNames = styled.div<{ height: number; width: number }>` + ${({ width }) => `width: ${width}px`}; ${({ height }) => `height: ${height}px`}; - overflow: auto; + overflow-y: hidden; padding: 5px; - ${({ width }) => `width: ${width}px`}; thead { display: none; } @@ -88,12 +88,13 @@ export const CategoriesPane = React.memo( ` ${({ height }) => `height: ${height}px`}; - overflow-x: hidden; - overflow-y: auto; ${({ width }) => `width: ${width}px`}; + overflow: hidden; `; TableContainer.displayName = 'TableContainer'; @@ -97,7 +96,7 @@ export const Category = React.memo( width={width} > { + const original = jest.requireActual('react-redux'); + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); describe('FieldsBrowser', () => { const timelineId = 'test'; - // `enzyme` doesn't mount the components into the global jsdom `document` - // but that's where the click detector listener is, so for testing, we - // pass the top-level mounted component's click event on to document - const triggerDocumentMouseDown = () => { - const event = new Event('mousedown'); - document.dispatchEvent(event); - }; + test('it renders the Close button', () => { + const wrapper = mount( + + ()} + selectedCategoryId={''} + timelineId={timelineId} + /> + + ); - const triggerDocumentMouseUp = () => { - const event = new Event('mouseup'); - document.dispatchEvent(event); - }; + expect(wrapper.find('[data-test-subj="close"]').first().text()).toEqual('Close'); + }); - test('it invokes onOutsideClick when onFieldSelected is undefined, and the user clicks outside the fields browser', () => { - const onOutsideClick = jest.fn(); + test('it invokes the Close button', () => { + const onHide = jest.fn(); + const wrapper = mount( + + ()} + selectedCategoryId={''} + timelineId={timelineId} + /> + + ); + + wrapper.find('[data-test-subj="close"]').first().simulate('click'); + expect(onHide).toBeCalled(); + }); + test('it renders the Reset Fields button', () => { const wrapper = mount( -
- ()} - selectedCategoryId={''} - timelineId={timelineId} - width={FIELD_BROWSER_WIDTH} - /> -
+ ()} + selectedCategoryId={''} + timelineId={timelineId} + />
); - wrapper.find('[data-test-subj="outside"]').simulate('mousedown'); - wrapper.find('[data-test-subj="outside"]').simulate('mouseup'); + expect(wrapper.find('[data-test-subj="reset-fields"]').first().text()).toEqual('Reset Fields'); + }); - expect(onOutsideClick).toHaveBeenCalled(); + test('it invokes updateColumns action when the user clicks the Reset Fields button', () => { + const wrapper = mount( + + ()} + selectedCategoryId={''} + timelineId={timelineId} + /> + + ); + + wrapper.find('[data-test-subj="reset-fields"]').first().simulate('click'); + + expect(mockDispatch).toBeCalledWith( + tGridActions.updateColumns({ + id: timelineId, + columns: defaultHeaders, + }) + ); }); - test('it does NOT invoke onOutsideClick when onFieldSelected is defined, and the user clicks outside the fields browser', () => { - const onOutsideClick = jest.fn(); + test('it invokes onHide when the user clicks the Reset Fields button', () => { + const onHide = jest.fn(); const wrapper = mount( -
- ()} - selectedCategoryId={''} - timelineId={timelineId} - width={FIELD_BROWSER_WIDTH} - /> -
+ ()} + selectedCategoryId={''} + timelineId={timelineId} + />
); - wrapper.find('[data-test-subj="outside"]').simulate('mousedown'); - wrapper.find('[data-test-subj="outside"]').simulate('mouseup'); + wrapper.find('[data-test-subj="reset-fields"]').first().simulate('click'); - expect(onOutsideClick).not.toHaveBeenCalled(); + expect(onHide).toBeCalled(); }); - test('it renders the header', () => { + test('it renders the search', () => { const wrapper = mount( { browserFields={mockBrowserFields} filteredBrowserFields={mockBrowserFields} searchInput={''} - height={FIELD_BROWSER_HEIGHT} isSearching={false} onCategorySelected={jest.fn()} - onHideFieldBrowser={jest.fn()} - onOutsideClick={jest.fn()} + onHide={jest.fn()} onSearchInputChange={jest.fn()} restoreFocusTo={React.createRef()} selectedCategoryId={''} timelineId={timelineId} - width={FIELD_BROWSER_WIDTH} /> ); - expect(wrapper.find('[data-test-subj="header"]').exists()).toBe(true); + expect(wrapper.find('[data-test-subj="field-search"]').exists()).toBe(true); }); test('it renders the categories pane', () => { @@ -135,16 +178,13 @@ describe('FieldsBrowser', () => { browserFields={mockBrowserFields} filteredBrowserFields={mockBrowserFields} searchInput={''} - height={FIELD_BROWSER_HEIGHT} isSearching={false} onCategorySelected={jest.fn()} - onHideFieldBrowser={jest.fn()} - onOutsideClick={jest.fn()} + onHide={jest.fn()} onSearchInputChange={jest.fn()} restoreFocusTo={React.createRef()} selectedCategoryId={''} timelineId={timelineId} - width={FIELD_BROWSER_WIDTH} /> ); @@ -160,16 +200,13 @@ describe('FieldsBrowser', () => { browserFields={mockBrowserFields} filteredBrowserFields={mockBrowserFields} searchInput={''} - height={FIELD_BROWSER_HEIGHT} isSearching={false} onCategorySelected={jest.fn()} - onHideFieldBrowser={jest.fn()} - onOutsideClick={jest.fn()} + onHide={jest.fn()} onSearchInputChange={jest.fn()} restoreFocusTo={React.createRef()} selectedCategoryId={''} timelineId={timelineId} - width={FIELD_BROWSER_WIDTH} /> ); @@ -185,16 +222,13 @@ describe('FieldsBrowser', () => { browserFields={mockBrowserFields} filteredBrowserFields={mockBrowserFields} searchInput={''} - height={FIELD_BROWSER_HEIGHT} isSearching={false} onCategorySelected={jest.fn()} - onHideFieldBrowser={jest.fn()} - onOutsideClick={jest.fn()} + onHide={jest.fn()} onSearchInputChange={jest.fn()} restoreFocusTo={React.createRef()} selectedCategoryId={''} timelineId={timelineId} - width={FIELD_BROWSER_WIDTH} /> ); @@ -216,16 +250,13 @@ describe('FieldsBrowser', () => { browserFields={mockBrowserFields} filteredBrowserFields={mockBrowserFields} searchInput={''} - height={FIELD_BROWSER_HEIGHT} isSearching={false} onCategorySelected={jest.fn()} - onHideFieldBrowser={jest.fn()} - onOutsideClick={jest.fn()} + onHide={jest.fn()} onSearchInputChange={onSearchInputChange} restoreFocusTo={React.createRef()} selectedCategoryId={''} timelineId={timelineId} - width={FIELD_BROWSER_WIDTH} /> ); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx index 8a89271797e95..a645235b620d8 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx @@ -6,16 +6,18 @@ */ import { - EuiButtonIcon, EuiFlexGroup, - EuiFocusTrap, EuiFlexItem, - EuiOutsideClickDetector, - EuiScreenReaderOnly, - EuiToolTip, + EuiModal, + EuiModalBody, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalFooter, + EuiButton, + EuiButtonEmpty, + EuiSpacer, } from '@elastic/eui'; -import React, { useEffect, useCallback, useRef } from 'react'; -import { noop } from 'lodash/fp'; +import React, { useEffect, useCallback, useRef, useMemo } from 'react'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; @@ -23,46 +25,30 @@ import type { BrowserFields, ColumnHeaderOptions } from '../../../../../common'; import { isEscape, isTab, stopPropagationAndPreventDefault } from '../../../../../common'; import { CategoriesPane } from './categories_pane'; import { FieldsPane } from './fields_pane'; -import { Header } from './header'; +import { Search } from './search'; import { CATEGORY_PANE_WIDTH, CLOSE_BUTTON_CLASS_NAME, FIELDS_PANE_WIDTH, + FIELD_BROWSER_WIDTH, focusSearchInput, onFieldsBrowserTabPressed, PANES_FLEX_GROUP_WIDTH, + RESET_FIELDS_CLASS_NAME, scrollCategoriesPane, } from './helpers'; -import type { FieldBrowserProps, OnHideFieldBrowser } from './types'; -import { tGridActions } from '../../../../store/t_grid'; +import type { FieldBrowserProps } from './types'; +import { tGridActions, tGridSelectors } from '../../../../store/t_grid'; import * as i18n from './translations'; - -const FieldsBrowserContainer = styled.div<{ width: number }>` - background-color: ${({ theme }) => theme.eui.euiColorLightestShade}; - border: ${({ theme }) => theme.eui.euiBorderWidthThin} solid - ${({ theme }) => theme.eui.euiColorMediumShade}; - border-radius: ${({ theme }) => theme.eui.euiBorderRadius}; - left: 12px; - padding: ${({ theme }) => theme.eui.paddingSizes.s} ${({ theme }) => theme.eui.paddingSizes.s} - ${({ theme }) => theme.eui.paddingSizes.s}; - position: fixed; - top: 50%; - transform: translateY(-50%); - width: ${({ width }) => width}px; - z-index: 9990; -`; -FieldsBrowserContainer.displayName = 'FieldsBrowserContainer'; +import { useDeepEqualSelector } from '../../../../hooks/use_selector'; const PanesFlexGroup = styled(EuiFlexGroup)` width: ${PANES_FLEX_GROUP_WIDTH}px; `; PanesFlexGroup.displayName = 'PanesFlexGroup'; -type Props = Pick< - FieldBrowserProps, - 'browserFields' | 'height' | 'onFieldSelected' | 'timelineId' | 'width' -> & { +type Props = Pick & { /** * The current timeline column headers */ @@ -92,11 +78,7 @@ type Props = Pick< /** * Hides the field browser when invoked */ - onHideFieldBrowser: OnHideFieldBrowser; - /** - * Invoked when the user clicks outside of the field browser - */ - onOutsideClick: () => void; + onHide: () => void; /** * Invoked when the user types in the search input */ @@ -119,15 +101,13 @@ const FieldsBrowserComponent: React.FC = ({ filteredBrowserFields, isSearching, onCategorySelected, - onFieldSelected, - onHideFieldBrowser, onSearchInputChange, - onOutsideClick, + onHide, restoreFocusTo, searchInput, selectedCategoryId, timelineId, - width, + width = FIELD_BROWSER_WIDTH, }) => { const dispatch = useDispatch(); const containerElement = useRef(null); @@ -137,6 +117,22 @@ const FieldsBrowserComponent: React.FC = ({ [dispatch, timelineId] ); + const closeAndRestoreFocus = useCallback(() => { + onHide(); + setTimeout(() => { + // restore focus on the next tick after we have escaped the EuiFocusTrap + restoreFocusTo.current?.focus(); + }, 0); + }, [onHide, restoreFocusTo]); + + const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []); + const { defaultColumns } = useDeepEqualSelector((state) => getManageTimeline(state, timelineId)); + + const onResetColumns = useCallback(() => { + onUpdateColumns(defaultColumns); + closeAndRestoreFocus(); + }, [onUpdateColumns, closeAndRestoreFocus, defaultColumns]); + /** Invoked when the user types in the input to filter the field browser */ const onInputChange = useCallback( (event: React.ChangeEvent) => { @@ -145,17 +141,6 @@ const FieldsBrowserComponent: React.FC = ({ [onSearchInputChange] ); - const selectFieldAndHide = useCallback( - (fieldId: string) => { - if (onFieldSelected != null) { - onFieldSelected(fieldId); - } - - onHideFieldBrowser(); - }, - [onFieldSelected, onHideFieldBrowser] - ); - const scrollViewsAndFocusInput = useCallback(() => { scrollCategoriesPane({ containerElement: containerElement.current, @@ -175,14 +160,6 @@ const FieldsBrowserComponent: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedCategoryId, timelineId]); - const closeAndRestoreFocus = useCallback(() => { - onOutsideClick(); - setTimeout(() => { - // restore focus on the next tick after we have escaped the EuiFocusTrap - restoreFocusTo.current?.focus(); - }, 0); - }, [onOutsideClick, restoreFocusTo]); - const onKeyDown = useCallback( (keyboardEvent: React.KeyboardEvent) => { if (isEscape(keyboardEvent)) { @@ -201,47 +178,24 @@ const FieldsBrowserComponent: React.FC = ({ ); return ( - - - - -

{i18n.YOU_ARE_IN_A_POPOVER}

-
- - - - - - - - - -
+
+ + +

{i18n.FIELDS_BROWSER}

+
+
+ + + - + = ({ data-test-subj="fields-pane" filteredBrowserFields={filteredBrowserFields} onCategorySelected={onCategorySelected} - onFieldSelected={selectFieldAndHide} onUpdateColumns={onUpdateColumns} searchInput={searchInput} selectedCategoryId={selectedCategoryId} @@ -269,9 +222,32 @@ const FieldsBrowserComponent: React.FC = ({ /> - - - + + + + + + {i18n.RESET_FIELDS} + + + + + + {i18n.CLOSE} + + + +
+ ); }; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.test.tsx index 275ce4907435f..aec21b4847136 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.test.tsx @@ -27,7 +27,6 @@ describe('FieldsPane', () => { columnHeaders={[]} filteredBrowserFields={mockBrowserFields} onCategorySelected={jest.fn()} - onFieldSelected={jest.fn()} onUpdateColumns={jest.fn()} searchInput="" selectedCategoryId={selectedCategory} @@ -51,7 +50,6 @@ describe('FieldsPane', () => { columnHeaders={[]} filteredBrowserFields={mockBrowserFields} onCategorySelected={jest.fn()} - onFieldSelected={jest.fn()} onUpdateColumns={jest.fn()} searchInput="" selectedCategoryId={selectedCategory} @@ -75,7 +73,6 @@ describe('FieldsPane', () => { columnHeaders={[]} filteredBrowserFields={{}} onCategorySelected={jest.fn()} - onFieldSelected={jest.fn()} onUpdateColumns={jest.fn()} searchInput={searchInput} selectedCategoryId="" @@ -99,7 +96,6 @@ describe('FieldsPane', () => { columnHeaders={[]} filteredBrowserFields={{}} onCategorySelected={jest.fn()} - onFieldSelected={jest.fn()} onUpdateColumns={jest.fn()} searchInput={searchInput} selectedCategoryId="" diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx index 633d1c536035a..11ad3b881b637 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx @@ -11,7 +11,6 @@ import styled from 'styled-components'; import { useDispatch } from 'react-redux'; import { Category } from './category'; -import type { FieldBrowserProps } from './types'; import { getFieldItems } from './field_items'; import { FIELDS_PANE_WIDTH, TABLE_HEIGHT } from './helpers'; @@ -33,7 +32,8 @@ const NoFieldsFlexGroup = styled(EuiFlexGroup)` NoFieldsFlexGroup.displayName = 'NoFieldsFlexGroup'; -type Props = Pick & { +interface Props { + timelineId: string; columnHeaders: ColumnHeaderOptions[]; /** * A map of categoryId -> metadata about the fields in that category, @@ -56,7 +56,7 @@ type Props = Pick & { selectedCategoryId: string; /** The width field browser */ width: number; -}; +} export const FieldsPane = React.memo( ({ columnHeaders, diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/header.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/header.tsx deleted file mode 100644 index 42ea20f1dfab8..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/header.tsx +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiButtonEmpty, - EuiFieldSearch, - EuiFlexGroup, - EuiFlexItem, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; -import styled from 'styled-components'; -import type { BrowserFields, OnUpdateColumns } from '../../../../../common'; -import { useDeepEqualSelector } from '../../../../hooks/use_selector'; -import { tGridSelectors } from '../../../../store/t_grid'; - -import { - getFieldBrowserSearchInputClassName, - getFieldCount, - RESET_FIELDS_CLASS_NAME, - SEARCH_INPUT_WIDTH, -} from './helpers'; - -import * as i18n from './translations'; - -const CountsFlexGroup = styled(EuiFlexGroup)` - margin-top: 5px; -`; - -CountsFlexGroup.displayName = 'CountsFlexGroup'; - -const CountFlexItem = styled(EuiFlexItem)` - margin-right: 5px; -`; - -CountFlexItem.displayName = 'CountFlexItem'; - -// background-color: ${props => props.theme.eui.euiColorLightestShade}; -const HeaderContainer = styled.div` - padding: 0 16px 16px 16px; - margin-bottom: 8px; -`; - -HeaderContainer.displayName = 'HeaderContainer'; - -const SearchContainer = styled.div` - input { - max-width: ${SEARCH_INPUT_WIDTH}px; - width: ${SEARCH_INPUT_WIDTH}px; - } -`; - -SearchContainer.displayName = 'SearchContainer'; - -interface Props { - filteredBrowserFields: BrowserFields; - isSearching: boolean; - onOutsideClick: () => void; - onSearchInputChange: (event: React.ChangeEvent) => void; - onUpdateColumns: OnUpdateColumns; - searchInput: string; - timelineId: string; -} - -const CountRow = React.memo>(({ filteredBrowserFields }) => ( - - - - {i18n.CATEGORIES_COUNT(Object.keys(filteredBrowserFields).length)} - - - - - - {i18n.FIELDS_COUNT( - Object.keys(filteredBrowserFields).reduce( - (fieldsCount, category) => getFieldCount(filteredBrowserFields[category]) + fieldsCount, - 0 - ) - )} - - - -)); - -CountRow.displayName = 'CountRow'; - -const TitleRow = React.memo<{ - id: string; - onOutsideClick: () => void; - onUpdateColumns: OnUpdateColumns; -}>(({ id, onOutsideClick, onUpdateColumns }) => { - const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []); - const { defaultColumns } = useDeepEqualSelector((state) => getManageTimeline(state, id)); - - const handleResetColumns = useCallback(() => { - onUpdateColumns(defaultColumns); - onOutsideClick(); - }, [onUpdateColumns, onOutsideClick, defaultColumns]); - - return ( - - - -

{i18n.FIELDS_BROWSER}

-
-
- - - - {i18n.RESET_FIELDS} - - -
- ); -}); - -TitleRow.displayName = 'TitleRow'; - -export const Header = React.memo( - ({ - isSearching, - filteredBrowserFields, - onOutsideClick, - onSearchInputChange, - onUpdateColumns, - searchInput, - timelineId, - }) => ( - - - - - - - - ) -); - -Header.displayName = 'Header'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx index 9a3559c51ef87..46aa5a00e0c3a 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx @@ -30,12 +30,11 @@ LoadingSpinner.displayName = 'LoadingSpinner'; export const CATEGORY_PANE_WIDTH = 200; export const DESCRIPTION_COLUMN_WIDTH = 300; export const FIELD_COLUMN_WIDTH = 200; -export const FIELD_BROWSER_WIDTH = 900; -export const FIELD_BROWSER_HEIGHT = 300; +export const FIELD_BROWSER_WIDTH = 925; export const FIELDS_PANE_WIDTH = 670; export const HEADER_HEIGHT = 40; export const PANES_FLEX_GROUP_WIDTH = CATEGORY_PANE_WIDTH + FIELDS_PANE_WIDTH + 10; -export const SEARCH_INPUT_WIDTH = 850; +export const PANES_FLEX_GROUP_HEIGHT = 260; export const TABLE_HEIGHT = 260; export const TYPE_COLUMN_WIDTH = 50; @@ -199,6 +198,11 @@ export const viewAllHasFocus = (containerElement: HTMLElement | null): boolean = containerElement?.querySelector(`.${VIEW_ALL_BUTTON_CLASS_NAME}`) ); +export const resetButtonHasFocus = (containerElement: HTMLElement | null): boolean => + elementOrChildrenHasFocus( + containerElement?.querySelector(`.${RESET_FIELDS_CLASS_NAME}`) + ); + export const scrollCategoriesPane = ({ containerElement, selectedCategoryId, @@ -308,15 +312,20 @@ export const onCategoryTableFocusChanging = ({ }: { containerElement: HTMLElement | null; shiftKey: boolean; -}) => (shiftKey ? focusViewAllButton(containerElement) : focusCloseButton(containerElement)); +}) => (shiftKey ? focusViewAllButton(containerElement) : focusResetFieldsButton(containerElement)); export const onCloseButtonFocusChanging = ({ containerElement, shiftKey, + timelineId, }: { containerElement: HTMLElement | null; shiftKey: boolean; -}) => (shiftKey ? focusCategoryTable(containerElement) : focusResetFieldsButton(containerElement)); + timelineId: string; +}) => + shiftKey + ? focusResetFieldsButton(containerElement) + : focusSearchInput({ containerElement, timelineId }); export const onSearchInputFocusChanging = ({ containerElement, @@ -330,7 +339,7 @@ export const onSearchInputFocusChanging = ({ timelineId: string; }) => shiftKey - ? focusResetFieldsButton(containerElement) + ? focusCloseButton(containerElement) : focusCategoriesPane({ containerElement, selectedCategoryId, timelineId }); export const onViewAllFocusChanging = ({ @@ -348,6 +357,14 @@ export const onViewAllFocusChanging = ({ ? focusCategoriesPane({ containerElement, selectedCategoryId, timelineId }) : focusCategoryTable(containerElement); +export const onResetButtonFocusChanging = ({ + containerElement, + shiftKey, +}: { + containerElement: HTMLElement | null; + shiftKey: boolean; +}) => (shiftKey ? focusCategoryTable(containerElement) : focusCloseButton(containerElement)); + export const onFieldsBrowserTabPressed = ({ containerElement, keyboardEvent, @@ -390,11 +407,18 @@ export const onFieldsBrowserTabPressed = ({ containerElement, shiftKey, }); + } else if (resetButtonHasFocus(containerElement)) { + stopPropagationAndPreventDefault(keyboardEvent); + onResetButtonFocusChanging({ + containerElement, + shiftKey, + }); } else if (closeButtonHasFocus(containerElement)) { stopPropagationAndPreventDefault(keyboardEvent); onCloseButtonFocusChanging({ containerElement, shiftKey, + timelineId, }); } }; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.test.tsx index 60c1e8da08b78..b8bc2a12ffd6e 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.test.tsx @@ -11,7 +11,7 @@ import { waitFor } from '@testing-library/react'; import { mockBrowserFields, TestProviders } from '../../../../mock'; -import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './helpers'; +import { FIELD_BROWSER_WIDTH } from './helpers'; import { StatefulFieldsBrowserComponent } from '.'; @@ -28,9 +28,7 @@ describe('StatefulFieldsBrowser', () => { ); @@ -45,9 +43,7 @@ describe('StatefulFieldsBrowser', () => { ); @@ -61,9 +57,7 @@ describe('StatefulFieldsBrowser', () => { ); @@ -84,9 +78,7 @@ describe('StatefulFieldsBrowser', () => { ); @@ -111,9 +103,7 @@ describe('StatefulFieldsBrowser', () => { ); @@ -157,10 +147,8 @@ describe('StatefulFieldsBrowser', () => { ); @@ -176,7 +164,6 @@ describe('StatefulFieldsBrowser', () => { = ({ + timelineId, columnHeaders, browserFields, - height, - onFieldSelected, - timelineId, width, }) => { const customizeColumnsButtonRef = useRef(null); @@ -65,9 +61,18 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ }, []); /** Shows / hides the field browser */ - const toggleShow = useCallback(() => { - setShow(!show); - }, [show]); + const onShow = useCallback(() => { + setShow(true); + }, []); + + /** Invoked when the field browser should be hidden */ + const onHide = useCallback(() => { + setFilterInput(''); + setFilteredBrowserFields(null); + setIsSearching(false); + setSelectedCategoryId(DEFAULT_CATEGORY_NAME); + setShow(false); + }, []); /** Invoked when the user types in the filter input */ const updateFilter = useCallback( @@ -108,16 +113,6 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ [browserFields, filterInput, inputTimeoutId.current] ); - /** Invoked when the field browser should be hidden */ - const hideFieldBrowser = useCallback(() => { - setFilterInput(''); - setFilterInput(''); - setFilteredBrowserFields(null); - setIsSearching(false); - setSelectedCategoryId(DEFAULT_CATEGORY_NAME); - setShow(false); - }, []); - // only merge in the default category if the field browser is visible const browserFieldsWithDefaultCategory = useMemo(() => { return show ? mergeBrowserFieldsWithDefaultCategory(browserFields) : {}; @@ -129,11 +124,11 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ {i18n.FIELDS} @@ -141,29 +136,22 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ {show && ( - - - + )} ); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/header.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.test.tsx similarity index 58% rename from x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/header.test.tsx rename to x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.test.tsx index 0270540fc491d..8d2c3d4714541 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/header.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.test.tsx @@ -8,100 +8,18 @@ import { mount } from 'enzyme'; import React from 'react'; import { mockBrowserFields, TestProviders } from '../../../../mock'; -import { Header } from './header'; +import { Search } from './search'; const timelineId = 'test'; -describe('Header', () => { - test('it renders the field browser title', () => { - const wrapper = mount( - -
- - ); - - expect(wrapper.find('[data-test-subj="field-browser-title"]').first().text()).toEqual('Fields'); - }); - - test('it renders the Reset Fields button', () => { - const wrapper = mount( - -
- - ); - - expect(wrapper.find('[data-test-subj="reset-fields"]').first().text()).toEqual('Reset Fields'); - }); - - test('it invokes onUpdateColumns when the user clicks the Reset Fields button', () => { - const onUpdateColumns = jest.fn(); - - const wrapper = mount( - -
- - ); - - wrapper.find('[data-test-subj="reset-fields"]').first().simulate('click'); - - expect(onUpdateColumns).toBeCalled(); - }); - - test('it invokes onOutsideClick when the user clicks the Reset Fields button', () => { - const onOutsideClick = jest.fn(); - - const wrapper = mount( - -
- - ); - - wrapper.find('[data-test-subj="reset-fields"]').first().simulate('click'); - - expect(onOutsideClick).toBeCalled(); - }); - +describe('Search', () => { test('it renders the field search input with the expected placeholder text when the searchInput prop is empty', () => { const wrapper = mount( -
@@ -118,12 +36,10 @@ describe('Header', () => { const wrapper = mount( -
@@ -136,12 +52,10 @@ describe('Header', () => { test('it renders the field search input with a spinner when isSearching is true', () => { const wrapper = mount( -
@@ -156,12 +70,10 @@ describe('Header', () => { const wrapper = mount( -
@@ -180,12 +92,10 @@ describe('Header', () => { test('it returns the expected categories count when filteredBrowserFields is empty', () => { const wrapper = mount( -
@@ -200,12 +110,10 @@ describe('Header', () => { test('it returns the expected categories count when filteredBrowserFields is NOT empty', () => { const wrapper = mount( -
@@ -220,12 +128,10 @@ describe('Header', () => { test('it returns the expected fields count when filteredBrowserFields is empty', () => { const wrapper = mount( -
@@ -238,12 +144,10 @@ describe('Header', () => { test('it returns the expected fields count when filteredBrowserFields is NOT empty', () => { const wrapper = mount( -
diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.tsx new file mode 100644 index 0000000000000..4ff41bc7e4339 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import styled from 'styled-components'; +import type { BrowserFields } from '../../../../../common'; + +import { getFieldBrowserSearchInputClassName, getFieldCount } from './helpers'; + +import * as i18n from './translations'; + +const CountsFlexGroup = styled(EuiFlexGroup)` + margin-top: ${({ theme }) => theme.eui.euiSizeXS}; + margin-left: ${({ theme }) => theme.eui.euiSizeXS}; +`; + +CountsFlexGroup.displayName = 'CountsFlexGroup'; + +interface Props { + filteredBrowserFields: BrowserFields; + isSearching: boolean; + onSearchInputChange: (event: React.ChangeEvent) => void; + searchInput: string; + timelineId: string; +} + +const CountRow = React.memo>(({ filteredBrowserFields }) => ( + + + + {i18n.CATEGORIES_COUNT(Object.keys(filteredBrowserFields).length)} + + + + + + {i18n.FIELDS_COUNT( + Object.keys(filteredBrowserFields).reduce( + (fieldsCount, category) => getFieldCount(filteredBrowserFields[category]) + fieldsCount, + 0 + ) + )} + + + +)); + +CountRow.displayName = 'CountRow'; + +export const Search = React.memo( + ({ isSearching, filteredBrowserFields, onSearchInputChange, searchInput, timelineId }) => ( + <> + + + + ) +); + +Search.displayName = 'Search'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/translations.ts index 11fcd18a00894..ac0160fad6cde 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/translations.ts +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/translations.ts @@ -36,10 +36,6 @@ export const CATEGORY_FIELDS_TABLE_CAPTION = (categoryId: string) => }, }); -export const COPY_TO_CLIPBOARD = i18n.translate('xpack.timelines.fieldBrowser.copyToClipboard', { - defaultMessage: 'Copy to Clipboard', -}); - export const CLOSE = i18n.translate('xpack.timelines.fieldBrowser.closeButton', { defaultMessage: 'Close', }); @@ -102,13 +98,6 @@ export const VIEW_ALL_CATEGORY_FIELDS = (categoryId: string) => }, }); -export const YOU_ARE_IN_A_POPOVER = i18n.translate( - 'xpack.timelines.fieldBrowser.youAreInAPopoverScreenReaderOnly', - { - defaultMessage: 'You are in the Customize Columns popup. To exit this popup, press Escape.', - } -); - export const VIEW_COLUMN = (field: string) => i18n.translate('xpack.timelines.fieldBrowser.viewColumnCheckboxAriaLabel', { values: { field }, diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/types.ts b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/types.ts index ebd72083a2bfe..2932f30cafca7 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/types.ts +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/types.ts @@ -9,25 +9,16 @@ import type { BrowserFields } from '../../../../../common/search_strategy/index_ import type { ColumnHeaderOptions } from '../../../../../common/types/timeline/columns'; export type OnFieldSelected = (fieldId: string) => void; -export type OnHideFieldBrowser = () => void; export interface FieldBrowserProps { + /** The timeline associated with this field browser */ + timelineId: string; /** The timeline's current column headers */ columnHeaders: ColumnHeaderOptions[]; /** A map of categoryId -> metadata about the fields in that category */ browserFields: BrowserFields; - /** The height of the field browser */ - height: number; /** When true, this Fields Browser is being used as an "events viewer" */ isEventViewer?: boolean; - /** - * Overrides the default behavior of the `FieldBrowser` to enable - * "selection" mode, where a field is selected by clicking a button - * instead of dragging it to the timeline - */ - onFieldSelected?: OnFieldSelected; - /** The timeline associated with this field browser */ - timelineId: string; /** The width of the field browser */ - width: number; + width?: number; } diff --git a/x-pack/plugins/timelines/public/methods/index.tsx b/x-pack/plugins/timelines/public/methods/index.tsx index f99594195842b..fa0ad55d065a3 100644 --- a/x-pack/plugins/timelines/public/methods/index.tsx +++ b/x-pack/plugins/timelines/public/methods/index.tsx @@ -11,11 +11,7 @@ import type { Store } from 'redux'; import type { Storage } from '../../../../../src/plugins/kibana_utils/public'; import type { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import type { TGridProps } from '../types'; -import type { - LastUpdatedAtProps, - LoadingPanelProps, - FieldBrowserWrappedProps, -} from '../components'; +import type { LastUpdatedAtProps, LoadingPanelProps, FieldBrowserProps } from '../components'; import type { AddToCaseActionProps } from '../components/actions/timeline/cases/add_to_case_action'; const TimelineLazy = lazy(() => import('../components')); @@ -59,10 +55,7 @@ export const getLoadingPanelLazy = (props: LoadingPanelProps) => { }; const FieldsBrowserLazy = lazy(() => import('../components/fields_browser')); -export const getFieldsBrowserLazy = ( - props: FieldBrowserWrappedProps, - { store }: { store: Store } -) => { +export const getFieldsBrowserLazy = (props: FieldBrowserProps, { store }: { store: Store }) => { return ( }> diff --git a/x-pack/plugins/timelines/public/mock/global_state.ts b/x-pack/plugins/timelines/public/mock/global_state.ts index f7d3297738373..610d1b26f2351 100644 --- a/x-pack/plugins/timelines/public/mock/global_state.ts +++ b/x-pack/plugins/timelines/public/mock/global_state.ts @@ -17,6 +17,7 @@ export const mockGlobalState: TimelineState = { start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z', }, + dataProviders: [], deletedEventIds: [], excludedRowRendererIds: [], expandedDetail: {}, diff --git a/x-pack/plugins/timelines/public/mock/mock_hover_actions.tsx b/x-pack/plugins/timelines/public/mock/mock_hover_actions.tsx index 52ea1fa827136..5a8afb2036abf 100644 --- a/x-pack/plugins/timelines/public/mock/mock_hover_actions.tsx +++ b/x-pack/plugins/timelines/public/mock/mock_hover_actions.tsx @@ -8,28 +8,9 @@ import React from 'react'; /* eslint-disable react/display-name */ export const mockHoverActions = { - addToTimeline: { - AddToTimelineButton: () => <>{'Add To Timeline'}, - keyboardShortcut: 'timelineAddShortcut', - useGetHandleStartDragToTimeline: () => jest.fn, - }, - columnToggle: { - ColumnToggleButton: () => <>{'Column Toggle'}, - columnToggleFn: jest.fn, - keyboardShortcut: 'columnToggleShortcut', - }, - copy: { - CopyButton: () => <>{'Copy button'}, - keyboardShortcut: 'copyShortcut', - }, - filterForValue: { - FilterForValueButton: () => <>{'Filter button'}, - filterForValueFn: jest.fn, - keyboardShortcut: 'filterForShortcut', - }, - filterOutValue: { - FilterOutValueButton: () => <>{'Filter out button'}, - filterOutValueFn: jest.fn, - keyboardShortcut: 'filterOutShortcut', - }, + getAddToTimelineButton: () => <>{'Add To Timeline'}, + getColumnToggleButton: () => <>{'Column Toggle'}, + getCopyButton: () => <>{'Copy button'}, + getFilterForValueButton: () => <>{'Filter button'}, + getFilterOutValueButton: () => <>{'Filter out button'}, }; diff --git a/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts b/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts index 31b6ea9e665ac..56631c498c755 100644 --- a/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts +++ b/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts @@ -1549,6 +1549,7 @@ export const mockTgridModel: TGridModel = { initialWidth: 180, }, ], + dataProviders: [], defaultColumns: [], queryFields: [], dateRange: { diff --git a/x-pack/plugins/timelines/public/plugin.ts b/x-pack/plugins/timelines/public/plugin.ts index 29d84331cffaa..24bc99e59aaf0 100644 --- a/x-pack/plugins/timelines/public/plugin.ts +++ b/x-pack/plugins/timelines/public/plugin.ts @@ -14,7 +14,7 @@ import type { PluginInitializerContext, CoreStart, } from '../../../../src/core/public'; -import type { LastUpdatedAtProps, LoadingPanelProps, FieldBrowserWrappedProps } from './components'; +import type { LastUpdatedAtProps, LoadingPanelProps, FieldBrowserProps } from './components'; import { getLastUpdatedLazy, getLoadingPanelLazy, @@ -26,7 +26,7 @@ import type { TimelinesUIStart, TGridProps, TimelinesStartPlugins } from './type import { tGridReducer } from './store/t_grid/reducer'; import { useDraggableKeyboardWrapper } from './components/drag_and_drop/draggable_keyboard_wrapper_hook'; import { useAddToTimeline, useAddToTimelineSensor } from './hooks/use_add_to_timeline'; -import * as hoverActions from './components/hover_actions'; +import { getHoverActions } from './components/hover_actions'; export class TimelinesPlugin implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} private _store: Store | undefined; @@ -41,7 +41,7 @@ export class TimelinesPlugin implements Plugin { } return { getHoverActions: () => { - return hoverActions; + return getHoverActions(this._store!); }, getTGrid: (props: TGridProps) => { return getTGridLazy(props, { @@ -60,7 +60,7 @@ export class TimelinesPlugin implements Plugin { getLastUpdated: (props: LastUpdatedAtProps) => { return getLastUpdatedLazy(props); }, - getFieldBrowser: (props: FieldBrowserWrappedProps) => { + getFieldBrowser: (props: FieldBrowserProps) => { return getFieldsBrowserLazy(props, { store: this._store!, }); diff --git a/x-pack/plugins/timelines/public/store/t_grid/actions.ts b/x-pack/plugins/timelines/public/store/t_grid/actions.ts index 6d9e9e5bc7379..64c4d8a78c7ac 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/actions.ts +++ b/x-pack/plugins/timelines/public/store/t_grid/actions.ts @@ -9,6 +9,7 @@ import actionCreatorFactory from 'typescript-fsa'; import type { TimelineNonEcsData } from '../../../common/search_strategy'; import type { ColumnHeaderOptions, + DataProvider, SortColumnTimeline, TimelineExpandedDetailType, } from '../../../common/types/timeline'; @@ -100,3 +101,7 @@ export const initializeTGridSettings = actionCreator('I export const setTGridSelectAll = actionCreator<{ id: string; selectAll: boolean }>( 'SET_TGRID_SELECT_ALL' ); + +export const addProviderToTimeline = actionCreator<{ id: string; dataProvider: DataProvider }>( + 'ADD_PROVIDER_TO_TIMELINE' +); diff --git a/x-pack/plugins/timelines/public/store/t_grid/helpers.ts b/x-pack/plugins/timelines/public/store/t_grid/helpers.ts index 8bcf246dadb03..dd056f1e9237a 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/helpers.ts +++ b/x-pack/plugins/timelines/public/store/t_grid/helpers.ts @@ -12,7 +12,11 @@ import type { ToggleDetailPanel } from './actions'; import { TGridPersistInput, TimelineById, TimelineId } from './types'; import type { TGridModel, TGridModelSettings } from './model'; -import type { ColumnHeaderOptions, SortColumnTimeline } from '../../../common/types/timeline'; +import type { + ColumnHeaderOptions, + DataProvider, + SortColumnTimeline, +} from '../../../common/types/timeline'; import { getTGridManageDefaults, tGridDefaults } from './defaults'; export const isNotNull = (value: T | null): value is T => value !== null; @@ -421,3 +425,35 @@ export const updateTimelineDetailsPanel = (action: ToggleDetailPanel) => { [expandedTabType]: {}, }; }; + +export const addProviderToTimelineHelper = ( + id: string, + provider: DataProvider, + timelineById: TimelineById +): TimelineById => { + const timeline = timelineById[id]; + const alreadyExistsAtIndex = timeline.dataProviders.findIndex((p) => p.id === provider.id); + + if (alreadyExistsAtIndex > -1 && !isEmpty(timeline.dataProviders[alreadyExistsAtIndex].and)) { + provider.id = `${provider.id}-${ + timeline.dataProviders.filter((p) => p.id === provider.id).length + }`; + } + + const dataProviders = + alreadyExistsAtIndex > -1 && isEmpty(timeline.dataProviders[alreadyExistsAtIndex].and) + ? [ + ...timeline.dataProviders.slice(0, alreadyExistsAtIndex), + provider, + ...timeline.dataProviders.slice(alreadyExistsAtIndex + 1), + ] + : [...timeline.dataProviders, provider]; + + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders, + }, + }; +}; diff --git a/x-pack/plugins/timelines/public/store/t_grid/model.ts b/x-pack/plugins/timelines/public/store/t_grid/model.ts index 2d4f9f0fca35f..4ed4448f4cf35 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/model.ts +++ b/x-pack/plugins/timelines/public/store/t_grid/model.ts @@ -10,6 +10,7 @@ import type { Filter, FilterManager } from '../../../../../../src/plugins/data/p import type { TimelineNonEcsData } from '../../../common/search_strategy'; import type { ColumnHeaderOptions, + DataProvider, TimelineExpandedDetail, SortColumnTimeline, SerializedFilterQuery, @@ -39,6 +40,8 @@ export interface TGridModel extends TGridModelSettings { Pick & ColumnHeaderOptions >; + /** The sources of the event data shown in the timeline */ + dataProviders: DataProvider[]; /** Specifies the granularity of the date range (e.g. 1 Day / Week / Month) applicable to the mini-map */ dateRange: { start: string; @@ -81,6 +84,7 @@ export interface TGridModel extends TGridModelSettings { export type TGridModelForTimeline = Pick< TGridModel, | 'columns' + | 'dataProviders' | 'dateRange' | 'deletedEventIds' | 'excludedRowRendererIds' diff --git a/x-pack/plugins/timelines/public/store/t_grid/reducer.ts b/x-pack/plugins/timelines/public/store/t_grid/reducer.ts index 57c45f857554d..751837691ea10 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/reducer.ts +++ b/x-pack/plugins/timelines/public/store/t_grid/reducer.ts @@ -7,6 +7,7 @@ import { reducerWithInitialState } from 'typescript-fsa-reducers'; import { + addProviderToTimeline, applyDeltaToColumnWidth, clearEventsDeleted, clearEventsLoading, @@ -28,6 +29,7 @@ import { } from './actions'; import { + addProviderToTimelineHelper, applyDeltaToTimelineColumnWidth, createInitTGrid, setInitializeTgridSettings, @@ -209,4 +211,8 @@ export const tGridReducer = reducerWithInitialState(initialTGridState) }, }, })) + .case(addProviderToTimeline, (state, { id, dataProvider }) => ({ + ...state, + timelineById: addProviderToTimelineHelper(id, dataProvider, state.timelineById), + })) .build(); diff --git a/x-pack/plugins/timelines/public/types.ts b/x-pack/plugins/timelines/public/types.ts index 13e5a52776f8d..782481d79e0c4 100644 --- a/x-pack/plugins/timelines/public/types.ts +++ b/x-pack/plugins/timelines/public/types.ts @@ -14,7 +14,7 @@ import { CasesUiStart } from '../../cases/public'; import type { LastUpdatedAtProps, LoadingPanelProps, - FieldBrowserWrappedProps, + FieldBrowserProps, UseDraggableKeyboardWrapper, UseDraggableKeyboardWrapperProps, } from './components'; @@ -33,7 +33,7 @@ export interface TimelinesUIStart { getTGridReducer: () => any; getLoadingPanel: (props: LoadingPanelProps) => ReactElement; getLastUpdated: (props: LastUpdatedAtProps) => ReactElement; - getFieldBrowser: (props: FieldBrowserWrappedProps) => ReactElement; + getFieldBrowser: (props: FieldBrowserProps) => ReactElement; getUseAddToTimeline: () => (props: UseAddToTimelineProps) => UseAddToTimeline; getUseAddToTimelineSensor: () => (api: SensorAPI) => void; getUseDraggableKeyboardWrapper: () => ( diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx index 07f4072e02b85..dff2ba17cb3f0 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx @@ -247,6 +247,7 @@ export const ExpandedRow: FC = ({ item }) => { onTabClick={() => {}} expand={false} style={{ width: '100%' }} + data-test-subj="transformExpandedRowTabbedContent" /> ); }; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_search_bar_filters.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_search_bar_filters.tsx index 7794d65934d6d..d9ee384f3ec69 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_search_bar_filters.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_search_bar_filters.tsx @@ -9,7 +9,12 @@ import React from 'react'; import { EuiBadge, SearchFilterConfig } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { TermClause, FieldClause, Value } from './common'; -import { TRANSFORM_MODE, TRANSFORM_STATE } from '../../../../../../common/constants'; +import { + TRANSFORM_FUNCTION, + TRANSFORM_MODE, + TRANSFORM_STATE, +} from '../../../../../../common/constants'; +import { isLatestTransform, isPivotTransform } from '../../../../../../common/types/transform'; import { TransformListRow } from '../../../../common'; import { getTaskStateBadge } from './use_columns'; @@ -93,7 +98,20 @@ export const filterTransforms = ( // the status value is an array of string(s) e.g. ['failed', 'stopped'] ts = transforms.filter((transform) => (c.value as Value[]).includes(transform.stats.state)); } else { - ts = transforms.filter((transform) => transform.mode === c.value); + ts = transforms.filter((transform) => { + if (c.field === 'mode') { + return transform.mode === c.value; + } + if (c.field === 'type') { + if (c.value === TRANSFORM_FUNCTION.PIVOT) { + return isPivotTransform(transform.config); + } + if (c.value === TRANSFORM_FUNCTION.LATEST) { + return isLatestTransform(transform.config); + } + } + return false; + }); } } diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx index f3974430b662c..af2325ede2021 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx @@ -20,13 +20,14 @@ describe('Transform: Job List Columns', () => { const columns: ReturnType['columns'] = result.current.columns; - expect(columns).toHaveLength(7); + expect(columns).toHaveLength(8); expect(columns[0].isExpander).toBeTruthy(); expect(columns[1].name).toBe('ID'); expect(columns[2].name).toBe('Description'); - expect(columns[3].name).toBe('Status'); - expect(columns[4].name).toBe('Mode'); - expect(columns[5].name).toBe('Progress'); - expect(columns[6].name).toBe('Actions'); + expect(columns[3].name).toBe('Type'); + expect(columns[4].name).toBe('Status'); + expect(columns[5].name).toBe('Mode'); + expect(columns[6].name).toBe('Progress'); + expect(columns[7].name).toBe('Actions'); }); }); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx index e186acf31d34f..dbdd3409c7e34 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx @@ -23,7 +23,11 @@ import { RIGHT_ALIGNMENT, } from '@elastic/eui'; -import { TransformId } from '../../../../../../common/types/transform'; +import { + isLatestTransform, + isPivotTransform, + TransformId, +} from '../../../../../../common/types/transform'; import { TransformStats } from '../../../../../../common/types/transform_stats'; import { TRANSFORM_STATE } from '../../../../../../common/constants'; @@ -95,6 +99,7 @@ export const useColumns = ( EuiTableComputedColumnType, EuiTableComputedColumnType, EuiTableComputedColumnType, + EuiTableComputedColumnType, EuiTableActionsColumnType ] = [ { @@ -145,6 +150,27 @@ export const useColumns = ( sortable: true, truncateText: true, }, + { + name: i18n.translate('xpack.transform.type', { defaultMessage: 'Type' }), + 'data-test-subj': 'transformListColumnType', + sortable: (item: TransformListRow) => item.mode, + truncateText: true, + render(item: TransformListRow) { + let transformType = i18n.translate('xpack.transform.type.unknown', { + defaultMessage: 'unknown', + }); + if (isPivotTransform(item.config) === true) { + transformType = i18n.translate('xpack.transform.type.pivot', { defaultMessage: 'pivot' }); + } + if (isLatestTransform(item.config) === true) { + transformType = i18n.translate('xpack.transform.type.latest', { + defaultMessage: 'latest', + }); + } + return {transformType}; + }, + width: '100px', + }, { name: i18n.translate('xpack.transform.status', { defaultMessage: 'Status' }), 'data-test-subj': 'transformListColumnStatus', diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e8a997e9361bf..8b19f3577bcfc 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1715,7 +1715,6 @@ "discover.howToChangeTheTimeTooltip": "時刻を変更するには、グローバル時刻フィルターを使用します。", "discover.howToSeeOtherMatchingDocumentsDescription": "これらは検索条件に一致した初めの {sampleSize} 件のドキュメントです。他の結果を表示するには検索条件を絞ってください。", "discover.howToSeeOtherMatchingDocumentsDescriptionGrid": "これらは検索条件に一致した初めの {sampleSize} 件のドキュメントです。他の結果を表示するには検索条件を絞ってください。", - "discover.inspectorRequestDataTitle": "データ", "discover.inspectorRequestDescriptionDocument": "このリクエストはElasticsearchにクエリをかけ、検索データを取得します。", "discover.json.codeEditorAriaLabel": "Elasticsearch ドキュメントの JSON ビューのみを読み込む", "discover.json.copyToClipboardLabel": "クリップボードにコピー", @@ -5913,11 +5912,6 @@ "xpack.apm.serviceNodeNameMissing": " (空) ", "xpack.apm.serviceOveriew.errorsTableOccurrences": "{occurrencesCount} occ.", "xpack.apm.serviceOverview.dependenciesTableColumnBackend": "バックエンド", - "xpack.apm.serviceOverview.dependenciesTableColumnErrorRate": "エラー率", - "xpack.apm.serviceOverview.dependenciesTableColumnImpact": "インパクト", - "xpack.apm.serviceOverview.dependenciesTableColumnLatency": "レイテンシ (平均) ", - "xpack.apm.serviceOverview.dependenciesTableColumnThroughput": "スループット", - "xpack.apm.serviceOverview.dependenciesTableLinkText": "サービスマップを表示", "xpack.apm.serviceOverview.dependenciesTableTitle": "依存関係", "xpack.apm.serviceOverview.errorsTableColumnLastSeen": "前回の認識", "xpack.apm.serviceOverview.errorsTableColumnName": "名前", @@ -13319,12 +13313,10 @@ "xpack.lens.breadcrumbsByValue": "ビジュアライゼーションを編集", "xpack.lens.breadcrumbsCreate": "作成", "xpack.lens.breadcrumbsTitle": "Visualizeライブラリ", - "xpack.lens.chartSwitch.dataLossDescription": "このグラフタイプを選択すると、現在適用されている構成選択の一部が失われます。", "xpack.lens.chartSwitch.dataLossLabel": "警告", "xpack.lens.chartSwitch.experimentalLabel": "実験的", "xpack.lens.chartSwitch.noResults": "{term}の結果が見つかりませんでした。", "xpack.lens.chartTitle.unsaved": "保存されていないビジュアライゼーション", - "xpack.lens.configPanel.chartType": "チャートタイプ", "xpack.lens.configPanel.color.tooltip.auto": "カスタム色を指定しない場合、Lensは自動的に色を選択します。", "xpack.lens.configPanel.color.tooltip.custom": "[自動]モードに戻すには、カスタム色をオフにしてください。", "xpack.lens.configPanel.color.tooltip.disabled": "レイヤーに「内訳条件」が含まれている場合は、個別の系列をカスタム色にできません。", @@ -13548,7 +13540,6 @@ "xpack.lens.indexPattern.cardinality": "ユニークカウント", "xpack.lens.indexPattern.cardinality.signature": "フィールド:文字列", "xpack.lens.indexPattern.cardinalityOf": "{name} のユニークカウント", - "xpack.lens.indexPattern.changeIndexPatternTitle": "インデックスパターンを変更", "xpack.lens.indexPattern.chooseField": "フィールドを選択", "xpack.lens.indexPattern.chooseFieldLabel": "この関数を使用するには、フィールドを選択してください。", "xpack.lens.indexPattern.chooseSubFunction": "サブ関数を選択", @@ -13782,7 +13773,6 @@ "xpack.lens.indexPatterns.actionsPopoverLabel": "インデックスパターン設定", "xpack.lens.indexPatterns.addFieldButton": "フィールドをインデックスパターンに追加", "xpack.lens.indexPatterns.clearFiltersLabel": "名前とタイプフィルターを消去", - "xpack.lens.indexPatterns.fieldFiltersLabel": "フィールドフィルター", "xpack.lens.indexPatterns.filterByNameLabel": "検索フィールド名", "xpack.lens.indexPatterns.manageFieldButton": "インデックスパターンを管理", "xpack.lens.indexPatterns.noAvailableDataLabel": "データを含むフィールドはありません。", @@ -13812,7 +13802,6 @@ "xpack.lens.pie.groupLabel": "比率", "xpack.lens.pie.groupsizeLabel": "サイズ単位", "xpack.lens.pie.pielabel": "円", - "xpack.lens.pie.pieWithNegativeWarningLabel": "{chartType}グラフは負の値では表示できません。別のグラフタイプを試してください。", "xpack.lens.pie.sliceGroupLabel": "スライス", "xpack.lens.pie.suggestionLabel": "{chartName}", "xpack.lens.pie.treemapGroupLabel": "グループ分けの条件", @@ -13910,7 +13899,6 @@ "xpack.lens.visualizeGeoFieldMessage": "Lensは{fieldType}フィールドを可視化できません", "xpack.lens.xyChart.addLayer": "レイヤーを追加", "xpack.lens.xyChart.addLayerButton": "レイヤーを追加", - "xpack.lens.xyChart.addLayerTooltip": "複数のレイヤーを使用すると、グラフタイプを組み合わせたり、別のインデックスパターンを可視化したりすることができます。", "xpack.lens.xyChart.axisExtent.custom": "カスタム", "xpack.lens.xyChart.axisExtent.dataBounds": "データ境界", "xpack.lens.xyChart.axisExtent.disabledDataBoundsMessage": "折れ線グラフのみをデータ境界に合わせることができます", @@ -19120,9 +19108,6 @@ "xpack.reporting.diagnostic.configSizeMismatch": "xpack.reporting.{KIBANA_MAX_SIZE_BYTES_PATH} ({kibanaMaxContentBytes}) はElasticSearchの{ES_MAX_SIZE_BYTES_PATH} ({elasticSearchMaxContentBytes}) を超えています。ElasticSearchで一致する{ES_MAX_SIZE_BYTES_PATH}を設定してください。あるいは、Kibanaでxpack.reporting.{KIBANA_MAX_SIZE_BYTES_PATH}を低くしてください。", "xpack.reporting.diagnostic.noUsableSandbox": "Chromiumサンドボックスを使用できません。これは「xpack.reporting.capture.browser.chromium.disableSandbox」で無効にすることができます。この作業はご自身の責任で行ってください。{url}を参照してください", "xpack.reporting.diagnostic.screenshotFailureMessage": "Kibanaインストールのスクリーンショットを作成できませんでした。", - "xpack.reporting.errorButton.showReportErrorAriaLabel": "レポートエラーを表示", - "xpack.reporting.errorButton.unableToFetchReportContentTitle": "レポートのコンテンツを取得できません", - "xpack.reporting.errorButton.unableToGenerateReportTitle": "レポートを生成できません", "xpack.reporting.exportTypes.common.failedToDecryptReportJobDataErrorMessage": "レポートジョブデータの解読に失敗しました。{encryptionKey}が設定されていることを確認してこのレポートを再生成してください。{err}", "xpack.reporting.exportTypes.common.missingJobHeadersErrorMessage": "ジョブヘッダーがありません", "xpack.reporting.exportTypes.csv.executeJob.dateFormateSetting": "Kibana の高度な設定「{dateFormatTimezone}」が「ブラウザー」に設定されています。あいまいさを避けるために日付は UTC 形式に変換されます。", @@ -19133,7 +19118,6 @@ "xpack.reporting.exportTypes.printablePdf.logoDescription": "Elastic 提供", "xpack.reporting.exportTypes.printablePdf.pagingDescription": "{pageCount} ページ中 {currentPage} ページ目", "xpack.reporting.jobsQuery.deleteError": "レポートを削除できません:{error}", - "xpack.reporting.jobStatuses.cancelledText": "キャンセル済み", "xpack.reporting.jobStatuses.completedText": "完了", "xpack.reporting.jobStatuses.failedText": "失敗", "xpack.reporting.jobStatuses.pendingText": "保留中", @@ -19159,7 +19143,6 @@ "xpack.reporting.listing.reports.subtitle": "Kibanaアプリケーションで生成されたレポートを取得します。", "xpack.reporting.listing.reportstitle": "レポート", "xpack.reporting.listing.table.captionDescription": "Kibanaアプリケーションでレポートが生成されました", - "xpack.reporting.listing.table.csvContainsFormulas": "CSVには、スプレッドシートアプリケーションで式と解釈される可能性のある文字が含まれています。", "xpack.reporting.listing.table.deleteCancelButton": "キャンセル", "xpack.reporting.listing.table.deleteConfim": "{reportTitle} レポートを削除しました", "xpack.reporting.listing.table.deleteConfirmButton": "削除", @@ -19171,17 +19154,12 @@ "xpack.reporting.listing.table.downloadReport": "レポートをダウンロード", "xpack.reporting.listing.table.downloadReportAriaLabel": "レポートをダウンロード", "xpack.reporting.listing.table.loadingReportsDescription": "レポートを読み込み中です", - "xpack.reporting.listing.table.maxSizeReachedTooltip": "最大サイズに達成、部分データが含まれています。", "xpack.reporting.listing.table.noCreatedReportsDescription": "レポートが作成されていません", "xpack.reporting.listing.table.requestFailedErrorMessage": "リクエストに失敗しました", "xpack.reporting.listing.tableColumns.actionsTitle": "アクション", "xpack.reporting.listing.tableColumns.createdAtTitle": "作成日時:", "xpack.reporting.listing.tableColumns.reportTitle": "レポート", "xpack.reporting.listing.tableColumns.statusTitle": "ステータス", - "xpack.reporting.listing.tableValue.statusDetail.maxSizeReachedText": " - 最大サイズに達成", - "xpack.reporting.listing.tableValue.statusDetail.pendingStatusReachedText": "保留中 - ジョブの処理持ち", - "xpack.reporting.listing.tableValue.statusDetail.statusTimestampText": "{statusTimestamp} 時点で {statusLabel}", - "xpack.reporting.listing.tableValue.statusDetail.warningsText": "エラー発生:詳細はジョブ情報をご覧ください。", "xpack.reporting.management.reportingTitle": "レポート", "xpack.reporting.panelContent.advancedOptions": "高度なオプション", "xpack.reporting.panelContent.copyUrlButtonLabel": "POST URL をコピー", @@ -20507,17 +20485,7 @@ "xpack.securitySolution.detectionEngine.alerts.documentTypeTitle": "アラート", "xpack.securitySolution.detectionEngine.alerts.histogram.allOthersGroupingLabel": "その他すべて", "xpack.securitySolution.detectionEngine.alerts.histogram.headerTitle": "傾向", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.destinationIpsDropDown": "上位のデスティネーションIP", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.eventActionsDropDown": "上位のイベントアクション", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.eventCategoriesDropDown": "上位のイベントカテゴリー", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.hostNamesDropDown": "上位のホスト名", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.riskScoresDropDown": "リスクスコア", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.rulesDropDown": "上位のルール", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.ruleTypesDropDown": "上位のルールタイプ", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.severitiesDropDown": "重要度", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.sourceIpsDropDown": "上位のソースIP", "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.stackByLabel": "積み上げ", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.usersDropDown": "上位のユーザー", "xpack.securitySolution.detectionEngine.alerts.histogram.topNLabel": "トップ{fieldName}", "xpack.securitySolution.detectionEngine.alerts.histogram.viewAlertsButtonLabel": "アラートを表示", "xpack.securitySolution.detectionEngine.alerts.inProgressAlertFailedToastMessage": "アラートを実行中に設定できませんでした", @@ -22559,7 +22527,6 @@ "xpack.securitySolution.timeline.autosave.warning.title": "更新されるまで自動保存は無効です", "xpack.securitySolution.timeline.body.actions.addNotesForRowAriaLabel": "行 {ariaRowindex}、列 {columnValues} のイベントのメモをタイムラインに追加", "xpack.securitySolution.timeline.body.actions.attachAlertToCaseForRowAriaLabel": "行 {ariaRowindex}、列 {columnValues} のアラートまたはイベントをケースに追加", - "xpack.securitySolution.timeline.body.actions.checkboxForRowAriaLabel": "行 {ariaRowindex}、列 {columnValues} のアラートまたはイベントのチェックボックスを{checked, select, false {オフ} true {オン}}", "xpack.securitySolution.timeline.body.actions.collapseAriaLabel": "縮小", "xpack.securitySolution.timeline.body.actions.expandEventTooltip": "詳細を表示", "xpack.securitySolution.timeline.body.actions.investigateInResolverDisabledTooltip": "このイベントを分析できません。フィールドマッピングの互換性がありません", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 2dae16efd76b0..b117bf714710b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1721,11 +1721,9 @@ "discover.helpMenu.appName": "Discover", "discover.hideChart": "隐藏图表", "discover.histogramOfFoundDocumentsAriaLabel": "已找到文档的直方图", - "discover.hitsPluralTitle": "{hits, plural, other {命中}}", "discover.howToChangeTheTimeTooltip": "要更改时间,请使用全局时间筛选。", "discover.howToSeeOtherMatchingDocumentsDescription": "下面是与您的搜索匹配的前 {sampleSize} 个文档,请优化您的搜索以查看其他文档。", "discover.howToSeeOtherMatchingDocumentsDescriptionGrid": "下面是与您的搜索匹配的前 {sampleSize} 个文档,请优化您的搜索以查看其他文档。", - "discover.inspectorRequestDataTitle": "数据", "discover.inspectorRequestDescriptionDocument": "此请求将查询 Elasticsearch 以获取搜索的数据。", "discover.json.codeEditorAriaLabel": "Elasticsearch 文档的只读 JSON 视图", "discover.json.copyToClipboardLabel": "复制到剪贴板", @@ -5949,11 +5947,6 @@ "xpack.apm.serviceNodeNameMissing": "(空)", "xpack.apm.serviceOveriew.errorsTableOccurrences": "{occurrencesCount} 次", "xpack.apm.serviceOverview.dependenciesTableColumnBackend": "后端", - "xpack.apm.serviceOverview.dependenciesTableColumnErrorRate": "错误率", - "xpack.apm.serviceOverview.dependenciesTableColumnImpact": "影响", - "xpack.apm.serviceOverview.dependenciesTableColumnLatency": "延迟(平均值)", - "xpack.apm.serviceOverview.dependenciesTableColumnThroughput": "吞吐量", - "xpack.apm.serviceOverview.dependenciesTableLinkText": "查看服务地图", "xpack.apm.serviceOverview.dependenciesTableTitle": "依赖项", "xpack.apm.serviceOverview.errorsTableColumnLastSeen": "最后看到时间", "xpack.apm.serviceOverview.errorsTableColumnName": "名称", @@ -7504,12 +7497,190 @@ "expressionRevealImage.functions.revealImageHelpText": "配置图像显示元素。", "expressionShape.renderer.shape.displayName": "形状", "expressionShape.renderer.shape.helpDescription": "呈现基本形状", + "expressionShape.functions.shape.args.borderHelpText": "形状轮廓边框的 {SVG} 颜色。", + "expressionShape.functions.shape.args.borderWidthHelpText": "边框的粗细。", + "expressionShape.functions.shape.args.fillHelpText": "填充形状的 {SVG} 颜色。", + "expressionShape.functions.shape.args.maintainAspectHelpText": "维持形状的原始纵横比?", + "expressionShape.functions.shape.args.shapeHelpText": "选取形状。", + "expressionShape.functions.shapeHelpText": "创建形状。", "expressionError.errorComponent.description": "表达式失败,并显示消息:", "expressionError.errorComponent.title": "哎哟!表达式失败", "expressionError.renderer.debug.displayName": "故障排查", "expressionError.renderer.debug.helpDescription": "将故障排查输出呈现为带格式的 {JSON}", "expressionError.renderer.error.displayName": "错误信息", "expressionError.renderer.error.helpDescription": "以用户友好的方式呈现错误数据", + "xpack.cases.addConnector.title": "添加连接器", + "xpack.cases.allCases.actions": "操作", + "xpack.cases.allCases.comments": "注释", + "xpack.cases.allCases.noTagsAvailable": "没有可用标记", + "xpack.cases.caseTable.addNewCase": "添加新案例", + "xpack.cases.caseTable.bulkActions": "批处理操作", + "xpack.cases.caseTable.bulkActions.closeSelectedTitle": "关闭所选", + "xpack.cases.caseTable.bulkActions.deleteSelectedTitle": "删除所选", + "xpack.cases.caseTable.bulkActions.markInProgressTitle": "标记为进行中", + "xpack.cases.caseTable.bulkActions.openSelectedTitle": "打开所选", + "xpack.cases.caseTable.caseDetailsLinkAria": "单击以访问标题为 {detailName} 的案例", + "xpack.cases.caseTable.changeStatus": "更改状态", + "xpack.cases.caseTable.closed": "已关闭", + "xpack.cases.caseTable.closedCases": "已关闭案例", + "xpack.cases.caseTable.delete": "删除", + "xpack.cases.caseTable.incidentSystem": "事件管理系统", + "xpack.cases.caseTable.inProgressCases": "进行中的案例", + "xpack.cases.caseTable.noCases.body": "没有可显示的案例。请创建新案例或在上面更改您的筛选设置。", + "xpack.cases.caseTable.noCases.readonly.body": "没有可显示的案例。请在上面更改您的筛选设置。", + "xpack.cases.caseTable.noCases.title": "无案例", + "xpack.cases.caseTable.notPushed": "未推送", + "xpack.cases.caseTable.openCases": "未结案例", + "xpack.cases.caseTable.pushLinkAria": "单击可在 { thirdPartyName } 上查看该事件。", + "xpack.cases.caseTable.refreshTitle": "刷新", + "xpack.cases.caseTable.requiresUpdate": " 需要更新", + "xpack.cases.caseTable.searchAriaLabel": "搜索案例", + "xpack.cases.caseTable.searchPlaceholder": "例如案例名", + "xpack.cases.caseTable.selectedCasesTitle": "已选择 {totalRules} 个{totalRules, plural, other {案例}}", + "xpack.cases.caseTable.showingCasesTitle": "正在显示 {totalRules} 个{totalRules, plural, other {案例}}", + "xpack.cases.caseTable.snIncident": "外部事件", + "xpack.cases.caseTable.status": "状态", + "xpack.cases.caseTable.unit": "{totalCount, plural, other {案例}}", + "xpack.cases.caseTable.upToDate": " 是最新的", + "xpack.cases.caseView.actionLabel.addDescription": "添加了描述", + "xpack.cases.caseView.actionLabel.addedField": "添加了", + "xpack.cases.caseView.actionLabel.changededField": "更改了", + "xpack.cases.caseView.actionLabel.editedField": "编辑了", + "xpack.cases.caseView.actionLabel.on": "在", + "xpack.cases.caseView.actionLabel.pushedNewIncident": "已推送为新事件", + "xpack.cases.caseView.actionLabel.removedField": "移除了", + "xpack.cases.caseView.actionLabel.removedThirdParty": "已移除外部事件管理系统", + "xpack.cases.caseView.actionLabel.selectedThirdParty": "已选择 { thirdParty } 作为事件管理系统", + "xpack.cases.caseView.actionLabel.updateIncident": "更新了事件", + "xpack.cases.caseView.actionLabel.viewIncident": "查看 {incidentNumber}", + "xpack.cases.caseView.alertCommentLabelTitle": "添加了告警,从", + "xpack.cases.caseView.alreadyPushedToExternalService": "已推送到 { externalService } 事件", + "xpack.cases.caseView.backLabel": "返回到案例", + "xpack.cases.caseView.cancel": "取消", + "xpack.cases.caseView.case": "案例", + "xpack.cases.caseView.caseClosed": "案例已关闭", + "xpack.cases.caseView.caseInProgress": "案例进行中", + "xpack.cases.caseView.caseName": "案例名称", + "xpack.cases.caseView.caseOpened": "案例已打开", + "xpack.cases.caseView.caseRefresh": "刷新案例", + "xpack.cases.caseView.closeCase": "关闭案例", + "xpack.cases.caseView.closedOn": "关闭日期", + "xpack.cases.caseView.cloudDeploymentLink": "云部署", + "xpack.cases.caseView.comment": "注释", + "xpack.cases.caseView.comment.addComment": "添加注释", + "xpack.cases.caseView.comment.addCommentHelpText": "添加新注释......", + "xpack.cases.caseView.commentFieldRequiredError": "注释必填。", + "xpack.cases.caseView.connectors": "外部事件管理系统", + "xpack.cases.caseView.copyCommentLinkAria": "复制参考链接", + "xpack.cases.caseView.create": "创建新案例", + "xpack.cases.caseView.createCase": "创建案例", + "xpack.cases.caseView.description": "描述", + "xpack.cases.caseView.description.save": "保存", + "xpack.cases.caseView.doesNotExist.button": "返回到案例", + "xpack.cases.caseView.doesNotExist.description": "找不到 ID 为 {caseId} 的案例。这很可能意味着案例已删除或 ID 不正确。", + "xpack.cases.caseView.doesNotExist.title": "此案例不存在", + "xpack.cases.caseView.edit": "编辑", + "xpack.cases.caseView.edit.comment": "编辑注释", + "xpack.cases.caseView.edit.description": "编辑描述", + "xpack.cases.caseView.edit.quote": "引述", + "xpack.cases.caseView.editActionsLinkAria": "单击可查看所有操作", + "xpack.cases.caseView.editTagsLinkAria": "单击可编辑标签", + "xpack.cases.caseView.emailBody": "案例参考:{caseUrl}", + "xpack.cases.caseView.emailSubject": "Security 案例 - {caseTitle}", + "xpack.cases.caseView.errorsPushServiceCallOutTitle": "选择外部连接器", + "xpack.cases.caseView.fieldChanged": "已更改连接器字段", + "xpack.cases.caseView.fieldRequiredError": "必填字段", + "xpack.cases.caseView.generatedAlertCommentLabelTitle": "添加自", + "xpack.cases.caseView.generatedAlertCountCommentLabelTitle": "{totalCount} 个{totalCount, plural, other {告警}}", + "xpack.cases.caseView.isolatedHost": "已隔离主机", + "xpack.cases.caseView.lockedIncidentDesc": "不需要任何更新", + "xpack.cases.caseView.lockedIncidentTitle": "{ thirdParty } 事件是最新的", + "xpack.cases.caseView.lockedIncidentTitleNone": "外部事件是最新的", + "xpack.cases.caseView.markedCaseAs": "将案例标记为", + "xpack.cases.caseView.markInProgress": "标记为进行中", + "xpack.cases.caseView.moveToCommentAria": "高亮显示引用的注释", + "xpack.cases.caseView.name": "名称", + "xpack.cases.caseView.noReportersAvailable": "没有报告者。", + "xpack.cases.caseView.noTags": "当前没有为此案例分配标签。", + "xpack.cases.caseView.openCase": "创建案例", + "xpack.cases.caseView.openedOn": "打开时间", + "xpack.cases.caseView.optional": "可选", + "xpack.cases.caseView.otherEndpoints": " 以及{endpoints, plural, other {其他}} {endpoints} 个", + "xpack.cases.caseView.particpantsLabel": "参与者", + "xpack.cases.caseView.pushNamedIncident": "推送为 { thirdParty } 事件", + "xpack.cases.caseView.pushThirdPartyIncident": "推送为外部事件", + "xpack.cases.caseView.pushToService.configureConnector": "要在外部系统中打开和更新案例,必须为此案例选择外部事件管理系统。", + "xpack.cases.caseView.pushToServiceDisableBecauseCaseClosedDescription": "关闭的案例无法发送到外部系统。如果希望在外部系统中打开或更新案例,请重新打开案例。", + "xpack.cases.caseView.pushToServiceDisableBecauseCaseClosedTitle": "重新打开案例", + "xpack.cases.caseView.pushToServiceDisableByConfigDescription": "kibana.yml 文件已配置为仅允许特定连接器。要在外部系统中打开案例,请将 .[actionTypeId](例如:.servicenow | .jira)添加到 xpack.actions.enabledActiontypes 设置。有关更多信息,请参阅{link}。", + "xpack.cases.caseView.pushToServiceDisableByConfigTitle": "在 Kibana 配置文件中启用外部服务", + "xpack.cases.caseView.pushToServiceDisableByLicenseDescription": "有{appropriateLicense}、正使用{cloud}或正在免费试用时,可在外部系统中创建案例。", + "xpack.cases.caseView.pushToServiceDisableByLicenseTitle": "升级适当的许可", + "xpack.cases.caseView.releasedHost": "已释放主机", + "xpack.cases.caseView.reopenCase": "重新打开案例", + "xpack.cases.caseView.reporterLabel": "报告者", + "xpack.cases.caseView.requiredUpdateToExternalService": "需要更新 { externalService } 事件", + "xpack.cases.caseView.sendEmalLinkAria": "单击可向 {user} 发送电子邮件", + "xpack.cases.caseView.showAlertTooltip": "显示告警详情", + "xpack.cases.caseView.statusLabel": "状态", + "xpack.cases.caseView.syncAlertsLabel": "同步告警", + "xpack.cases.caseView.tags": "标签", + "xpack.cases.caseView.to": "到", + "xpack.cases.caseView.unknown": "未知", + "xpack.cases.caseView.unknownRule.label": "未知规则", + "xpack.cases.caseView.updateNamedIncident": "更新 { thirdParty } 事件", + "xpack.cases.caseView.updateThirdPartyIncident": "更新外部事件", + "xpack.cases.common.alertAddedToCase": "已添加到案例", + "xpack.cases.common.alertLabel": "告警", + "xpack.cases.common.alertsLabel": "告警", + "xpack.cases.common.allCases.caseModal.title": "选择案例", + "xpack.cases.common.allCases.table.selectableMessageCollections": "无法选择具有子案例的案例", + "xpack.cases.common.noConnector": "未选择任何连接器", + "xpack.cases.components.connectors.cases.actionTypeTitle": "案例", + "xpack.cases.components.connectors.cases.addNewCaseOption": "添加新案例", + "xpack.cases.components.connectors.cases.callOutMsg": "案例可以包含多个子案例以允许分组生成的告警。子案例将为这些已生成告警的状态提供更精细的控制,从而防止在一个案例上附加过多的告警。", + "xpack.cases.components.connectors.cases.callOutTitle": "已生成告警将附加到子案例", + "xpack.cases.components.connectors.cases.caseRequired": "必须选择策略。", + "xpack.cases.components.connectors.cases.casesDropdownRowLabel": "允许有子案例的案例", + "xpack.cases.components.connectors.cases.createCaseLabel": "创建案例", + "xpack.cases.components.connectors.cases.optionAddToExistingCase": "添加到现有案例", + "xpack.cases.components.connectors.cases.selectMessageText": "创建或更新案例。", + "xpack.cases.components.create.syncAlertHelpText": "启用此选项将使本案例中的告警状态与案例状态同步。", + "xpack.cases.configure.readPermissionsErrorDescription": "您无权查看连接器。如果要查看与此案例关联的连接器,请联系Kibana 管理员。", + "xpack.cases.configure.successSaveToast": "已保存外部连接设置", + "xpack.cases.configureCases.addNewConnector": "添加新连接器", + "xpack.cases.configureCases.cancelButton": "取消", + "xpack.cases.configureCases.caseClosureOptionsDesc": "定义如何关闭案例。要自动关闭,需要与外部事件管理系统建立连接。", + "xpack.cases.configureCases.caseClosureOptionsLabel": "案例关闭选项", + "xpack.cases.configureCases.caseClosureOptionsManual": "手动关闭案例", + "xpack.cases.configureCases.caseClosureOptionsNewIncident": "将新事件推送到外部系统时自动关闭案例", + "xpack.cases.configureCases.caseClosureOptionsSubCases": "不支持自动关闭子案例。", + "xpack.cases.configureCases.caseClosureOptionsTitle": "案例关闭", + "xpack.cases.configureCases.commentMapping": "注释", + "xpack.cases.configureCases.fieldMappingDesc": "将数据推送到 { thirdPartyName } 时,将案例字段映射到 { thirdPartyName } 字段。字段映射需要与 { thirdPartyName } 建立连接。", + "xpack.cases.configureCases.fieldMappingDescErr": "无法检索 { thirdPartyName } 的映射。", + "xpack.cases.configureCases.fieldMappingEditAppend": "追加", + "xpack.cases.configureCases.fieldMappingFirstCol": "Kibana 案例字段", + "xpack.cases.configureCases.fieldMappingSecondCol": "{ thirdPartyName } 字段", + "xpack.cases.configureCases.fieldMappingThirdCol": "编辑和更新时", + "xpack.cases.configureCases.fieldMappingTitle": "{ thirdPartyName } 字段映射", + "xpack.cases.configureCases.headerTitle": "配置案例", + "xpack.cases.configureCases.incidentManagementSystemDesc": "将您的案例连接到外部事件管理系统。然后,您便可以将案例数据推送为第三方系统中的事件。", + "xpack.cases.configureCases.incidentManagementSystemLabel": "事件管理系统", + "xpack.cases.configureCases.incidentManagementSystemTitle": "外部事件管理系统", + "xpack.cases.configureCases.requiredMappings": "至少有一个案例字段需要映射到以下所需的 { connectorName } 字段:{ fields }", + "xpack.cases.configureCases.saveAndCloseButton": "保存并关闭", + "xpack.cases.configureCases.saveButton": "保存", + "xpack.cases.configureCases.updateConnector": "更新字段映射", + "xpack.cases.configureCases.updateSelectedConnector": "更新 { connectorName }", + "xpack.cases.configureCases.warningTitle": "警告", + "xpack.cases.configureCasesButton": "编辑外部连接", + "xpack.cases.confirmDeleteCase.confirmQuestion": "删除{quantity, plural, =1 {此案例} other {这些案例}}即会永久移除所有相关案例数据,而且您将无法再将数据推送到外部事件管理系统。是否确定要继续?", + "xpack.cases.confirmDeleteCase.deleteCase": "删除{quantity, plural, other {案例}}", + "xpack.cases.confirmDeleteCase.deleteTitle": "删除“{caseTitle}”", + "xpack.cases.confirmDeleteCase.selectedCases": "删除“{quantity, plural, =1 {{title}} other {选定的 {quantity} 个案例}}”", + "xpack.cases.connecors.get.missingCaseConnectorErrorMessage": "对象类型“{id}”未注册。", + "xpack.cases.connecors.register.duplicateCaseConnectorErrorMessage": "已注册对象类型“{id}”。", "xpack.cases.connectors.cases.externalIncidentAdded": " (由 {user} 于 {date}添加) ", "xpack.cases.connectors.cases.externalIncidentCreated": " (由 {user} 于 {date}创建) ", "xpack.cases.connectors.cases.externalIncidentDefault": " (由 {user} 于 {date}创建) ", @@ -13488,13 +13659,11 @@ "xpack.lens.breadcrumbsByValue": "编辑可视化", "xpack.lens.breadcrumbsCreate": "创建", "xpack.lens.breadcrumbsTitle": "Visualize 库", - "xpack.lens.chartSwitch.dataLossDescription": "选择此图表类型将使当前应用的配置选择部分丢失。", "xpack.lens.chartSwitch.dataLossLabel": "警告", "xpack.lens.chartSwitch.experimentalLabel": "实验性", "xpack.lens.chartSwitch.noResults": "找不到 {term} 的结果。", "xpack.lens.chartTitle.unsaved": "未保存的可视化", "xpack.lens.chartWarnings.number": "{warningsCount} 个{warningsCount, plural, other {警告}}", - "xpack.lens.configPanel.chartType": "图表类型", "xpack.lens.configPanel.color.tooltip.auto": "Lens 自动为您选取颜色,除非您指定定制颜色。", "xpack.lens.configPanel.color.tooltip.custom": "清除定制颜色以返回到“自动”模式。", "xpack.lens.configPanel.color.tooltip.disabled": "当图层包括“细分依据”,各个系列无法定制颜色。", @@ -13725,7 +13894,6 @@ "xpack.lens.indexPattern.cardinality": "唯一计数", "xpack.lens.indexPattern.cardinality.signature": "field: string", "xpack.lens.indexPattern.cardinalityOf": "{name} 的唯一计数", - "xpack.lens.indexPattern.changeIndexPatternTitle": "更改索引模式", "xpack.lens.indexPattern.chooseField": "选择字段", "xpack.lens.indexPattern.chooseFieldLabel": "要使用此函数,请选择字段。", "xpack.lens.indexPattern.chooseSubFunction": "选择子函数", @@ -13962,7 +14130,6 @@ "xpack.lens.indexPatterns.actionsPopoverLabel": "索引模式设置", "xpack.lens.indexPatterns.addFieldButton": "将字段添加到索引模式", "xpack.lens.indexPatterns.clearFiltersLabel": "清除名称和类型筛选", - "xpack.lens.indexPatterns.fieldFiltersLabel": "字段筛选", "xpack.lens.indexPatterns.fieldSearchLiveRegion": "{availableFields} 个可用{availableFields, plural, other {字段}}。{emptyFields} 个空{emptyFields, plural, other {字段}}。{metaFields} 个元{metaFields, plural,other {字段}}。", "xpack.lens.indexPatterns.filterByNameLabel": "搜索字段名称", "xpack.lens.indexPatterns.manageFieldButton": "管理索引模式字段", @@ -13993,7 +14160,6 @@ "xpack.lens.pie.groupLabel": "比例", "xpack.lens.pie.groupsizeLabel": "大小调整依据", "xpack.lens.pie.pielabel": "饼图", - "xpack.lens.pie.pieWithNegativeWarningLabel": "{chartType} 图表无法使用负值进行呈现。请尝试不同的图表类型。", "xpack.lens.pie.sliceGroupLabel": "切片依据", "xpack.lens.pie.suggestionLabel": "为 {chartName}", "xpack.lens.pie.treemapGroupLabel": "分组依据", @@ -14091,7 +14257,6 @@ "xpack.lens.visualizeGeoFieldMessage": "Lens 无法可视化 {fieldType} 字段", "xpack.lens.xyChart.addLayer": "添加图层", "xpack.lens.xyChart.addLayerButton": "添加图层", - "xpack.lens.xyChart.addLayerTooltip": "使用多个图层以组合图表类型或可视化不同的索引模式。", "xpack.lens.xyChart.axisExtent.custom": "定制", "xpack.lens.xyChart.axisExtent.dataBounds": "数据边界", "xpack.lens.xyChart.axisExtent.disabledDataBoundsMessage": "仅折线图可适应数据边界", @@ -19372,9 +19537,6 @@ "xpack.reporting.diagnostic.configSizeMismatch": "xpack.reporting.{KIBANA_MAX_SIZE_BYTES_PATH} ({kibanaMaxContentBytes}) 大于 ElasticSearch 的 {ES_MAX_SIZE_BYTES_PATH} ({elasticSearchMaxContentBytes})。请在 ElasticSearch 中将 {ES_MAX_SIZE_BYTES_PATH} 设置为匹配或减小 Kibana 中的 xpack.reporting.{KIBANA_MAX_SIZE_BYTES_PATH}。", "xpack.reporting.diagnostic.noUsableSandbox": "无法使用 Chromium 沙盒。您自行承担使用“xpack.reporting.capture.browser.chromium.disableSandbox”禁用此项的风险。请参见 {url}", "xpack.reporting.diagnostic.screenshotFailureMessage": "我们无法拍摄 Kibana 安装的屏幕截图。", - "xpack.reporting.errorButton.showReportErrorAriaLabel": "显示报告错误", - "xpack.reporting.errorButton.unableToFetchReportContentTitle": "无法提取报告内容", - "xpack.reporting.errorButton.unableToGenerateReportTitle": "无法生成报告", "xpack.reporting.exportTypes.common.failedToDecryptReportJobDataErrorMessage": "无法解密报告作业数据。请确保已设置 {encryptionKey},然后重新生成此报告。{err}", "xpack.reporting.exportTypes.common.missingJobHeadersErrorMessage": "作业标头缺失", "xpack.reporting.exportTypes.csv.executeJob.dateFormateSetting": "Kibana 高级设置“{dateFormatTimezone}”已设置为“浏览器”。日期将格式化为 UTC 以避免混淆。", @@ -19385,7 +19547,6 @@ "xpack.reporting.exportTypes.printablePdf.logoDescription": "由 Elastic 提供支持", "xpack.reporting.exportTypes.printablePdf.pagingDescription": "第 {currentPage} 页,共 {pageCount} 页", "xpack.reporting.jobsQuery.deleteError": "无法删除报告:{error}", - "xpack.reporting.jobStatuses.cancelledText": "已取消", "xpack.reporting.jobStatuses.completedText": "已完成", "xpack.reporting.jobStatuses.failedText": "失败", "xpack.reporting.jobStatuses.pendingText": "待处理", @@ -19411,7 +19572,6 @@ "xpack.reporting.listing.reports.subtitle": "获取在 Kibana 应用程序中生成的报告。", "xpack.reporting.listing.reportstitle": "报告", "xpack.reporting.listing.table.captionDescription": "在 Kibana 应用程序中生成的报告", - "xpack.reporting.listing.table.csvContainsFormulas": "您的 CSV 包含电子表格应用程序可解释为公式的字符。", "xpack.reporting.listing.table.deleteCancelButton": "取消", "xpack.reporting.listing.table.deleteConfim": "报告 {reportTitle} 已删除", "xpack.reporting.listing.table.deleteConfirmButton": "删除", @@ -19423,17 +19583,12 @@ "xpack.reporting.listing.table.downloadReport": "下载报告", "xpack.reporting.listing.table.downloadReportAriaLabel": "下载报告", "xpack.reporting.listing.table.loadingReportsDescription": "正在载入报告", - "xpack.reporting.listing.table.maxSizeReachedTooltip": "已达到最大大小,包含部分数据。", "xpack.reporting.listing.table.noCreatedReportsDescription": "未创建任何报告", "xpack.reporting.listing.table.requestFailedErrorMessage": "请求失败", "xpack.reporting.listing.tableColumns.actionsTitle": "操作", "xpack.reporting.listing.tableColumns.createdAtTitle": "创建于", "xpack.reporting.listing.tableColumns.reportTitle": "报告", "xpack.reporting.listing.tableColumns.statusTitle": "状态", - "xpack.reporting.listing.tableValue.statusDetail.maxSizeReachedText": " - 最大大小已达到", - "xpack.reporting.listing.tableValue.statusDetail.pendingStatusReachedText": "待处理 - 正在等候处理作业", - "xpack.reporting.listing.tableValue.statusDetail.statusTimestampText": "{statusTimestamp} 时为 {statusLabel}", - "xpack.reporting.listing.tableValue.statusDetail.warningsText": "发生了错误:请参阅作业信息以了解详情。", "xpack.reporting.management.reportingTitle": "Reporting", "xpack.reporting.panelContent.advancedOptions": "高级选项", "xpack.reporting.panelContent.copyUrlButtonLabel": "复制 POST URL", @@ -20792,17 +20947,7 @@ "xpack.securitySolution.detectionEngine.alerts.histogram.allOthersGroupingLabel": "所有其他", "xpack.securitySolution.detectionEngine.alerts.histogram.headerTitle": "趋势", "xpack.securitySolution.detectionEngine.alerts.histogram.showingAlertsTitle": "正在显示:{modifier}{totalAlertsFormatted} 个{totalAlerts, plural, other {告警}}", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.destinationIpsDropDown": "排名靠前的目标 IP", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.eventActionsDropDown": "排名靠前的事件操作", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.eventCategoriesDropDown": "排名靠前的事件类别", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.hostNamesDropDown": "排名靠前的主机名", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.riskScoresDropDown": "风险分数", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.rulesDropDown": "排名靠前的规则", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.ruleTypesDropDown": "排名靠前的规则类型", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.severitiesDropDown": "严重性", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.sourceIpsDropDown": "排名靠前的源 IP", "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.stackByLabel": "堆叠依据", - "xpack.securitySolution.detectionEngine.alerts.histogram.stackByOptions.usersDropDown": "排名靠前的用户", "xpack.securitySolution.detectionEngine.alerts.histogram.topNLabel": "排名靠前的{fieldName}", "xpack.securitySolution.detectionEngine.alerts.histogram.viewAlertsButtonLabel": "查看告警", "xpack.securitySolution.detectionEngine.alerts.inProgressAlertFailedToastMessage": "无法将告警标记为进行中", @@ -22906,7 +23051,6 @@ "xpack.securitySolution.timeline.autosave.warning.title": "刷新后才会启用自动保存", "xpack.securitySolution.timeline.body.actions.addNotesForRowAriaLabel": "将事件第 {ariaRowindex} 行的备注添加到时间线,其中列为 {columnValues}", "xpack.securitySolution.timeline.body.actions.attachAlertToCaseForRowAriaLabel": "将第 {ariaRowindex} 行的告警或事件附加到案例,其中列为 {columnValues}", - "xpack.securitySolution.timeline.body.actions.checkboxForRowAriaLabel": "告警或事件第 {ariaRowindex} 行的{checked, select, false {已取消选中} true {已选中}}复选框,其中列为 {columnValues}", "xpack.securitySolution.timeline.body.actions.collapseAriaLabel": "折叠", "xpack.securitySolution.timeline.body.actions.expandEventTooltip": "查看详情", "xpack.securitySolution.timeline.body.actions.investigateInResolverDisabledTooltip": "无法分析此事件,因为其包含不兼容的字段映射", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx index 762526dfd7fa7..f990e12ed76e5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx @@ -153,7 +153,7 @@ const TlsError = ({ docLinks, className }: PromptErrorProps) => (

} diff --git a/x-pack/plugins/uptime/common/runtime_types/ping/synthetics.test.ts b/x-pack/plugins/uptime/common/runtime_types/ping/synthetics.test.ts index de92cfeb29e08..84f405d6ee494 100644 --- a/x-pack/plugins/uptime/common/runtime_types/ping/synthetics.test.ts +++ b/x-pack/plugins/uptime/common/runtime_types/ping/synthetics.test.ts @@ -80,22 +80,6 @@ describe('synthetics runtime types', () => { maxSteps: 1, ref: { screenshotRef: refResult, - blocks: [ - { - id: 'hash1', - synthetics: { - blob: 'image data', - blob_mime: 'image/jpeg', - }, - }, - { - id: 'hash2', - synthetics: { - blob: 'image data', - blob_mime: 'image/jpeg', - }, - }, - ], }, }; }); diff --git a/x-pack/plugins/uptime/common/runtime_types/ping/synthetics.ts b/x-pack/plugins/uptime/common/runtime_types/ping/synthetics.ts index cd6be645c7a62..e7948f4ad532c 100644 --- a/x-pack/plugins/uptime/common/runtime_types/ping/synthetics.ts +++ b/x-pack/plugins/uptime/common/runtime_types/ping/synthetics.ts @@ -82,10 +82,9 @@ export const FullScreenshotType = t.type({ synthetics: t.intersection([ t.partial({ blob: t.string, + blob_mime: t.string, }), t.type({ - blob: t.string, - blob_mime: t.string, step: t.type({ name: t.string, }), @@ -158,6 +157,10 @@ export const ScreenshotBlockDocType = t.type({ export type ScreenshotBlockDoc = t.TypeOf; +export function isScreenshotBlockDoc(data: unknown): data is ScreenshotBlockDoc { + return isRight(ScreenshotBlockDocType.decode(data)); +} + /** * Contains the fields requried by the Synthetics UI when utilizing screenshot refs. */ @@ -166,7 +169,6 @@ export const ScreenshotRefImageDataType = t.type({ maxSteps: t.number, ref: t.type({ screenshotRef: RefResultType, - blocks: t.array(ScreenshotBlockDocType), }), }); diff --git a/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap index 1403e6c3e52b1..016714d028171 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap @@ -39,7 +39,6 @@ exports[`DonutChart component passes correct props without errors for valid prop "fill": "white", "opacity": 1, "radius": 2, - "shape": "circle", "strokeWidth": 1, "visible": false, }, @@ -146,7 +145,6 @@ exports[`DonutChart component passes correct props without errors for valid prop "fill": "white", "opacity": 1, "radius": 2, - "shape": "circle", "strokeWidth": 1, "visible": true, }, @@ -214,7 +212,6 @@ exports[`DonutChart component passes correct props without errors for valid prop "fill": "white", "opacity": 1, "radius": 2, - "shape": "circle", "strokeWidth": 1, "visible": true, }, diff --git a/x-pack/plugins/uptime/public/components/fleet_package/header_field.tsx b/x-pack/plugins/uptime/public/components/fleet_package/header_field.tsx index eaf9be50e9665..035d71d3c7132 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/header_field.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/header_field.tsx @@ -15,9 +15,15 @@ interface Props { contentMode?: Mode; defaultValue: Record; onChange: (value: Record) => void; + 'data-test-subj'?: string; } -export const HeaderField = ({ contentMode, defaultValue, onChange }: Props) => { +export const HeaderField = ({ + contentMode, + defaultValue, + onChange, + 'data-test-subj': dataTestSubj, +}: Props) => { const defaultValueKeys = Object.keys(defaultValue).filter((key) => key !== 'Content-Type'); // Content-Type is a secret header we hide from the user const formattedDefaultValues: Pair[] = [ ...defaultValueKeys.map((key) => { @@ -55,6 +61,7 @@ export const HeaderField = ({ contentMode, defaultValue, onChange }: Props) => { } defaultPairs={headers} onChange={setHeaders} + data-test-subj={dataTestSubj} /> ); }; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/http/advanced_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/http/advanced_fields.tsx index aeaa452c38db9..267ccd678ddad 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/http/advanced_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/http/advanced_fields.tsx @@ -205,7 +205,6 @@ export const HTTPAdvancedFields = memo(({ validate }) => { defaultMessage="A dictionary of additional HTTP headers to send. By default the client will set the User-Agent header to identify itself." /> } - data-test-subj="syntheticsRequestHeaders" > (({ validate }) => { }), [handleInputChange] )} + data-test-subj="syntheticsRequestHeaders" /> (({ validate }) => { defaultMessage="A list of expected response headers." /> } - data-test-subj="syntheticsResponseHeaders" > (({ validate }) => { }), [handleInputChange] )} + data-test-subj="syntheticsResponseHeaders" /> void; + 'data-test-subj'?: string; } -export const KeyValuePairsField = ({ addPairControlLabel, defaultPairs, onChange }: Props) => { +export const KeyValuePairsField = ({ + addPairControlLabel, + defaultPairs, + onChange, + 'data-test-subj': dataTestSubj, +}: Props) => { const [pairs, setPairs] = useState(defaultPairs); const handleOnChange = useCallback( @@ -89,11 +95,15 @@ export const KeyValuePairsField = ({ addPairControlLabel, defaultPairs, onChange }, [onChange, pairs]); return ( - <> +
- + {addPairControlLabel} @@ -176,6 +186,6 @@ export const KeyValuePairsField = ({ addPairControlLabel, defaultPairs, onChange ); })} - +
); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx index 8e2dc1b4c24e0..df4c73908b627 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx @@ -57,7 +57,7 @@ export const PingTimestamp = ({ label, checkGroup, initialStepNo = 1 }: Props) = const { data, status } = useFetcher(() => { if (intersection && intersection.intersectionRatio === 1 && !stepImages[stepNumber - 1]) return getJourneyScreenshot(imgPath); - }, [intersection?.intersectionRatio, stepNumber]); + }, [intersection?.intersectionRatio, stepNumber, imgPath]); const [screenshotRef, setScreenshotRef] = useState(undefined); useEffect(() => { diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_popover.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_popover.tsx index 6b78c4046da95..73c43da98bfc4 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_popover.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_popover.tsx @@ -64,7 +64,7 @@ const RecomposedScreenshotImage: React.FC< } > = ({ captionContent, imageCaption, imageData, imgRef, setImageData }) => { // initially an undefined URL value is passed to the image display, and a loading spinner is rendered. - // `useCompositeImage` will call `setUrl` when the image is composited, and the updated `url` will display. + // `useCompositeImage` will call `setImageData` when the image is composited, and the updated `imageData` will display. useCompositeImage(imgRef, setImageData, imageData); return ( diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx index c24ecd9183865..9d0555d97cbd4 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx @@ -109,7 +109,7 @@ export const StepDetailContainer: React.FC = ({ checkGroup, stepIndex }) {(!journey || journey.loading) && ( - + )} diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_expanded_row/step_screenshots.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_expanded_row/step_screenshots.tsx index 316154929320d..54f73fb39a52a 100644 --- a/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_expanded_row/step_screenshots.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_expanded_row/step_screenshots.tsx @@ -60,8 +60,8 @@ export const StepScreenshots = ({ step }: Props) => { { { // expect only one accordion to be expanded expect(Object.keys(result.current.expandedRows)).toEqual(['0']); }); + + describe('getExpandedStepCallback', () => { + it('matches step index to key', () => { + const callback = getExpandedStepCallback(2); + expect(callback(defaultSteps[0])).toBe(false); + expect(callback(defaultSteps[1])).toBe(true); + }); + }); }); diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.tsx index 4b50a94f602b7..e58e1cca8660b 100644 --- a/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.tsx @@ -18,6 +18,10 @@ interface HookProps { type ExpandRowType = Record; +export function getExpandedStepCallback(key: number) { + return (step: JourneyStep) => step.synthetics?.step?.index === key; +} + export const useExpandedRow = ({ loading, steps, allSteps }: HookProps) => { const [expandedRows, setExpandedRows] = useState({}); // eui table uses index from 0, synthetics uses 1 @@ -37,21 +41,18 @@ export const useExpandedRow = ({ loading, steps, allSteps }: HookProps) => { useEffect(() => { const expandedRowsN: ExpandRowType = {}; - for (const expandedRowKeyStr in expandedRows) { - if (expandedRows.hasOwnProperty(expandedRowKeyStr)) { - const expandedRowKey = Number(expandedRowKeyStr); - const step = steps.find((stepF) => stepF.synthetics?.step?.index !== expandedRowKey); + for (const expandedRowKey of Object.keys(expandedRows).map((key) => Number(key))) { + const step = steps.find(getExpandedStepCallback(expandedRowKey + 1)); - if (step) { - expandedRowsN[expandedRowKey] = ( - - ); - } + if (step) { + expandedRowsN[expandedRowKey] = ( + + ); } } diff --git a/x-pack/plugins/uptime/public/components/synthetics/executed_step.tsx b/x-pack/plugins/uptime/public/components/synthetics/executed_step.tsx index c3016864c72a7..add34c3f71f0d 100644 --- a/x-pack/plugins/uptime/public/components/synthetics/executed_step.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/executed_step.tsx @@ -51,7 +51,7 @@ export const ExecutedStep: FC = ({ return ( {loading ? ( - + ) : ( <> diff --git a/x-pack/plugins/uptime/public/components/synthetics/step_screenshot_display.test.tsx b/x-pack/plugins/uptime/public/components/synthetics/step_screenshot_display.test.tsx index 8d35df51c2421..5b86ed525bc31 100644 --- a/x-pack/plugins/uptime/public/components/synthetics/step_screenshot_display.test.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/step_screenshot_display.test.tsx @@ -30,7 +30,7 @@ describe('StepScreenshotDisplayProps', () => { const { getByAltText } = render( { const { getByAltText } = render( @@ -57,7 +57,7 @@ describe('StepScreenshotDisplayProps', () => { const { getByTestId } = render( { const { getByAltText } = render( = ({ checkGroup, - isScreenshotBlob: isScreenshotBlob, + isFullScreenshot: isScreenshotBlob, isScreenshotRef, stepIndex, stepName, @@ -134,7 +134,7 @@ export const StepScreenshotDisplay: FC = ({ if (isScreenshotRef) { return getJourneyScreenshot(imgSrc); } - }, [basePath, checkGroup, stepIndex, isScreenshotRef]); + }, [basePath, checkGroup, imgSrc, stepIndex, isScreenshotRef]); const refDimensions = useMemo(() => { if (isAScreenshotRef(screenshotRef)) { diff --git a/x-pack/plugins/uptime/public/hooks/update_kuery_string.ts b/x-pack/plugins/uptime/public/hooks/update_kuery_string.ts index 11b5d8a51c9a8..6ed4add217ae7 100644 --- a/x-pack/plugins/uptime/public/hooks/update_kuery_string.ts +++ b/x-pack/plugins/uptime/public/hooks/update_kuery_string.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { esKuery, IIndexPattern } from '../../../../../src/plugins/data/public'; +import { esKuery } from '../../../../../src/plugins/data/public'; +import type { IndexPattern } from '../../../../../src/plugins/data/public'; import { combineFiltersAndUserSearch, stringifyKueries } from '../../common/lib'; const getKueryString = (urlFilters: string): string => { @@ -25,7 +26,7 @@ const getKueryString = (urlFilters: string): string => { }; export const useUpdateKueryString = ( - indexPattern: IIndexPattern | null, + indexPattern: IndexPattern | null, filterQueryString = '', urlFilters: string ): [string?, Error?] => { diff --git a/x-pack/plugins/uptime/public/hooks/use_composite_image.test.tsx b/x-pack/plugins/uptime/public/hooks/use_composite_image.test.tsx new file mode 100644 index 0000000000000..79e0cde1eaab8 --- /dev/null +++ b/x-pack/plugins/uptime/public/hooks/use_composite_image.test.tsx @@ -0,0 +1,200 @@ +/* + * 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 * as redux from 'react-redux'; +import { renderHook } from '@testing-library/react-hooks'; +import { ScreenshotRefImageData } from '../../common/runtime_types'; +import { ScreenshotBlockCache } from '../state/reducers/synthetics'; +import { shouldCompose, useCompositeImage } from './use_composite_image'; +import * as compose from '../lib/helper/compose_screenshot_images'; + +const MIME = 'image/jpeg'; + +describe('use composite image', () => { + let imageData: string | undefined; + let imgRef: ScreenshotRefImageData; + let curRef: ScreenshotRefImageData; + let blocks: ScreenshotBlockCache; + + beforeEach(() => { + imgRef = { + stepName: 'step-1', + maxSteps: 3, + ref: { + screenshotRef: { + '@timestamp': '123', + monitor: { + check_group: 'check-group', + }, + screenshot_ref: { + width: 100, + height: 200, + blocks: [ + { + hash: 'hash1', + top: 0, + left: 0, + width: 10, + height: 10, + }, + { + hash: 'hash2', + top: 0, + left: 10, + width: 10, + height: 10, + }, + ], + }, + synthetics: { + package_version: 'v1', + step: { index: 0, name: 'first' }, + type: 'step/screenshot_ref', + }, + }, + }, + }; + curRef = { + stepName: 'step-1', + maxSteps: 3, + ref: { + screenshotRef: { + '@timestamp': '234', + monitor: { + check_group: 'check-group-2', + }, + screenshot_ref: { + width: 100, + height: 200, + blocks: [ + { + hash: 'hash1', + top: 0, + left: 0, + width: 10, + height: 10, + }, + { + hash: 'hash2', + top: 0, + left: 10, + width: 10, + height: 10, + }, + ], + }, + synthetics: { + package_version: 'v1', + step: { index: 1, name: 'second' }, + type: 'step/screenshot_ref', + }, + }, + }, + }; + blocks = { + hash1: { + id: 'id1', + synthetics: { + blob: 'blob', + blob_mime: MIME, + }, + }, + hash2: { + id: 'id2', + synthetics: { + blob: 'blob', + blob_mime: MIME, + }, + }, + }; + }); + + describe('shouldCompose', () => { + it('returns true if all blocks are loaded and ref is new', () => { + expect(shouldCompose(imageData, imgRef, curRef, blocks)).toBe(true); + }); + + it('returns false if a required block is pending', () => { + blocks.hash2 = { status: 'pending' }; + expect(shouldCompose(imageData, imgRef, curRef, blocks)).toBe(false); + }); + + it('returns false if a required block is missing', () => { + delete blocks.hash2; + expect(shouldCompose(imageData, imgRef, curRef, blocks)).toBe(false); + }); + + it('returns false if imageData is defined and the refs have matching step index/check_group', () => { + imageData = 'blob'; + curRef.ref.screenshotRef.synthetics.step.index = 0; + curRef.ref.screenshotRef.monitor.check_group = 'check-group'; + expect(shouldCompose(imageData, imgRef, curRef, blocks)).toBe(false); + }); + + it('returns true if imageData is defined and the refs have different step names', () => { + imageData = 'blob'; + curRef.ref.screenshotRef.synthetics.step.index = 0; + expect(shouldCompose(imageData, imgRef, curRef, blocks)).toBe(true); + }); + }); + + describe('useCompositeImage', () => { + let useDispatchMock: jest.Mock; + let canvasMock: unknown; + let removeChildSpy: jest.Mock; + let selectorSpy: jest.SpyInstance; + let composeSpy: jest.SpyInstance; + + beforeEach(() => { + useDispatchMock = jest.fn(); + removeChildSpy = jest.fn(); + canvasMock = { + parentElement: { + removeChild: removeChildSpy, + }, + toDataURL: jest.fn().mockReturnValue('compose success'), + }; + // @ts-expect-error mocking canvas element for testing + jest.spyOn(document, 'createElement').mockReturnValue(canvasMock); + jest.spyOn(redux, 'useDispatch').mockReturnValue(useDispatchMock); + selectorSpy = jest.spyOn(redux, 'useSelector').mockReturnValue({ blocks }); + composeSpy = jest + .spyOn(compose, 'composeScreenshotRef') + .mockReturnValue(new Promise((r) => r([]))); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('does not compose if all blocks are not loaded', () => { + blocks = {}; + renderHook(() => useCompositeImage(imgRef, jest.fn(), imageData)); + + expect(useDispatchMock).toHaveBeenCalledWith({ + payload: ['hash1', 'hash2'], + type: 'FETCH_BLOCKS', + }); + }); + + it('composes when all required blocks are loaded', async () => { + const onComposeImageSuccess = jest.fn(); + const { waitFor } = renderHook(() => useCompositeImage(imgRef, onComposeImageSuccess)); + + expect(selectorSpy).toHaveBeenCalled(); + expect(composeSpy).toHaveBeenCalledTimes(1); + expect(composeSpy.mock.calls[0][0]).toEqual(imgRef); + expect(composeSpy.mock.calls[0][1]).toBe(canvasMock); + expect(composeSpy.mock.calls[0][2]).toBe(blocks); + + await waitFor(() => { + expect(onComposeImageSuccess).toHaveBeenCalledTimes(1); + expect(onComposeImageSuccess).toHaveBeenCalledWith('compose success'); + }); + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/hooks/use_composite_image.ts b/x-pack/plugins/uptime/public/hooks/use_composite_image.ts index 6db3d05b8c968..3af1e798d43e1 100644 --- a/x-pack/plugins/uptime/public/hooks/use_composite_image.ts +++ b/x-pack/plugins/uptime/public/hooks/use_composite_image.ts @@ -5,19 +5,70 @@ * 2.0. */ +import { useDispatch, useSelector } from 'react-redux'; import React from 'react'; import { composeScreenshotRef } from '../lib/helper/compose_screenshot_images'; import { ScreenshotRefImageData } from '../../common/runtime_types/ping/synthetics'; +import { + fetchBlocksAction, + isPendingBlock, + ScreenshotBlockCache, + StoreScreenshotBlock, +} from '../state/reducers/synthetics'; +import { syntheticsSelector } from '../state/selectors'; + +function allBlocksLoaded(blocks: { [key: string]: StoreScreenshotBlock }, hashes: string[]) { + for (const hash of hashes) { + if (!blocks[hash] || isPendingBlock(blocks[hash])) { + return false; + } + } + return true; +} /** * Checks if two refs are the same. If the ref is unchanged, there's no need * to run the expensive draw procedure. + * + * The key fields here are `step.index` and `check_group`, as there's a 1:1 between + * journey and check group, and each step has a unique index within a journey. */ -function isNewRef(a: ScreenshotRefImageData, b: ScreenshotRefImageData): boolean { - if (typeof a === 'undefined' || typeof b === 'undefined') return false; - const stepA = a.ref.screenshotRef.synthetics.step; - const stepB = b.ref.screenshotRef.synthetics.step; - return stepA.index !== stepB.index || stepA.name !== stepB.name; +const isNewRef = ( + { + ref: { + screenshotRef: { + synthetics: { + step: { index: indexA }, + }, + monitor: { check_group: checkGroupA }, + }, + }, + }: ScreenshotRefImageData, + { + ref: { + screenshotRef: { + synthetics: { + step: { index: indexB }, + }, + monitor: { check_group: checkGroupB }, + }, + }, + }: ScreenshotRefImageData +): boolean => indexA !== indexB || checkGroupA !== checkGroupB; + +export function shouldCompose( + imageData: string | undefined, + imgRef: ScreenshotRefImageData, + curRef: ScreenshotRefImageData, + blocks: ScreenshotBlockCache +): boolean { + return ( + allBlocksLoaded( + blocks, + imgRef.ref.screenshotRef.screenshot_ref.blocks.map(({ hash }) => hash) + ) && + (typeof imageData === 'undefined' || isNewRef(imgRef, curRef)) + ); } /** @@ -31,25 +82,34 @@ export const useCompositeImage = ( onComposeImageSuccess: React.Dispatch, imageData?: string ): void => { + const dispatch = useDispatch(); + const { blocks }: { blocks: ScreenshotBlockCache } = useSelector(syntheticsSelector); + + React.useEffect(() => { + dispatch( + fetchBlocksAction(imgRef.ref.screenshotRef.screenshot_ref.blocks.map(({ hash }) => hash)) + ); + }, [dispatch, imgRef.ref.screenshotRef.screenshot_ref.blocks]); + const [curRef, setCurRef] = React.useState(imgRef); React.useEffect(() => { const canvas = document.createElement('canvas'); async function compose() { - await composeScreenshotRef(imgRef, canvas); - const imgData = canvas.toDataURL('image/png', 1.0); + await composeScreenshotRef(imgRef, canvas, blocks); + const imgData = canvas.toDataURL('image/jpg', 1.0); onComposeImageSuccess(imgData); } // if the URL is truthy it means it's already been composed, so there // is no need to call the function - if (typeof imageData === 'undefined' || isNewRef(imgRef, curRef)) { + if (shouldCompose(imageData, imgRef, curRef, blocks)) { compose(); setCurRef(imgRef); } return () => { canvas.parentElement?.removeChild(canvas); }; - }, [imgRef, onComposeImageSuccess, curRef, imageData]); + }, [blocks, curRef, imageData, imgRef, onComposeImageSuccess]); }; diff --git a/x-pack/plugins/uptime/public/lib/__mocks__/screenshot_ref.mock.ts b/x-pack/plugins/uptime/public/lib/__mocks__/screenshot_ref.mock.ts index d3a005d982168..a95aa77371b23 100644 --- a/x-pack/plugins/uptime/public/lib/__mocks__/screenshot_ref.mock.ts +++ b/x-pack/plugins/uptime/public/lib/__mocks__/screenshot_ref.mock.ts @@ -40,23 +40,5 @@ export const mockRef: ScreenshotRefImageData = { }, monitor: { check_group: 'a567cc7a-c891-11eb-bdf9-3e22fb19bf97' }, }, - blocks: [ - { - id: 'd518801fc523cf02727cd520f556c4113b3098c7', - synthetics: { - blob: - '/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCABaAKADASIAAhEBAxEB/8QAHAABAAIDAQEBAAAAAAAAAAAAAAMFBAYHAggB/8QANRAAAQMDAwEHAwMCBwEAAAAAAQACAwQFEQYSITEHE0FRUpLRFBVhIjKBcaEIFiMkMzSxQv/EABkBAQADAQEAAAAAAAAAAAAAAAADBQYEAv/EACYRAQABBAEEAgEFAAAAAAAAAAABAgMEESEFEjFBBjJxE0JRYYH/2gAMAwEAAhEDEQA/APqlERAREQEREBERAREQEREBERAREQEREBERAREQeT4J5qn1XdDZbFU1rWB74wA1p6FxIAz+OVp+ldV3O6UdxfVd0BEWCNzG4ILieOvgAVFk3qcezVeq8Uxt12cK7dtzdp8ROnRTKxmA57QT+cL2CCucOc55LnuLnHkknJKvNNV7xUtpZHF0bwdmT+0jlZPA+WUZWRFmujtiqdRO98+t/lLe6fVbomqJ3ptqIi2KvEWHc7hR2uhlrbjVQUlJCN0k9RII2MGcZLjwOSsprg5oc0gtIyCPFB6REQERQPqIWVDIHSxtnkBc2MuAc4DqQOpwgnREQEREBERAREQcf7QdZXKK+z0FvlENPTkMcCxru8djnOQeOcYUuj7hLdLZWCop6SlxKwsmYxsDJpMEbT0Bdg54Vjr3SlD9ZLfqieSOmG01EDB+qQ8AbT4Z4z/JXP7ncZK90bdoipYRsgp2fsjb+PM+ZPJVhl2MXNwpxu37RqZ9wydzqGX03LquV1zMb3FO+Jj1uPUOiTQvik2yxuY/w3DCptS6vpNHPj3MFXdjhzaUOwImnxkPgSOjeviVV6Wv9dBWU9C+eV9LK7ug3hzoi7gOZnOCCc+S0+5dnepX3CUQNhuj3ykOlhqWOcXE9XBxDh+SVkek/D8TCyv1ci53RHNMTxz/AH/Ol3f+TXc3H1i0TFXifevw+hdI32n1LYqa50zSxsoLXMccljgcFufHnx8Rha7fdeVrNUVWn9KacqL/AHGgZHLXFtVHTRUweMsaXv6vI52gdPHri27ONOP0xpWnoKh7X1Jc6WYt6B7vAf0GB/C5ZZxNQ9rGvaCr1rPpupqaqCrgiLKYtqonRABzTOxxO3G0hp4wtBXFMVzFPh2WJrm3TNz7a5Zvadq+HVnYRrV30VTbrjQYpK6hqMF8EokYcZHDmkEEOHBC3683nUlufRw2LSv3mldTse6o+4xU+1/ILNrhk8AHP5/C5h2g2C22zsj7SbrR6kfqCquggNZUGSEhskbmNAxE0NacEZGPAK8u94rLn2jzabrtVT6atFHaqeriFM6KGWse4nc4SSNd+luACGrylbNYu0enqbRqWpv1tqrNXacy65Ub3tlcxvd941zHN4eHN6dP/CcO3641hcaamraLs5qzQVO18T5btTxy907kPdGTwcHO3OfBc/7N5NLSdoPaxZ6/UUN1tldTULTU11wZI6pibTvEx70EAhhftJH7cDyXrU9c7s40/DcND9okt5ZDLDDTWCsqIa36lrpGs7qJw/1G4BJGM4DcILnUOpNUUfb2Y7TpWruQjsUjI6YXKGFs0f1LP9wNzsDB/RtOHc+S3aW70b9caTprtZDBqGst087HmVrzRYEZli3Dh3JAyODtVDe7lR2j/ERbai51MNJT1WmpqeKWeRrGOkFSx5bkkc4HRZF8qIartu0LPTSxzQy2uveySNwc17T3RBBHBB80Eg7RrrdrtdabRmkKu90lsqH0c9a+thpY3Ts/cyPfkuxnrwP7Z6BbppqihppqymdSVEkTXyU7nNeYXEAlhc0kEg8ZBwccLk3YfqGzWSxXux3i40FuuluvNY2ogqZmxOIdKXNeA4jLSCMEccLrtNPFVU8U9NKyaGVofHJG4Oa9pGQQRwQR4oMhERAREQEREFTqa2/drJV0WQHSt/ST0DgcjP4yAuDXCiqbfVPp6yF8MzDgtcP7jzH5X0aenkoZqaGYDv4mSY8HNB/9U1q9NvhUdS6VTnTFcTqqOP8AHF9A2apuF7pqlsJ+lp373SPGG7hyAD4nOFu2ndL1lJeGVddIwd2S4BhyXk8fwOVu0bGsADWhoHAwMBe8LlyrVOTcprr/AG+EuB06jDt9m9zve36qe96bsV/MRvtmttyMWRGayljm2Z8twOFcIpFkpqXTVipbZNbKWy22C2zHMlJHSRtif0/cwDB6DqPBL1pmw30Q/fLJbLj3IIj+spI5u7B6hu4HH8K5RBUf5cseAPs1uwIjB/1Wf8Z6s6ftPl0WLbNGaXtVa2ttmnLLRVjc7Z6egijkGfJzWgrYUQVN70/Zr/FEy+Wm33KOIl0baymZMGE9SA4HBU1NabdS/Smlt9JCaSPuafu4Wt7iPgbGYH6W8DgccKwRBRXjSWnL1VCqvGn7RcKkANEtXRRyvAHhuc0lWtNBFS08UFNEyGGJoZHHG0NaxoGAABwAB4LIRAREQEREBERAREQEREBERAREQEREBERAREQEREBERBHul9DPcfhN0voZ7j8KREEe6X0M9x+E3S+hnuPwpEQR7pfQz3H4TdL6Ge4/CkRBHul9DPcfhN0voZ7j8KREEe6X0M9x+E3S+hnuPwpEQR7pfQz3H4TdL6Ge4/CkRBHul9DPcfhN0voZ7j8KREEe6X0M9x+E3S+hnuPwpEQR7pfQz3H4TdL6Ge4/CkRBHuk9DPcfhVV1vcdtexssL3ucM4jOcf1zhW/ktF1OT92k5/8AgL3RT3Ty4c/IqsW+6jy//9k=', - blob_mime: 'image/jpeg', - }, - }, - { - id: 'fa90345d5d7b05b1601e9ee645e663bc358869e0', - synthetics: { - blob: - '/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCABaAKADASIAAhEBAxEB/8QAHAABAAIDAQEBAAAAAAAAAAAAAAYIAwUHBAIB/8QANBAAAQQCAQICCAUDBQAAAAAAAQACAwQFEQYSIQcxExQWQVFSVKEiMmGS0RVTcQhDgZHh/8QAGQEBAAIDAAAAAAAAAAAAAAAAAAIEAwUG/8QAHxEBAAEEAwADAAAAAAAAAAAAAAEEExVRAgORBRIh/9oADAMBAAIRAxEAPwC1KIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIg+JHBkbnnyaCVyXj3jlhsqMRYs4PP43GZax6rUyViuz1Z8vUWhhe150dtI8vcfcCV1iyCa8oAJJYQAP8KvPg54WZPJ8D4qOY5TKwY/HWn3IuPzVG1xHK2aQtMjiOtwO+rR9zu3ZB3qDL46xYNeDIVJJ+p7TGyZpdtmuoaB3sbG/htZaORpZBshoW61psbul5gla/pPwOj2KrvjvD7J2uD+Kliph5q3KLuYvMozzxGKWWqXMcWxF2vwvBkGx2dvz7LyYvi+Vyd/MTcI4vk+K1TxWTG2GW63qvrVw76Q0bHU73el/nuHcLvPMPByvAYKCT1yfMOstimrSMfFEYI+t4eQ7YOj5AH9dLfR5fGyQVp48hUdDaf6OCQTtLZX7I6WHenHYI0Pgq18A47IefeGcuN4PmcEyhQt1crbs0XRMfY9Vc3rcfftx7Pdrq6gB+XQxcaxvI4cB4acXs8Sz0M/HuStluXDVJr9BmkeHscPNmn93a6Rrz7hBZ0ZKi6+aDbtY3gOo1xK30gHx6d70tJxbnGA5PPlYsPejlfjLD69jbgO7fN7e/dnf83kuI8B48/HZmjjuQcCy9/lsOdfcm5AA6GH0ZcSJvWR+duv9o9j/lfVHhLK2L8WMDZ4rlIn3LslmnNjKbWelqGRjmRQyHTTot2Yt+QIHdBYmhfqZGEy0LVezED0l8Ege3fw2CvWuJ/6fqeTo5TkLLGDFTGlkDYsg/EnFS2ntBBa6vst/DvXU0Dvvz327YgIiINXyPM0ePYS5lstO2ChUjMksh9w/wAe8k6AHvJChHG/FrGZfOY7GW8LnsN/VQX46xk6gjit6G9NcHHRI7gH3a+IC93jfxm5y/wxzeGxfe9Mxj4mF3SJHMka/oJ/Xp0N9t6Wm4/zPOcizGCx0PAsnj44AHZG3lq3oIqpa0dq52esk9hrXbX66DoozGMMDLAyNMwPk9C2T07el0nl0A711fp5rKclRF4UTcrC6R1CuZW+kI+PTvaq6cTySpxanxB3FM66zQ5ay9JdZVLqz4DKSHscO7vPZ0NADZI8ls8txzJY3xYns4XjN+/JazrbbxkcUHRsBO3Tw32OBYwe6N3l27HyQd04/wA1wfIM5l8RjLrZL2LeGWGbA3sebe/4gPIkeRW2gy+MsVp7MGRpy16+/TSMma5sevPqIOh5e9V6yvDMnFl/F3HYXj89bJZWBk2Mvw1AyGSH8JmhZMAA1z9kFuxsg78lro+NXLo5Ba4pw3L8fxrOITY+3WnpGF124QekMjHeRw+fWz/yNhZMZzFCOZ/9UoBkLGySu9YZpjXflc477A7GifNZ7eSo1Kgt27laCq7XTNLK1rDvy04nXdV1wHhnUfybhbbnFZPU5eJj+o+kqPDHXOkdptjXpQSezu4IHwGo9W4ryUcO8Np8zispNjqFe5Xs1H4g3pK0rpn+jc+q/RILOkA67AA+8ILcIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiCOe1Nb6eb7J7U1vp5vsoj5u7r9PkrFuHMZSo3HiW+1Nb6eb7J7U1vp5vsoiiW4MpUbjxLvamt9PN9k9qa30832URRLcGUqNx4l3tTW+nm+ye1Nb6eb7KIoluDKVG48S72prfTzfZPamt9PN9lEUS3BlKjceJd7U1vp5vsntTW/sTfZRFEtwZSo3HiW+1Nf+xN/0Fuq05sQtliawscNj8X/AIucKccYJOJi3+qhz4REfi98fW9nfz+vNteqX5GfuP8ACdUvyM/cf4WRFiblj6pfkZ+4/wAJ1S/Iz9x/hZEQY+qX5GfuP8J1S/Iz9x/hZEQf/9k=', - blob_mime: 'image/jpeg', - }, - }, - ], }, }; diff --git a/x-pack/plugins/uptime/public/lib/__mocks__/uptime_store.mock.ts b/x-pack/plugins/uptime/public/lib/__mocks__/uptime_store.mock.ts index 8ad3379615549..c0b4c893d93d8 100644 --- a/x-pack/plugins/uptime/public/lib/__mocks__/uptime_store.mock.ts +++ b/x-pack/plugins/uptime/public/lib/__mocks__/uptime_store.mock.ts @@ -111,4 +111,9 @@ export const mockState: AppState = { }, journeys: {}, networkEvents: {}, + synthetics: { + blocks: {}, + cacheSize: 0, + hitCount: [], + }, }; diff --git a/x-pack/plugins/uptime/public/lib/helper/compose_screenshot_images.test.ts b/x-pack/plugins/uptime/public/lib/helper/compose_screenshot_images.test.ts new file mode 100644 index 0000000000000..0bf809d4e7a40 --- /dev/null +++ b/x-pack/plugins/uptime/public/lib/helper/compose_screenshot_images.test.ts @@ -0,0 +1,87 @@ +/* + * 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 { ScreenshotRefImageData } from '../../../common/runtime_types/ping/synthetics'; +import { composeScreenshotRef } from './compose_screenshot_images'; + +describe('composeScreenshotRef', () => { + let getContextMock: jest.Mock; + let drawImageMock: jest.Mock; + let ref: ScreenshotRefImageData; + let contextMock: unknown; + + beforeEach(() => { + drawImageMock = jest.fn(); + contextMock = { + drawImage: drawImageMock, + }; + getContextMock = jest.fn().mockReturnValue(contextMock); + ref = { + stepName: 'step', + maxSteps: 3, + ref: { + screenshotRef: { + '@timestamp': '123', + monitor: { check_group: 'check-group' }, + screenshot_ref: { + blocks: [ + { + hash: '123', + top: 0, + left: 0, + width: 10, + height: 10, + }, + ], + height: 100, + width: 100, + }, + synthetics: { + package_version: 'v1', + step: { + name: 'step-name', + index: 0, + }, + type: 'step/screenshot_ref', + }, + }, + }, + }; + }); + + it('throws error when blob does not exist', async () => { + try { + // @ts-expect-error incomplete invocation for test + await composeScreenshotRef(ref, { getContext: getContextMock }, {}); + } catch (e: any) { + expect(e).toMatchInlineSnapshot( + `[Error: Error processing image. Expected image data with hash 123 is missing]` + ); + expect(getContextMock).toHaveBeenCalled(); + expect(getContextMock.mock.calls[0][0]).toBe('2d'); + expect(getContextMock.mock.calls[0][1]).toEqual({ alpha: false }); + } + }); + + it('throws error when block is pending', async () => { + try { + await composeScreenshotRef( + ref, + // @ts-expect-error incomplete invocation for test + { getContext: getContextMock }, + { '123': { status: 'pending' } } + ); + } catch (e: any) { + expect(e).toMatchInlineSnapshot( + `[Error: Error processing image. Expected image data with hash 123 is missing]` + ); + expect(getContextMock).toHaveBeenCalled(); + expect(getContextMock.mock.calls[0][0]).toBe('2d'); + expect(getContextMock.mock.calls[0][1]).toEqual({ alpha: false }); + } + }); +}); diff --git a/x-pack/plugins/uptime/public/lib/helper/compose_screenshot_images.ts b/x-pack/plugins/uptime/public/lib/helper/compose_screenshot_images.ts index 7481a517d3c9e..60cd248c1487a 100644 --- a/x-pack/plugins/uptime/public/lib/helper/compose_screenshot_images.ts +++ b/x-pack/plugins/uptime/public/lib/helper/compose_screenshot_images.ts @@ -5,7 +5,11 @@ * 2.0. */ -import { ScreenshotRefImageData } from '../../../common/runtime_types'; +import { + isScreenshotBlockDoc, + ScreenshotRefImageData, +} from '../../../common/runtime_types/ping/synthetics'; +import { ScreenshotBlockCache } from '../../state/reducers/synthetics'; /** * Draws image fragments on a canvas. @@ -15,30 +19,30 @@ import { ScreenshotRefImageData } from '../../../common/runtime_types'; */ export async function composeScreenshotRef( data: ScreenshotRefImageData, - canvas: HTMLCanvasElement + canvas: HTMLCanvasElement, + blocks: ScreenshotBlockCache ) { const { - ref: { screenshotRef, blocks }, + ref: { screenshotRef }, } = data; + const ctx = canvas.getContext('2d', { alpha: false }); + canvas.width = screenshotRef.screenshot_ref.width; canvas.height = screenshotRef.screenshot_ref.height; - const ctx = canvas.getContext('2d', { alpha: false }); - /** * We need to treat each operation as an async task, otherwise we will race between drawing image * chunks and extracting the final data URL from the canvas; without this, the image could be blank or incomplete. */ const drawOperations: Array> = []; - for (const block of screenshotRef.screenshot_ref.blocks) { + for (const { hash, top, left, width, height } of screenshotRef.screenshot_ref.blocks) { drawOperations.push( new Promise((resolve, reject) => { const img = new Image(); - const { top, left, width, height, hash } = block; - const blob = blocks.find((b) => b.id === hash); - if (!blob) { + const blob = blocks[hash]; + if (!blob || !isScreenshotBlockDoc(blob)) { reject(Error(`Error processing image. Expected image data with hash ${hash} is missing`)); } else { img.onload = () => { diff --git a/x-pack/plugins/uptime/public/state/api/journey.ts b/x-pack/plugins/uptime/public/state/api/journey.ts index 4e71a07c70b68..8ed3fadf5c346 100644 --- a/x-pack/plugins/uptime/public/state/api/journey.ts +++ b/x-pack/plugins/uptime/public/state/api/journey.ts @@ -12,11 +12,16 @@ import { FailedStepsApiResponseType, JourneyStep, JourneyStepType, + ScreenshotBlockDoc, ScreenshotImageBlob, ScreenshotRefImageData, SyntheticsJourneyApiResponse, SyntheticsJourneyApiResponseType, -} from '../../../common/runtime_types'; +} from '../../../common/runtime_types/ping/synthetics'; + +export async function fetchScreenshotBlockSet(params: string[]): Promise { + return apiService.post('/api/uptime/journey/screenshot/block', { hashes: params }); +} export async function fetchJourneySteps( params: FetchJourneyStepsParams diff --git a/x-pack/plugins/uptime/public/state/effects/index.ts b/x-pack/plugins/uptime/public/state/effects/index.ts index a5e9ffecadaf8..df02180b1c28d 100644 --- a/x-pack/plugins/uptime/public/state/effects/index.ts +++ b/x-pack/plugins/uptime/public/state/effects/index.ts @@ -20,6 +20,11 @@ import { fetchCertificatesEffect } from '../certificates/certificates'; import { fetchAlertsEffect } from '../alerts/alerts'; import { fetchJourneyStepsEffect } from './journey'; import { fetchNetworkEventsEffect } from './network_events'; +import { + fetchScreenshotBlocks, + generateBlockStatsOnPut, + pruneBlockCache, +} from './synthetic_journey_blocks'; export function* rootEffect() { yield fork(fetchMonitorDetailsEffect); @@ -38,4 +43,7 @@ export function* rootEffect() { yield fork(fetchAlertsEffect); yield fork(fetchJourneyStepsEffect); yield fork(fetchNetworkEventsEffect); + yield fork(fetchScreenshotBlocks); + yield fork(generateBlockStatsOnPut); + yield fork(pruneBlockCache); } diff --git a/x-pack/plugins/uptime/public/state/effects/synthetic_journey_blocks.ts b/x-pack/plugins/uptime/public/state/effects/synthetic_journey_blocks.ts new file mode 100644 index 0000000000000..829048747ddf7 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/effects/synthetic_journey_blocks.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Action } from 'redux-actions'; +import { call, fork, put, select, takeEvery, throttle } from 'redux-saga/effects'; +import { ScreenshotBlockDoc } from '../../../common/runtime_types/ping/synthetics'; +import { fetchScreenshotBlockSet } from '../api/journey'; +import { + fetchBlocksAction, + setBlockLoadingAction, + isPendingBlock, + pruneCacheAction, + putBlocksAction, + putCacheSize, + ScreenshotBlockCache, + updateHitCountsAction, +} from '../reducers/synthetics'; +import { syntheticsSelector } from '../selectors'; + +function* fetchBlocks(hashes: string[]) { + yield put(setBlockLoadingAction(hashes)); + const blocks: ScreenshotBlockDoc[] = yield call(fetchScreenshotBlockSet, hashes); + yield put(putBlocksAction({ blocks })); +} + +export function* fetchScreenshotBlocks() { + /** + * We maintain a list of each hash and how many times it is requested so we can avoid + * subsequent re-requests if the block is dropped due to cache pruning. + */ + yield takeEvery(String(fetchBlocksAction), function* (action: Action) { + if (action.payload.length > 0) { + yield put(updateHitCountsAction(action.payload)); + } + }); + + /** + * We do a short delay to allow multiple item renders to queue up before dispatching + * a fetch to the backend. + */ + yield throttle(20, String(fetchBlocksAction), function* () { + const { blocks }: { blocks: ScreenshotBlockCache } = yield select(syntheticsSelector); + const toFetch = Object.keys(blocks).filter((hash) => { + const block = blocks[hash]; + return isPendingBlock(block) && block.status !== 'loading'; + }); + + if (toFetch.length > 0) { + yield fork(fetchBlocks, toFetch); + } + }); +} + +export function* generateBlockStatsOnPut() { + yield takeEvery( + String(putBlocksAction), + function* (action: Action<{ blocks: ScreenshotBlockDoc[] }>) { + const batchSize = action.payload.blocks.reduce((total, cur) => { + return cur.synthetics.blob.length + total; + }, 0); + yield put(putCacheSize(batchSize)); + } + ); +} + +// 4 MB cap for cache size +const MAX_CACHE_SIZE = 4000000; + +export function* pruneBlockCache() { + yield takeEvery(String(putCacheSize), function* (_action: Action) { + const { cacheSize }: { cacheSize: number } = yield select(syntheticsSelector); + + if (cacheSize > MAX_CACHE_SIZE) { + yield put(pruneCacheAction(cacheSize - MAX_CACHE_SIZE)); + } + }); +} diff --git a/x-pack/plugins/uptime/public/state/reducers/index.ts b/x-pack/plugins/uptime/public/state/reducers/index.ts index 05fb7c732466d..53cb6d6bffb0c 100644 --- a/x-pack/plugins/uptime/public/state/reducers/index.ts +++ b/x-pack/plugins/uptime/public/state/reducers/index.ts @@ -23,6 +23,7 @@ import { selectedFiltersReducer } from './selected_filters'; import { alertsReducer } from '../alerts/alerts'; import { journeyReducer } from './journey'; import { networkEventsReducer } from './network_events'; +import { syntheticsReducer } from './synthetics'; export const rootReducer = combineReducers({ monitor: monitorReducer, @@ -42,4 +43,5 @@ export const rootReducer = combineReducers({ alerts: alertsReducer, journeys: journeyReducer, networkEvents: networkEventsReducer, + synthetics: syntheticsReducer, }); diff --git a/x-pack/plugins/uptime/public/state/reducers/index_pattern.ts b/x-pack/plugins/uptime/public/state/reducers/index_pattern.ts index 3fb2ada060d65..d16860850bd78 100644 --- a/x-pack/plugins/uptime/public/state/reducers/index_pattern.ts +++ b/x-pack/plugins/uptime/public/state/reducers/index_pattern.ts @@ -7,10 +7,10 @@ import { handleActions, Action } from 'redux-actions'; import { getIndexPattern, getIndexPatternSuccess, getIndexPatternFail } from '../actions'; -import { IIndexPattern } from '../../../../../../src/plugins/data/common/index_patterns'; +import type { IndexPattern } from '../../../../../../src/plugins/data/common/index_patterns'; export interface IndexPatternState { - index_pattern: IIndexPattern | null; + index_pattern: IndexPattern | null; errors: any[]; loading: boolean; } diff --git a/x-pack/plugins/uptime/public/state/reducers/synthetics.test.ts b/x-pack/plugins/uptime/public/state/reducers/synthetics.test.ts new file mode 100644 index 0000000000000..06d738d01b42f --- /dev/null +++ b/x-pack/plugins/uptime/public/state/reducers/synthetics.test.ts @@ -0,0 +1,429 @@ +/* + * 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 { + fetchBlocksAction, + isPendingBlock, + pruneCacheAction, + setBlockLoadingAction, + putBlocksAction, + putCacheSize, + syntheticsReducer, + SyntheticsReducerState, + updateHitCountsAction, +} from './synthetics'; + +const MIME = 'image/jpeg'; + +describe('syntheticsReducer', () => { + jest.spyOn(Date, 'now').mockImplementation(() => 10); + + describe('isPendingBlock', () => { + it('returns true for pending block', () => { + expect(isPendingBlock({ status: 'pending' })).toBe(true); + }); + + it('returns true for loading block', () => { + expect(isPendingBlock({ status: 'loading' })).toBe(true); + }); + + it('returns false for non-pending block', () => { + expect(isPendingBlock({ synthetics: { blob: 'blobdata', blob_mime: MIME } })).toBe(false); + expect(isPendingBlock({})).toBe(false); + }); + }); + + describe('prune cache', () => { + let state: SyntheticsReducerState; + + beforeEach(() => { + const blobs = ['large', 'large2', 'large3', 'large4']; + state = { + blocks: { + '123': { + synthetics: { + blob: blobs[0], + blob_mime: MIME, + }, + id: '123', + }, + '234': { + synthetics: { + blob: blobs[1], + blob_mime: MIME, + }, + id: '234', + }, + '345': { + synthetics: { + blob: blobs[2], + blob_mime: MIME, + }, + id: '345', + }, + '456': { + synthetics: { + blob: blobs[3], + blob_mime: MIME, + }, + id: '456', + }, + }, + cacheSize: 23, + hitCount: [ + { hash: '123', hitTime: 89 }, + { hash: '234', hitTime: 23 }, + { hash: '345', hitTime: 4 }, + { hash: '456', hitTime: 1 }, + ], + }; + }); + + it('removes lowest common hits', () => { + // @ts-expect-error redux-actions doesn't handle types well + expect(syntheticsReducer(state, pruneCacheAction(10))).toMatchInlineSnapshot(` + Object { + "blocks": Object { + "123": Object { + "id": "123", + "synthetics": Object { + "blob": "large", + "blob_mime": "image/jpeg", + }, + }, + "234": Object { + "id": "234", + "synthetics": Object { + "blob": "large2", + "blob_mime": "image/jpeg", + }, + }, + }, + "cacheSize": 11, + "hitCount": Array [ + Object { + "hash": "123", + "hitTime": 89, + }, + Object { + "hash": "234", + "hitTime": 23, + }, + ], + } + `); + }); + + it('skips pending blocks', () => { + state.blocks = { ...state.blocks, '000': { status: 'pending' } }; + state.hitCount.push({ hash: '000', hitTime: 1 }); + // @ts-expect-error redux-actions doesn't handle types well + const newState = syntheticsReducer(state, pruneCacheAction(10)); + expect(newState.blocks['000']).toEqual({ status: 'pending' }); + }); + + it('ignores a hash from `hitCount` that does not exist', () => { + state.hitCount.push({ hash: 'not exist', hitTime: 1 }); + // @ts-expect-error redux-actions doesn't handle types well + expect(syntheticsReducer(state, pruneCacheAction(2))).toMatchInlineSnapshot(` + Object { + "blocks": Object { + "123": Object { + "id": "123", + "synthetics": Object { + "blob": "large", + "blob_mime": "image/jpeg", + }, + }, + "234": Object { + "id": "234", + "synthetics": Object { + "blob": "large2", + "blob_mime": "image/jpeg", + }, + }, + "345": Object { + "id": "345", + "synthetics": Object { + "blob": "large3", + "blob_mime": "image/jpeg", + }, + }, + }, + "cacheSize": 17, + "hitCount": Array [ + Object { + "hash": "123", + "hitTime": 89, + }, + Object { + "hash": "234", + "hitTime": 23, + }, + Object { + "hash": "345", + "hitTime": 4, + }, + ], + } + `); + }); + + it('will prune a block with an empty blob', () => { + state.blocks = { + ...state.blocks, + '000': { id: '000', synthetics: { blob: '', blob_mime: MIME } }, + }; + state.hitCount.push({ hash: '000', hitTime: 1 }); + // @ts-expect-error redux-actions doesn't handle types well + const newState = syntheticsReducer(state, pruneCacheAction(10)); + expect(Object.keys(newState.blocks)).not.toContain('000'); + }); + }); + + describe('fetch blocks', () => { + it('sets targeted blocks as pending', () => { + const state: SyntheticsReducerState = { blocks: {}, cacheSize: 0, hitCount: [] }; + const action = fetchBlocksAction(['123', '234']); + // @ts-expect-error redux-actions doesn't handle types well + expect(syntheticsReducer(state, action)).toMatchInlineSnapshot(` + Object { + "blocks": Object { + "123": Object { + "status": "pending", + }, + "234": Object { + "status": "pending", + }, + }, + "cacheSize": 0, + "hitCount": Array [], + } + `); + }); + + it('will not overwrite a cached block', () => { + const state: SyntheticsReducerState = { + blocks: { '123': { id: '123', synthetics: { blob: 'large', blob_mime: MIME } } }, + cacheSize: 'large'.length, + hitCount: [{ hash: '123', hitTime: 1 }], + }; + const action = fetchBlocksAction(['123']); + // @ts-expect-error redux-actions doesn't handle types well + expect(syntheticsReducer(state, action)).toMatchInlineSnapshot(` + Object { + "blocks": Object { + "123": Object { + "id": "123", + "synthetics": Object { + "blob": "large", + "blob_mime": "image/jpeg", + }, + }, + }, + "cacheSize": 5, + "hitCount": Array [ + Object { + "hash": "123", + "hitTime": 1, + }, + ], + } + `); + }); + }); + describe('update hit counts', () => { + let state: SyntheticsReducerState; + + beforeEach(() => { + const blobs = ['large', 'large2', 'large3']; + state = { + blocks: { + '123': { + synthetics: { + blob: blobs[0], + blob_mime: MIME, + }, + id: '123', + }, + '234': { + synthetics: { + blob: blobs[1], + blob_mime: MIME, + }, + id: '234', + }, + '345': { + synthetics: { + blob: blobs[2], + blob_mime: MIME, + }, + id: '345', + }, + }, + cacheSize: 17, + hitCount: [ + { hash: '123', hitTime: 1 }, + { hash: '234', hitTime: 1 }, + ], + }; + }); + + it('increments hit count for selected hashes', () => { + // @ts-expect-error redux-actions doesn't handle types well + expect(syntheticsReducer(state, updateHitCountsAction(['123', '234'])).hitCount).toEqual([ + { + hash: '123', + hitTime: 10, + }, + { hash: '234', hitTime: 10 }, + ]); + }); + + it('adds new hit count for missing item', () => { + // @ts-expect-error redux-actions doesn't handle types well + expect(syntheticsReducer(state, updateHitCountsAction(['345'])).hitCount).toEqual([ + { hash: '345', hitTime: 10 }, + { hash: '123', hitTime: 1 }, + { hash: '234', hitTime: 1 }, + ]); + }); + }); + describe('put cache size', () => { + let state: SyntheticsReducerState; + + beforeEach(() => { + state = { + blocks: {}, + cacheSize: 0, + hitCount: [], + }; + }); + + it('updates the cache size', () => { + // @ts-expect-error redux-actions doesn't handle types well + expect(syntheticsReducer(state, putCacheSize(100))).toEqual({ + blocks: {}, + cacheSize: 100, + hitCount: [], + }); + }); + }); + + describe('in-flight blocks', () => { + let state: SyntheticsReducerState; + + beforeEach(() => { + state = { + blocks: { + '123': { status: 'pending' }, + }, + cacheSize: 1, + hitCount: [{ hash: '123', hitTime: 1 }], + }; + }); + + it('sets pending blocks to loading', () => { + // @ts-expect-error redux-actions doesn't handle types well + expect(syntheticsReducer(state, setBlockLoadingAction(['123']))).toEqual({ + blocks: { '123': { status: 'loading' } }, + cacheSize: 1, + hitCount: [{ hash: '123', hitTime: 1 }], + }); + }); + }); + + describe('put blocks', () => { + let state: SyntheticsReducerState; + + beforeEach(() => { + state = { + blocks: { + '123': { + status: 'pending', + }, + }, + cacheSize: 0, + hitCount: [{ hash: '123', hitTime: 1 }], + }; + }); + + it('resolves pending blocks', () => { + const action = putBlocksAction({ + blocks: [ + { + id: '123', + synthetics: { + blob: 'reallybig', + blob_mime: MIME, + }, + }, + ], + }); + // @ts-expect-error redux-actions doesn't handle types well + const result = syntheticsReducer(state, action); + expect(result).toMatchInlineSnapshot(` + Object { + "blocks": Object { + "123": Object { + "id": "123", + "synthetics": Object { + "blob": "reallybig", + "blob_mime": "image/jpeg", + }, + }, + }, + "cacheSize": 0, + "hitCount": Array [ + Object { + "hash": "123", + "hitTime": 1, + }, + ], + } + `); + }); + + it('keeps unresolved blocks', () => { + const action = putBlocksAction({ + blocks: [ + { + id: '234', + synthetics: { + blob: 'also big', + blob_mime: MIME, + }, + }, + ], + }); + // @ts-expect-error redux-actions doesn't handle types well + expect(syntheticsReducer(state, action)).toMatchInlineSnapshot(` + Object { + "blocks": Object { + "123": Object { + "status": "pending", + }, + "234": Object { + "id": "234", + "synthetics": Object { + "blob": "also big", + "blob_mime": "image/jpeg", + }, + }, + }, + "cacheSize": 0, + "hitCount": Array [ + Object { + "hash": "123", + "hitTime": 1, + }, + ], + } + `); + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/state/reducers/synthetics.ts b/x-pack/plugins/uptime/public/state/reducers/synthetics.ts new file mode 100644 index 0000000000000..1e97c3972444b --- /dev/null +++ b/x-pack/plugins/uptime/public/state/reducers/synthetics.ts @@ -0,0 +1,180 @@ +/* + * 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 { createAction, handleActions, Action } from 'redux-actions'; +import { + isScreenshotBlockDoc, + ScreenshotBlockDoc, +} from '../../../common/runtime_types/ping/synthetics'; + +export interface PendingBlock { + status: 'pending' | 'loading'; +} + +export function isPendingBlock(data: unknown): data is PendingBlock { + return ['pending', 'loading'].some((s) => s === (data as PendingBlock)?.status); +} +export type StoreScreenshotBlock = ScreenshotBlockDoc | PendingBlock; +export interface ScreenshotBlockCache { + [hash: string]: StoreScreenshotBlock; +} + +export interface CacheHitCount { + hash: string; + hitTime: number; +} + +export interface SyntheticsReducerState { + blocks: ScreenshotBlockCache; + cacheSize: number; + hitCount: CacheHitCount[]; +} + +export interface PutBlocksPayload { + blocks: ScreenshotBlockDoc[]; +} + +// this action denotes a set of blocks is required +export const fetchBlocksAction = createAction('FETCH_BLOCKS'); +// this action denotes a request for a set of blocks is in flight +export const setBlockLoadingAction = createAction('IN_FLIGHT_BLOCKS_ACTION'); +// block data has been received, and should be added to the store +export const putBlocksAction = createAction('PUT_SCREENSHOT_BLOCKS'); +// updates the total size of the image blob data cached in the store +export const putCacheSize = createAction('PUT_CACHE_SIZE'); +// keeps track of the most-requested blocks +export const updateHitCountsAction = createAction('UPDATE_HIT_COUNTS'); +// reduce the cache size to the value in the action payload +export const pruneCacheAction = createAction('PRUNE_SCREENSHOT_BLOCK_CACHE'); + +const initialState: SyntheticsReducerState = { + blocks: {}, + cacheSize: 0, + hitCount: [], +}; + +// using `any` here because `handleActions` is not set up well to handle the multi-type +// nature of all the actions it supports. redux-actions is looking for new maintainers https://github.com/redux-utilities/redux-actions#looking-for-maintainers +// and seems that we should move to something else like Redux Toolkit. +export const syntheticsReducer = handleActions< + SyntheticsReducerState, + string[] & PutBlocksPayload & number +>( + { + /** + * When removing blocks from the cache, we receive an action with a number. + * The number equates to the desired ceiling size of the cache. We then discard + * blocks, ordered by the least-requested. We continue dropping blocks until + * the newly-pruned size will be less than the ceiling supplied by the action. + */ + [String(pruneCacheAction)]: (state, action: Action) => handlePruneAction(state, action), + + /** + * Keep track of the least- and most-requested blocks, so when it is time to + * prune we keep the most commonly-used ones. + */ + [String(updateHitCountsAction)]: (state, action: Action) => + handleUpdateHitCountsAction(state, action), + + [String(putCacheSize)]: (state, action: Action) => ({ + ...state, + cacheSize: state.cacheSize + action.payload, + }), + + [String(fetchBlocksAction)]: (state, action: Action) => ({ + // increment hit counts + ...state, + blocks: { + ...state.blocks, + ...action.payload + // there's no need to overwrite existing blocks because the key + // is either storing a pending req or a cached result + .filter((b) => !state.blocks[b]) + // convert the list of new hashes in the payload to an object that + // will combine with with the existing blocks cache + .reduce( + (acc, cur) => ({ + ...acc, + [cur]: { status: 'pending' }, + }), + {} + ), + }, + }), + + /** + * All hashes contained in the action payload have been requested, so we can + * indicate that they're loading. Subsequent requests will skip them. + */ + [String(setBlockLoadingAction)]: (state, action: Action) => ({ + ...state, + blocks: { + ...state.blocks, + ...action.payload.reduce( + (acc, cur) => ({ + ...acc, + [cur]: { status: 'loading' }, + }), + {} + ), + }, + }), + + [String(putBlocksAction)]: (state, action: Action) => ({ + ...state, + blocks: { + ...state.blocks, + ...action.payload.blocks.reduce((acc, cur) => ({ ...acc, [cur.id]: cur }), {}), + }, + }), + }, + initialState +); + +function handlePruneAction(state: SyntheticsReducerState, action: Action) { + const { blocks, hitCount } = state; + const hashesToPrune: string[] = []; + let sizeToRemove = 0; + let removeIndex = hitCount.length - 1; + while (sizeToRemove < action.payload && removeIndex >= 0) { + const { hash } = hitCount[removeIndex]; + removeIndex--; + if (!blocks[hash]) continue; + const block = blocks[hash]; + if (isScreenshotBlockDoc(block)) { + sizeToRemove += block.synthetics.blob.length; + hashesToPrune.push(hash); + } + } + for (const hash of hashesToPrune) { + delete blocks[hash]; + } + return { + cacheSize: state.cacheSize - sizeToRemove, + blocks: { ...blocks }, + hitCount: hitCount.slice(0, removeIndex + 1), + }; +} + +function handleUpdateHitCountsAction(state: SyntheticsReducerState, action: Action) { + const newHitCount = [...state.hitCount]; + const hitTime = Date.now(); + action.payload.forEach((hash) => { + const countItem = newHitCount.find((item) => item.hash === hash); + if (!countItem) { + newHitCount.push({ hash, hitTime }); + } else { + countItem.hitTime = hitTime; + } + }); + // sorts in descending order + newHitCount.sort((a, b) => b.hitTime - a.hitTime); + return { + ...state, + hitCount: newHitCount, + }; +} diff --git a/x-pack/plugins/uptime/public/state/selectors/index.test.ts b/x-pack/plugins/uptime/public/state/selectors/index.test.ts index e4094c72a6e10..520ebdac0c1e0 100644 --- a/x-pack/plugins/uptime/public/state/selectors/index.test.ts +++ b/x-pack/plugins/uptime/public/state/selectors/index.test.ts @@ -109,6 +109,11 @@ describe('state selectors', () => { }, journeys: {}, networkEvents: {}, + synthetics: { + blocks: {}, + cacheSize: 0, + hitCount: [], + }, }; it('selects base path from state', () => { diff --git a/x-pack/plugins/uptime/public/state/selectors/index.ts b/x-pack/plugins/uptime/public/state/selectors/index.ts index 6c4ea8201398c..222687c78a868 100644 --- a/x-pack/plugins/uptime/public/state/selectors/index.ts +++ b/x-pack/plugins/uptime/public/state/selectors/index.ts @@ -97,3 +97,5 @@ export const monitorIdSelector = ({ ui: { monitorId } }: AppState) => monitorId; export const journeySelector = ({ journeys }: AppState) => journeys; export const networkEventsSelector = ({ networkEvents }: AppState) => networkEvents; + +export const syntheticsSelector = ({ synthetics }: AppState) => synthetics; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.test.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.test.ts index 3d8bc04a10565..15cee91606e66 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.test.ts @@ -12,6 +12,7 @@ describe('getJourneyScreenshot', () => { it('returns screenshot data', async () => { const screenshotResult = { _id: 'id', + _index: 'index', _source: { synthetics: { blob_mime: 'image/jpeg', @@ -26,8 +27,14 @@ describe('getJourneyScreenshot', () => { expect( await getJourneyScreenshot({ uptimeEsClient: mockSearchResult([], { - // @ts-expect-error incomplete search result - step: { image: { hits: { hits: [screenshotResult] } } }, + step: { + image: { + hits: { + total: 1, + hits: [screenshotResult], + }, + }, + }, }), checkGroup: 'checkGroup', stepIndex: 0, @@ -48,6 +55,7 @@ describe('getJourneyScreenshot', () => { it('returns ref data', async () => { const screenshotRefResult = { _id: 'id', + _index: 'index', _source: { '@timestamp': '123', monitor: { @@ -86,8 +94,7 @@ describe('getJourneyScreenshot', () => { expect( await getJourneyScreenshot({ uptimeEsClient: mockSearchResult([], { - // @ts-expect-error incomplete search result - step: { image: { hits: { hits: [screenshotRefResult] } } }, + step: { image: { hits: { hits: [screenshotRefResult], total: 1 } } }, }), checkGroup: 'checkGroup', stepIndex: 0, diff --git a/x-pack/plugins/uptime/server/rest_api/index.ts b/x-pack/plugins/uptime/server/rest_api/index.ts index d4d0e13bd23db..8ae878669ba32 100644 --- a/x-pack/plugins/uptime/server/rest_api/index.ts +++ b/x-pack/plugins/uptime/server/rest_api/index.ts @@ -12,7 +12,7 @@ import { createGetPingsRoute, createJourneyRoute, createJourneyScreenshotRoute, - createJourneyScreenshotBlockRoute, + createJourneyScreenshotBlocksRoute, } from './pings'; import { createGetDynamicSettingsRoute, createPostDynamicSettingsRoute } from './dynamic_settings'; import { createLogPageViewRoute } from './telemetry'; @@ -52,8 +52,8 @@ export const restApiRoutes: UMRestApiRouteFactory[] = [ createGetMonitorDurationRoute, createJourneyRoute, createJourneyScreenshotRoute, - createJourneyScreenshotBlockRoute, createNetworkEventsRoute, createJourneyFailedStepsRoute, createLastSuccessfulStepRoute, + createJourneyScreenshotBlocksRoute, ]; diff --git a/x-pack/plugins/uptime/server/rest_api/pings/index.ts b/x-pack/plugins/uptime/server/rest_api/pings/index.ts index 45cd23dea42ed..0e1cc7baa9ad1 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/index.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/index.ts @@ -9,4 +9,4 @@ export { createGetPingsRoute } from './get_pings'; export { createGetPingHistogramRoute } from './get_ping_histogram'; export { createJourneyRoute } from './journeys'; export { createJourneyScreenshotRoute } from './journey_screenshots'; -export { createJourneyScreenshotBlockRoute } from './journey_screenshot_blocks'; +export { createJourneyScreenshotBlocksRoute } from './journey_screenshot_blocks'; diff --git a/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshot_blocks.test.ts b/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshot_blocks.test.ts new file mode 100644 index 0000000000000..4909e2eb80108 --- /dev/null +++ b/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshot_blocks.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createJourneyScreenshotBlocksRoute } from './journey_screenshot_blocks'; + +describe('journey screenshot blocks route', () => { + let libs: unknown; + beforeEach(() => { + libs = { + uptimeEsClient: jest.fn(), + request: { + body: { + hashes: ['hash1', 'hash2'], + }, + }, + response: { + badRequest: jest.fn().mockReturnValue({ status: 400, message: 'Bad request.' }), + ok: jest.fn((responseData) => ({ ...responseData, status: 200, message: 'Ok' })), + notFound: jest.fn().mockReturnValue({ status: 404, message: 'Not found.' }), + }, + }; + }); + + it('returns status code 400 if hash list is invalid', async () => { + // @ts-expect-error incomplete implementation for testing + const route = createJourneyScreenshotBlocksRoute(); + + libs = Object.assign({}, libs, { request: { body: { hashes: undefined } } }); + + // @ts-expect-error incomplete implementation for testing + const response = await route.handler(libs); + expect(response.status).toBe(400); + }); + + it('returns status code 404 if result is empty set', async () => { + const route = createJourneyScreenshotBlocksRoute({ + // @ts-expect-error incomplete implementation for testing + requests: { + getJourneyScreenshotBlocks: jest.fn().mockReturnValue([]), + }, + }); + + // @ts-expect-error incomplete implementation for testing + expect((await route.handler(libs)).status).toBe(404); + }); + + it('returns blocks for request', async () => { + const responseData = [ + { + id: 'hash1', + synthetics: { + blob: 'blob1', + blob_mime: 'image/jpeg', + }, + }, + { + id: 'hash2', + synthetics: { + blob: 'blob2', + blob_mime: 'image/jpeg', + }, + }, + ]; + const route = createJourneyScreenshotBlocksRoute({ + // @ts-expect-error incomplete implementation for testing + requests: { + getJourneyScreenshotBlocks: jest.fn().mockReturnValue(responseData), + }, + }); + + // @ts-expect-error incomplete implementation for testing + const response = await route.handler(libs); + expect(response.status).toBe(200); + // @ts-expect-error incomplete implementation for testing + expect(response.body).toEqual(responseData); + }); +}); diff --git a/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshot_blocks.ts b/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshot_blocks.ts index 63c2cfe7e2d48..3127c34590ef5 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshot_blocks.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshot_blocks.ts @@ -10,44 +10,38 @@ import { isRight } from 'fp-ts/lib/Either'; import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { ScreenshotBlockDoc } from '../../../common/runtime_types/ping/synthetics'; -export const createJourneyScreenshotBlockRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ - method: 'GET', +function isStringArray(data: unknown): data is string[] { + return isRight(t.array(t.string).decode(data)); +} + +export const createJourneyScreenshotBlocksRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ + method: 'POST', path: '/api/uptime/journey/screenshot/block', validate: { + body: schema.object({ + hashes: schema.arrayOf(schema.string()), + }), query: schema.object({ - hash: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]), _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ request, response, uptimeEsClient }) => { - const { hash } = request.query; + const { hashes: blockIds } = request.body; + + if (!isStringArray(blockIds)) return response.badRequest(); + + const result = await libs.requests.getJourneyScreenshotBlocks({ + blockIds, + uptimeEsClient, + }); - const decoded = t.union([t.string, t.array(t.string)]).decode(hash); - if (!isRight(decoded)) { - return response.badRequest(); - } - const { right: data } = decoded; - let result: ScreenshotBlockDoc[]; - try { - result = await libs.requests.getJourneyScreenshotBlocks({ - blockIds: Array.isArray(data) ? data : [data], - uptimeEsClient, - }); - } catch (e: unknown) { - return response.custom({ statusCode: 500, body: { message: e } }); - } if (result.length === 0) { return response.notFound(); } + return response.ok({ body: result, - headers: { - // we can cache these blocks with extreme prejudice as they are inherently unchanging - // when queried by ID, since the ID is the hash of the data - 'Cache-Control': 'max-age=604800', - }, }); }, }); diff --git a/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.test.ts b/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.test.ts new file mode 100644 index 0000000000000..22aef54fa10bd --- /dev/null +++ b/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.test.ts @@ -0,0 +1,179 @@ +/* + * 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 { createJourneyScreenshotRoute } from './journey_screenshots'; + +describe('journey screenshot route', () => { + let libs: unknown; + beforeEach(() => { + libs = { + uptimeEsClient: jest.fn(), + request: { + params: { + checkGroup: 'check_group', + stepIndex: 0, + }, + }, + response: { + ok: jest.fn((responseData) => ({ ...responseData, status: 200, message: 'Ok' })), + notFound: jest.fn().mockReturnValue({ status: 404, message: 'Not found.' }), + }, + }; + }); + + it('will 404 for missing screenshot', async () => { + const route = createJourneyScreenshotRoute({ + // @ts-expect-error incomplete implementation for testing + requests: { + getJourneyScreenshot: jest.fn(), + }, + }); + + // @ts-expect-error incomplete implementation for testing + expect(await route.handler(libs)).toMatchInlineSnapshot(` + Object { + "message": "Not found.", + "status": 404, + } + `); + }); + + it('returns screenshot ref', async () => { + const mock = { + '@timestamp': '123', + monitor: { + check_group: 'check_group', + }, + screenshot_ref: { + width: 100, + height: 200, + blocks: [{ hash: 'hash', top: 0, left: 0, height: 10, width: 10 }], + }, + synthetics: { + package_version: '1.0.0', + step: { + name: 'a step name', + index: 0, + }, + type: 'step/screenshot_ref', + }, + totalSteps: 3, + }; + + const route = createJourneyScreenshotRoute({ + // @ts-expect-error incomplete implementation for testing + requests: { + getJourneyScreenshot: jest.fn().mockReturnValue(mock), + }, + }); + + // @ts-expect-error incomplete implementation for testing + const response = await route.handler(libs); + expect(response.status).toBe(200); + // @ts-expect-error response doesn't match interface for testing + expect(response.headers).toMatchInlineSnapshot(` + Object { + "cache-control": "max-age=600", + "caption-name": "a step name", + "max-steps": "3", + } + `); + // @ts-expect-error response doesn't match interface for testing + expect(response.body.screenshotRef).toEqual(mock); + }); + + it('returns full screenshot blob', async () => { + const mock = { + synthetics: { + blob: 'a blob', + blob_mime: 'image/jpeg', + step: { + name: 'a step name', + }, + type: 'step/screenshot', + }, + totalSteps: 3, + }; + const route = createJourneyScreenshotRoute({ + // @ts-expect-error incomplete implementation for testing + requests: { + getJourneyScreenshot: jest.fn().mockReturnValue(mock), + }, + }); + + // @ts-expect-error incomplete implementation for testing + expect(await route.handler(libs)).toMatchInlineSnapshot(` + Object { + "body": Object { + "data": Array [ + 105, + 185, + 104, + ], + "type": "Buffer", + }, + "headers": Object { + "cache-control": "max-age=600", + "caption-name": "a step name", + "content-type": "image/jpeg", + "max-steps": "3", + }, + "message": "Ok", + "status": 200, + } + `); + }); + + it('defaults to png when mime is undefined', async () => { + const mock = { + synthetics: { + blob: 'a blob', + step: { + name: 'a step name', + }, + type: 'step/screenshot', + }, + }; + const route = createJourneyScreenshotRoute({ + // @ts-expect-error incomplete implementation for testing + requests: { + getJourneyScreenshot: jest.fn().mockReturnValue(mock), + }, + }); + + // @ts-expect-error incomplete implementation for testing + const response = await route.handler(libs); + + expect(response.status).toBe(200); + // @ts-expect-error incomplete implementation for testing + expect(response.headers['content-type']).toBe('image/png'); + }); + + it('returns 404 for screenshot missing blob', async () => { + const route = createJourneyScreenshotRoute({ + // @ts-expect-error incomplete implementation for testing + requests: { + getJourneyScreenshot: jest.fn().mockReturnValue({ + synthetics: { + step: { + name: 'a step name', + }, + type: 'step/screenshot', + }, + }), + }, + }); + + // @ts-expect-error incomplete implementation for testing + expect(await route.handler(libs)).toMatchInlineSnapshot(` + Object { + "message": "Not found.", + "status": 404, + } + `); + }); +}); diff --git a/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts b/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts index bd7cf6af4f843..5f0825279ecfa 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts @@ -6,11 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { - isRefResult, - isFullScreenshot, - ScreenshotBlockDoc, -} from '../../../common/runtime_types/ping/synthetics'; +import { isRefResult, isFullScreenshot } from '../../../common/runtime_types/ping/synthetics'; import { UMServerLibs } from '../../lib/lib'; import { ScreenshotReturnTypesUnion } from '../../lib/requests/get_journey_screenshot'; import { UMRestApiRouteFactory } from '../types'; @@ -39,22 +35,13 @@ export const createJourneyScreenshotRoute: UMRestApiRouteFactory = (libs: UMServ handler: async ({ uptimeEsClient, request, response }) => { const { checkGroup, stepIndex } = request.params; - let result: ScreenshotReturnTypesUnion | null = null; - try { - result = await libs.requests.getJourneyScreenshot({ - uptimeEsClient, - checkGroup, - stepIndex, - }); - } catch (e) { - return response.customError({ body: { message: e }, statusCode: 500 }); - } - - if (isFullScreenshot(result)) { - if (!result.synthetics.blob) { - return response.notFound(); - } + const result: ScreenshotReturnTypesUnion | null = await libs.requests.getJourneyScreenshot({ + uptimeEsClient, + checkGroup, + stepIndex, + }); + if (isFullScreenshot(result) && typeof result.synthetics?.blob !== 'undefined') { return response.ok({ body: Buffer.from(result.synthetics.blob, 'base64'), headers: { @@ -63,22 +50,11 @@ export const createJourneyScreenshotRoute: UMRestApiRouteFactory = (libs: UMServ }, }); } else if (isRefResult(result)) { - const blockIds = result.screenshot_ref.blocks.map(({ hash }) => hash); - let blocks: ScreenshotBlockDoc[]; - try { - blocks = await libs.requests.getJourneyScreenshotBlocks({ - uptimeEsClient, - blockIds, - }); - } catch (e: unknown) { - return response.custom({ statusCode: 500, body: { message: e } }); - } return response.ok({ body: { screenshotRef: result, - blocks, }, - headers: getSharedHeaders(result.synthetics.step.name, result.totalSteps ?? 0), + headers: getSharedHeaders(result.synthetics.step.name, result.totalSteps), }); } diff --git a/x-pack/test/accessibility/apps/index_lifecycle_management.ts b/x-pack/test/accessibility/apps/index_lifecycle_management.ts index 0daeda5ac61f3..dcd690607c64a 100644 --- a/x-pack/test/accessibility/apps/index_lifecycle_management.ts +++ b/x-pack/test/accessibility/apps/index_lifecycle_management.ts @@ -34,6 +34,8 @@ const POLICY_ALL_PHASES = { }, }; +const indexTemplateName = 'ilm-a11y-test-template'; + export default function ({ getService, getPageObjects }: FtrProviderContext) { const { common, indexLifecycleManagement } = getPageObjects([ 'common', @@ -65,11 +67,25 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Index Lifecycle Management', async () => { before(async () => { await esClient.ilm.putLifecycle({ policy: POLICY_NAME, body: POLICY_ALL_PHASES }); + await esClient.indices.putIndexTemplate({ + name: indexTemplateName, + body: { + template: { + settings: { + lifecycle: { + name: POLICY_NAME, + }, + }, + }, + index_patterns: ['test*'], + }, + }); }); after(async () => { // @ts-expect-error @elastic/elasticsearch DeleteSnapshotLifecycleRequest.policy_id is required await esClient.ilm.deleteLifecycle({ policy: POLICY_NAME }); + await esClient.indices.deleteIndexTemplate({ name: indexTemplateName }); }); beforeEach(async () => { @@ -165,5 +181,19 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await a11y.testAppSnapshot(); }); + + it('Index templates flyout', async () => { + const policyRow = await testSubjects.find(`policyTableRow-${POLICY_NAME}`); + const actionsButton = await policyRow.findByTestSubject('viewIndexTemplates'); + + await actionsButton.click(); + + const flyoutTitleSelector = 'indexTemplatesFlyoutHeader'; + await retry.waitFor('Index templates flyout', async () => { + return testSubjects.isDisplayed(flyoutTitleSelector); + }); + + await a11y.testAppSnapshot(); + }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/connector_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/connector_types.ts index a76654415c16d..ec23719880926 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/connector_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/connector_types.ts @@ -14,7 +14,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function listActionTypesTests({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); - describe('list_action_types', () => { + describe('connector_types', () => { for (const scenario of UserAtSpaceScenarios) { const { user, space } = scenario; describe(scenario.id, () => { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ephemeral.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ephemeral.ts index 507ec7a420bfb..99801cf838836 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ephemeral.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ephemeral.ts @@ -24,7 +24,7 @@ import { DEFAULT_MAX_EPHEMERAL_ACTIONS_PER_ALERT } from '../../../../../plugins/ export default function createNotifyWhenTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const retry = getService('retry'); - const es = getService('legacyEs'); + const es = getService('es'); const esTestIndexTool = new ESTestIndexTool(es, retry); @@ -117,7 +117,7 @@ export default function createNotifyWhenTests({ getService }: FtrProviderContext ); const searchResult = await esTestIndexTool.search('action:test.index-record'); - expect(searchResult.hits.total.value).equal( + expect(searchResult.body.hits.total.value).equal( nonEphemeralTasks + DEFAULT_MAX_EPHEMERAL_ACTIONS_PER_ALERT ); }); diff --git a/x-pack/test/api_integration/apis/management/index_management/component_templates.ts b/x-pack/test/api_integration/apis/management/index_management/component_templates.ts index b631869145e1e..c6a3b77edd1c0 100644 --- a/x-pack/test/api_integration/apis/management/index_management/component_templates.ts +++ b/x-pack/test/api_integration/apis/management/index_management/component_templates.ts @@ -281,8 +281,19 @@ export default function ({ getService }: FtrProviderContext) { expect(body).to.eql({ statusCode: 404, error: 'Not Found', - message: - '[resource_not_found_exception] component template matching [component_does_not_exist] not found', + message: 'component template matching [component_does_not_exist] not found', + attributes: { + error: { + reason: 'component template matching [component_does_not_exist] not found', + root_cause: [ + { + reason: 'component template matching [component_does_not_exist] not found', + type: 'resource_not_found_exception', + }, + ], + type: 'resource_not_found_exception', + }, + }, }); }); }); @@ -356,10 +367,19 @@ export default function ({ getService }: FtrProviderContext) { const uri = `${API_BASE_PATH}/component_templates/${componentTemplateName},${COMPONENT_DOES_NOT_EXIST}`; const { body } = await supertest.delete(uri).set('kbn-xsrf', 'xxx').expect(200); - expect(body.itemsDeleted).to.eql([componentTemplateName]); expect(body.errors[0].name).to.eql(COMPONENT_DOES_NOT_EXIST); - expect(body.errors[0].error.msg).to.contain('resource_not_found_exception'); + + expect(body.errors[0].error.payload.attributes.error).to.eql({ + root_cause: [ + { + type: 'resource_not_found_exception', + reason: 'component_does_not_exist', + }, + ], + type: 'resource_not_found_exception', + reason: 'component_does_not_exist', + }); }); }); diff --git a/x-pack/test/api_integration/apis/management/index_management/data_streams.ts b/x-pack/test/api_integration/apis/management/index_management/data_streams.ts index 74498eb8c91b9..8970e8cd642fd 100644 --- a/x-pack/test/api_integration/apis/management/index_management/data_streams.ts +++ b/x-pack/test/api_integration/apis/management/index_management/data_streams.ts @@ -14,11 +14,11 @@ import { DataStream } from '../../../../../plugins/index_management/common'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const es = getService('legacyEs'); + const es = getService('es'); const createDataStream = async (name: string) => { // A data stream requires an index template before it can be created. - await es.dataManagement.saveComposableIndexTemplate({ + await es.indices.putIndexTemplate({ name, body: { // We need to match the names of backing indices with this template. @@ -36,15 +36,15 @@ export default function ({ getService }: FtrProviderContext) { }, }); - await es.dataManagement.createDataStream({ name }); + await es.indices.createDataStream({ name }); }; const deleteComposableIndexTemplate = async (name: string) => { - await es.dataManagement.deleteComposableIndexTemplate({ name }); + await es.indices.deleteIndexTemplate({ name }); }; const deleteDataStream = async (name: string) => { - await es.dataManagement.deleteDataStream({ name }); + await es.indices.deleteDataStream({ name }); await deleteComposableIndexTemplate(name); }; diff --git a/x-pack/test/api_integration/apis/management/index_management/indices.js b/x-pack/test/api_integration/apis/management/index_management/indices.js index 25b1ef97d3087..589887329fcd1 100644 --- a/x-pack/test/api_integration/apis/management/index_management/indices.js +++ b/x-pack/test/api_integration/apis/management/index_management/indices.js @@ -56,13 +56,17 @@ export default function ({ getService }) { const index = await createIndex(); // Make sure the index is open - const [cat1] = await catIndex(index); + const { + body: [cat1], + } = await catIndex(index); expect(cat1.status).to.be('open'); await closeIndex(index).expect(200); // Make sure the index has been closed - const [cat2] = await catIndex(index); + const { + body: [cat2], + } = await catIndex(index); expect(cat2.status).to.be('close'); }); }); @@ -78,13 +82,17 @@ export default function ({ getService }) { await closeIndex(index); // Make sure the index is closed - const [cat1] = await catIndex(index); + const { + body: [cat1], + } = await catIndex(index); expect(cat1.status).to.be('close'); await openIndex(index).expect(200); // Make sure the index is opened - const [cat2] = await catIndex(index); + const { + body: [cat2], + } = await catIndex(index); expect(cat2.status).to.be('open'); }); }); @@ -93,12 +101,12 @@ export default function ({ getService }) { it('should delete an index', async () => { const index = await createIndex(); - const indices1 = await catIndex(undefined, 'i'); + const { body: indices1 } = await catIndex(undefined, 'i'); expect(indices1.map((index) => index.i)).to.contain(index); await deleteIndex([index]).expect(200); - const indices2 = await catIndex(undefined, 'i'); + const { body: indices2 } = await catIndex(undefined, 'i'); expect(indices2.map((index) => index.i)).not.to.contain(index); }); @@ -112,12 +120,16 @@ export default function ({ getService }) { it('should flush an index', async () => { const index = await createIndex(); - const { indices: indices1 } = await indexStats(index, 'flush'); + const { + body: { indices: indices1 }, + } = await indexStats(index, 'flush'); expect(indices1[index].total.flush.total).to.be(0); await flushIndex(index).expect(200); - const { indices: indices2 } = await indexStats(index, 'flush'); + const { + body: { indices: indices2 }, + } = await indexStats(index, 'flush'); expect(indices2[index].total.flush.total).to.be(1); }); }); @@ -126,12 +138,16 @@ export default function ({ getService }) { it('should refresh an index', async () => { const index = await createIndex(); - const { indices: indices1 } = await indexStats(index, 'refresh'); + const { + body: { indices: indices1 }, + } = await indexStats(index, 'refresh'); const previousRefreshes = indices1[index].total.refresh.total; await refreshIndex(index).expect(200); - const { indices: indices2 } = await indexStats(index, 'refresh'); + const { + body: { indices: indices2 }, + } = await indexStats(index, 'refresh'); expect(indices2[index].total.refresh.total).to.be(previousRefreshes + 1); }); }); @@ -153,12 +169,16 @@ export default function ({ getService }) { const index = await createIndex(); // "sth" correspond to search throttling. Frozen indices are normal indices // with search throttling turned on. - const [cat1] = await catIndex(index, 'sth'); + const { + body: [cat1], + } = await catIndex(index, 'sth'); expect(cat1.sth).to.be('false'); await freeze(index).expect(200); - const [cat2] = await catIndex(index, 'sth'); + const { + body: [cat2], + } = await catIndex(index, 'sth'); expect(cat2.sth).to.be('true'); }); }); @@ -168,11 +188,15 @@ export default function ({ getService }) { const index = await createIndex(); await freeze(index).expect(200); - const [cat1] = await catIndex(index, 'sth'); + const { + body: [cat1], + } = await catIndex(index, 'sth'); expect(cat1.sth).to.be('true'); await unfreeze(index).expect(200); - const [cat2] = await catIndex(index, 'sth'); + const { + body: [cat2], + } = await catIndex(index, 'sth'); expect(cat2.sth).to.be('false'); }); }); diff --git a/x-pack/test/api_integration/apis/management/index_management/lib/elasticsearch.js b/x-pack/test/api_integration/apis/management/index_management/lib/elasticsearch.js index 5a26356328f1f..22824227f1275 100644 --- a/x-pack/test/api_integration/apis/management/index_management/lib/elasticsearch.js +++ b/x-pack/test/api_integration/apis/management/index_management/lib/elasticsearch.js @@ -13,7 +13,7 @@ import { getRandomString } from './random'; * @param {ElasticsearchClient} es The Elasticsearch client instance */ export const initElasticsearchHelpers = (getService) => { - const es = getService('legacyEs'); + const es = getService('es'); const esDeleteAllIndices = getService('esDeleteAllIndices'); let indicesCreated = []; @@ -42,11 +42,11 @@ export const initElasticsearchHelpers = (getService) => { componentTemplatesCreated.push(componentTemplate.name); } - return es.dataManagement.saveComponentTemplate(componentTemplate); + return es.cluster.putComponentTemplate(componentTemplate); }; const deleteComponentTemplate = (componentTemplateName) => { - return es.dataManagement.deleteComponentTemplate({ name: componentTemplateName }); + return es.cluster.deleteComponentTemplate({ name: componentTemplateName }); }; const cleanUpComponentTemplates = () => diff --git a/x-pack/test/api_integration/apis/management/index_management/templates.js b/x-pack/test/api_integration/apis/management/index_management/templates.js index fd21427343577..1cb58c0957e17 100644 --- a/x-pack/test/api_integration/apis/management/index_management/templates.js +++ b/x-pack/test/api_integration/apis/management/index_management/templates.js @@ -193,8 +193,8 @@ export default function ({ getService }) { }); it('should parse the ES error and return the cause', async () => { - const templateName = `template-${getRandomString()}`; - const payload = getTemplatePayload(templateName, [getRandomString()]); + const templateName = `template-create-parse-es-error}`; + const payload = getTemplatePayload(templateName, ['create-parse-es-error']); const runtime = { myRuntimeField: { type: 'boolean', @@ -207,9 +207,9 @@ export default function ({ getService }) { const { body } = await createTemplate(payload).expect(400); expect(body.attributes).an('object'); - expect(body.attributes.message).contain('template after composition is invalid'); + expect(body.attributes.error.reason).contain('template after composition is invalid'); // one of the item of the cause array should point to our script - expect(body.attributes.cause.join(',')).contain('"hello with error'); + expect(body.attributes.causes.join(',')).contain('"hello with error'); }); }); @@ -220,7 +220,7 @@ export default function ({ getService }) { await createTemplate(indexTemplate).expect(200); - let catTemplateResponse = await catTemplate(templateName); + let { body: catTemplateResponse } = await catTemplate(templateName); const { name, version } = indexTemplate; @@ -234,7 +234,7 @@ export default function ({ getService }) { 200 ); - catTemplateResponse = await catTemplate(templateName); + ({ body: catTemplateResponse } = await catTemplate(templateName)); expect( catTemplateResponse.find(({ name: templateName }) => templateName === name).version @@ -247,7 +247,7 @@ export default function ({ getService }) { await createTemplate(legacyIndexTemplate).expect(200); - let catTemplateResponse = await catTemplate(templateName); + let { body: catTemplateResponse } = await catTemplate(templateName); const { name, version } = legacyIndexTemplate; @@ -262,7 +262,7 @@ export default function ({ getService }) { templateName ).expect(200); - catTemplateResponse = await catTemplate(templateName); + ({ body: catTemplateResponse } = await catTemplate(templateName)); expect( catTemplateResponse.find(({ name: templateName }) => templateName === name).version @@ -270,8 +270,8 @@ export default function ({ getService }) { }); it('should parse the ES error and return the cause', async () => { - const templateName = `template-${getRandomString()}`; - const payload = getTemplatePayload(templateName, [getRandomString()]); + const templateName = `template-update-parse-es-error}`; + const payload = getTemplatePayload(templateName, ['update-parse-es-error']); const runtime = { myRuntimeField: { type: 'keyword', @@ -292,7 +292,7 @@ export default function ({ getService }) { expect(body.attributes).an('object'); // one of the item of the cause array should point to our script - expect(body.attributes.cause.join(',')).contain('"hello with error'); + expect(body.attributes.causes.join(',')).contain('"hello with error'); }); }); @@ -306,7 +306,7 @@ export default function ({ getService }) { throw new Error(`Error creating template: ${createStatus} ${createBody.message}`); } - let catTemplateResponse = await catTemplate(templateName); + let { body: catTemplateResponse } = await catTemplate(templateName); expect( catTemplateResponse.find((template) => template.name === payload.name).name @@ -322,7 +322,7 @@ export default function ({ getService }) { expect(deleteBody.errors).to.be.empty; expect(deleteBody.templatesDeleted[0]).to.equal(templateName); - catTemplateResponse = await catTemplate(templateName); + ({ body: catTemplateResponse } = await catTemplate(templateName)); expect(catTemplateResponse.find((template) => template.name === payload.name)).to.equal( undefined @@ -335,7 +335,7 @@ export default function ({ getService }) { await createTemplate(payload).expect(200); - let catTemplateResponse = await catTemplate(templateName); + let { body: catTemplateResponse } = await catTemplate(templateName); expect( catTemplateResponse.find((template) => template.name === payload.name).name @@ -348,7 +348,7 @@ export default function ({ getService }) { expect(body.errors).to.be.empty; expect(body.templatesDeleted[0]).to.equal(templateName); - catTemplateResponse = await catTemplate(templateName); + ({ body: catTemplateResponse } = await catTemplate(templateName)); expect(catTemplateResponse.find((template) => template.name === payload.name)).to.equal( undefined diff --git a/x-pack/test/api_integration/apis/metrics_ui/metrics_alerting.ts b/x-pack/test/api_integration/apis/metrics_ui/metrics_alerting.ts index 6f9c0f5fc708f..31d4bd147f526 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/metrics_alerting.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/metrics_alerting.ts @@ -12,7 +12,7 @@ import { MetricExpressionParams } from '../../../../plugins/infra/server/lib/ale import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { - const client = getService('legacyEs'); + const client = getService('es'); const index = 'test-index'; const getSearchParams = (aggType: string) => ({ @@ -42,11 +42,11 @@ export default function ({ getService }: FtrProviderContext) { '@timestamp', timeframe ); - const result = await client.search({ + const { body: result } = await client.search({ index, body: searchBody, }); - expect(result.error).to.not.be.ok(); + expect(result.hits).to.be.ok(); expect(result.aggregations).to.be.ok(); }); @@ -63,11 +63,11 @@ export default function ({ getService }: FtrProviderContext) { undefined, '{"bool":{"should":[{"match_phrase":{"agent.hostname":"foo"}}],"minimum_should_match":1}}' ); - const result = await client.search({ + const { body: result } = await client.search({ index, body: searchBody, }); - expect(result.error).to.not.be.ok(); + expect(result.hits).to.be.ok(); expect(result.aggregations).to.be.ok(); }); @@ -85,11 +85,11 @@ export default function ({ getService }: FtrProviderContext) { timeframe, 'agent.id' ); - const result = await client.search({ + const { body: result } = await client.search({ index, body: searchBody, }); - expect(result.error).to.not.be.ok(); + expect(result.hits).to.be.ok(); expect(result.aggregations).to.be.ok(); }); @@ -106,11 +106,11 @@ export default function ({ getService }: FtrProviderContext) { 'agent.id', '{"bool":{"should":[{"match_phrase":{"agent.hostname":"foo"}}],"minimum_should_match":1}}' ); - const result = await client.search({ + const { body: result } = await client.search({ index, body: searchBody, }); - expect(result.error).to.not.be.ok(); + expect(result.hits).to.be.ok(); expect(result.aggregations).to.be.ok(); }); diff --git a/x-pack/test/api_integration/apis/monitoring/common/mappings_exist.js b/x-pack/test/api_integration/apis/monitoring/common/mappings_exist.js index 1f1151010cefb..f193e0dbe091a 100644 --- a/x-pack/test/api_integration/apis/monitoring/common/mappings_exist.js +++ b/x-pack/test/api_integration/apis/monitoring/common/mappings_exist.js @@ -14,7 +14,7 @@ import * as beatsMetrics from '../../../../../plugins/monitoring/server/lib/metr import * as apmMetrics from '../../../../../plugins/monitoring/server/lib/metrics/apm/metrics'; export default function ({ getService }) { - const es = getService('legacyEs'); + const es = getService('es'); const metricSets = [ { @@ -49,7 +49,7 @@ export default function ({ getService }) { let mappings; before('load mappings', async () => { - const template = await es.indices.getTemplate({ name: indexTemplate }); + const { body: template } = await es.indices.getTemplate({ name: indexTemplate }); mappings = get(template, [indexTemplate, 'mappings', 'properties']); }); diff --git a/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/cluster.json b/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/cluster.json index 13535347f437c..313481998d6c8 100644 --- a/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/cluster.json +++ b/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/cluster.json @@ -1,61 +1,66 @@ -[{ - "cluster_uuid": "__standalone_cluster__", - "license": {}, - "elasticsearch": { - "cluster_stats": { - "indices": { - "docs": {}, - "shards": {}, - "store": {} - }, - "nodes": { - "count": { - "total": {} +[ + { + "cluster_uuid": "__standalone_cluster__", + "license": {}, + "elasticsearch": { + "cluster_stats": { + "indices": { + "docs": {}, + "shards": {}, + "store": {} }, - "fs": {}, - "jvm": { - "mem": {} + "nodes": { + "count": { + "total": {} + }, + "fs": {}, + "jvm": { + "mem": {} + } } + }, + "logs": { + "enabled": false, + "reason": { + "clusterExists": false, + "indexPatternExists": false, + "indexPatternInTimeRangeExists": false, + "typeExistsAtAnyTime": false, + "usingStructuredLogs": false, + "nodeExists": null, + "indexExists": null, + "typeExists": false + }, + "types": [] } }, - "logs": { - "enabled": false, - "reason": { - "clusterExists": false, - "indexPatternExists": false, - "indexPatternInTimeRangeExists": false, - "typeExistsAtAnyTime": false, - "usingStructuredLogs": false, - "nodeExists": null, - "indexExists": null, - "typeExists": false - }, - "types": [] - } - }, - "logstash": {}, - "kibana": {}, - "beats": { - "totalEvents": 348, - "bytesSent": 319913, + "logstash": {}, + "kibana": {}, "beats": { - "total": 1, - "types": [{ - "type": "Packetbeat", - "count": 1 - }] - } - }, - "apm": { - "totalEvents": 0, - "memRss": 0, - "apms": { - "total": 0 + "totalEvents": 348, + "bytesSent": 319913, + "beats": { + "total": 1, + "types": [ + { + "type": "Packetbeat", + "count": 1 + } + ] + } }, - "config": { - "container": false + "apm": { + "totalEvents": 0, + "memRss": 0, + "apms": { + "total": 0 + }, + "config": { + "container": false + }, + "versions": [] }, - "versions": [] - }, - "isPrimary": false -}] + "isPrimary": false, + "isCcrEnabled": false + } +] diff --git a/x-pack/test/api_integration/services/legacy_es.js b/x-pack/test/api_integration/services/legacy_es.js index 0b02d394b107f..bc0f9946243c8 100644 --- a/x-pack/test/api_integration/services/legacy_es.js +++ b/x-pack/test/api_integration/services/legacy_es.js @@ -9,7 +9,6 @@ import { format as formatUrl } from 'url'; import * as legacyElasticsearch from 'elasticsearch'; -import { elasticsearchJsPlugin as indexManagementEsClientPlugin } from '../../../plugins/index_management/server/client/elasticsearch'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { DEFAULT_API_VERSION } from '../../../../src/core/server/elasticsearch/elasticsearch_config'; @@ -20,6 +19,5 @@ export function LegacyEsProvider({ getService }) { apiVersion: DEFAULT_API_VERSION, host: formatUrl(config.get('servers.elasticsearch')), requestTimeout: config.get('timeouts.esRequestTimeout'), - plugins: [indexManagementEsClientPlugin], }); } diff --git a/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts b/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts index 9319babc3896a..2ea0410419080 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts @@ -8,9 +8,13 @@ import expect from '@kbn/expect'; import { last, omit, pick, sortBy } from 'lodash'; import { ValuesType } from 'utility-types'; +import { Node, NodeType } from '../../../../../plugins/apm/common/connections'; import { createApmApiSupertest } from '../../../common/apm_api_supertest'; import { roundNumber } from '../../../utils'; -import { ENVIRONMENT_ALL } from '../../../../../plugins/apm/common/environment_filter_values'; +import { + ENVIRONMENT_ALL, + ENVIRONMENT_NOT_DEFINED, +} from '../../../../../plugins/apm/common/environment_filter_values'; import { APIReturnType } from '../../../../../plugins/apm/public/services/rest/createCallApmApi'; import archives from '../../../common/fixtures/es_archiver/archives_metadata'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; @@ -24,6 +28,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { const archiveName = 'apm_8.0.0'; const { start, end } = archives[archiveName]; + function getName(node: Node) { + return node.type === NodeType.service ? node.serviceName : node.backendName; + } + registry.when( 'Service overview dependencies when data is not loaded', { config: 'basic', archives: [] }, @@ -228,16 +236,17 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns opbeans-node as a dependency', () => { const opbeansNode = response.body.serviceDependencies.find( - (item) => item.type === 'service' && item.serviceName === 'opbeans-node' + (item) => getName(item.location) === 'opbeans-node' ); expect(opbeansNode !== undefined).to.be(true); const values = { - latency: roundNumber(opbeansNode?.latency.value), - throughput: roundNumber(opbeansNode?.throughput.value), - errorRate: roundNumber(opbeansNode?.errorRate.value), - ...pick(opbeansNode, 'serviceName', 'type', 'agentName', 'environment', 'impact'), + latency: roundNumber(opbeansNode?.currentStats.latency.value), + throughput: roundNumber(opbeansNode?.currentStats.throughput.value), + errorRate: roundNumber(opbeansNode?.currentStats.errorRate.value), + impact: opbeansNode?.currentStats.impact, + ...pick(opbeansNode?.location, 'serviceName', 'type', 'agentName', 'environment'), }; const count = 4; @@ -246,7 +255,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(values).to.eql({ agentName: 'nodejs', - environment: '', + environment: ENVIRONMENT_NOT_DEFINED.value, serviceName: 'opbeans-node', type: 'service', errorRate: roundNumber(errors / count), @@ -255,8 +264,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { impact: 100, }); - const firstValue = roundNumber(opbeansNode?.latency.timeseries[0].y); - const lastValue = roundNumber(last(opbeansNode?.latency.timeseries)?.y); + const firstValue = roundNumber(opbeansNode?.currentStats.latency.timeseries[0].y); + const lastValue = roundNumber(last(opbeansNode?.currentStats.latency.timeseries)?.y); expect(firstValue).to.be(roundNumber(20 / 3)); expect(lastValue).to.be('1.000'); @@ -264,16 +273,17 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns postgres as an external dependency', () => { const postgres = response.body.serviceDependencies.find( - (item) => item.type === 'external' && item.name === 'postgres' + (item) => getName(item.location) === 'postgres' ); expect(postgres !== undefined).to.be(true); const values = { - latency: roundNumber(postgres?.latency.value), - throughput: roundNumber(postgres?.throughput.value), - errorRate: roundNumber(postgres?.errorRate.value), - ...pick(postgres, 'spanType', 'spanSubtype', 'name', 'impact', 'type'), + latency: roundNumber(postgres?.currentStats.latency.value), + throughput: roundNumber(postgres?.currentStats.throughput.value), + errorRate: roundNumber(postgres?.currentStats.errorRate.value), + impact: postgres?.currentStats.impact, + ...pick(postgres?.location, 'spanType', 'spanSubtype', 'backendName', 'type'), }; const count = 1; @@ -283,8 +293,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(values).to.eql({ spanType: 'external', spanSubtype: 'http', - name: 'postgres', - type: 'external', + backendName: 'postgres', + type: 'backend', errorRate: roundNumber(errors / count), latency: roundNumber(sum / count), throughput: roundNumber(count / ((endTime - startTime) / 1000 / 60)), @@ -325,8 +335,25 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns at least one item', () => { expect(response.body.serviceDependencies.length).to.be.greaterThan(0); + expectSnapshot(response.body.serviceDependencies.length).toMatchInline(`4`); + + const { currentStats, ...firstItem } = sortBy( + response.body.serviceDependencies, + 'currentStats.impact' + ).reverse()[0]; + + expectSnapshot(firstItem.location).toMatchInline(` + Object { + "backendName": "postgresql", + "id": "d4e2a4d33829d41c096c26f8037921cfc7e566b2", + "spanSubtype": "postgresql", + "spanType": "db", + "type": "backend", + } + `); + expectSnapshot( - omit(response.body.serviceDependencies[0], [ + omit(currentStats, [ 'errorRate.timeseries', 'throughput.timeseries', 'latency.timeseries', @@ -340,19 +367,15 @@ export default function ApiTest({ getService }: FtrProviderContext) { "latency": Object { "value": 30177.8418777023, }, - "name": "postgresql", - "spanSubtype": "postgresql", - "spanType": "db", "throughput": Object { "value": 53.9666666666667, }, - "type": "external", } `); }); it('returns the right names', () => { - const names = response.body.serviceDependencies.map((item) => item.name); + const names = response.body.serviceDependencies.map((item) => getName(item.location)); expectSnapshot(names.sort()).toMatchInline(` Array [ "elasticsearch", @@ -365,7 +388,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns the right service names', () => { const serviceNames = response.body.serviceDependencies - .map((item) => (item.type === 'service' ? item.serviceName : undefined)) + .map((item) => + item.location.type === NodeType.service ? getName(item.location) : undefined + ) .filter(Boolean); expectSnapshot(serviceNames.sort()).toMatchInline(` @@ -378,8 +403,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns the right latency values', () => { const latencyValues = sortBy( response.body.serviceDependencies.map((item) => ({ - name: item.name, - latency: item.latency.value, + name: getName(item.location), + latency: item.currentStats.latency.value, })), 'name' ); @@ -409,8 +434,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns the right throughput values', () => { const throughputValues = sortBy( response.body.serviceDependencies.map((item) => ({ - name: item.name, - throughput: item.throughput.value, + name: getName(item.location), + throughput: item.currentStats.throughput.value, })), 'name' ); @@ -440,10 +465,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns the right impact values', () => { const impactValues = sortBy( response.body.serviceDependencies.map((item) => ({ - name: item.name, - impact: item.impact, - latency: item.latency.value, - throughput: item.throughput.value, + name: getName(item.location), + impact: item.currentStats.impact, + latency: item.currentStats.latency.value, + throughput: item.currentStats.throughput.value, })), 'name' ); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts index 3729b20f82b30..79af6bb279a3d 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts @@ -24,6 +24,7 @@ import { ExternalServiceSimulator, getExternalServiceSimulatorPath, } from '../../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; +import { getCreateConnectorUrl } from '../../../../../../../plugins/cases/common/utils/connectors_api'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -49,7 +50,7 @@ export default ({ getService }: FtrProviderContext): void => { it(`on new push to service, user action: 'push-to-service' should be called with actionFields: ['pushed']`, async () => { const { body: connector } = await supertest - .post('/api/actions/connector') + .post(getCreateConnectorUrl()) .set('kbn-xsrf', 'true') .send({ ...getServiceNowConnector(), diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts index 4c72dafed053b..f72db1ac1b27e 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts @@ -34,7 +34,8 @@ export default ({ getService }: FtrProviderContext): void => { const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); - describe('get_reporters', () => { + // Failing: See https://github.com/elastic/kibana/issues/106658 + describe.skip('get_reporters', () => { afterEach(async () => { await deleteCasesByESQuery(es); }); diff --git a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts index 10846442d1c84..e1cf24521d1b1 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts @@ -134,7 +134,8 @@ function defineTypeWithMigration(core: CoreSetup, deps: PluginsSet core.savedObjects.registerType({ name: SAVED_OBJECT_WITH_MIGRATION_TYPE, hidden: false, - namespaceType: 'single', + namespaceType: 'multiple-isolated', // in data.json, we simulate that existing objects were created with `namespaceType: 'single'` + convertToMultiNamespaceTypeVersion: '8.0.0', // in this version we convert from a single-namespace type to a "share-capable" multi-namespace isolated type mappings: { properties: { nonEncryptedAttribute: { @@ -199,6 +200,18 @@ function defineTypeWithMigration(core: CoreSetup, deps: PluginsSet }, inputType: typePriorTo790, }), + + // NOTE FOR MAINTAINERS: do not add any more migrations before 8.0.0 unless you regenerate the test data for two of the objects in + // data.json: '362828f0-eef2-11eb-9073-11359682300a' and '36448a90-eef2-11eb-9073-11359682300a. These are used in the test cases 'for + // a saved object that does not need to be migrated before it is converted'. + + // This empty migration is necessary to ensure that the saved object is decrypted with its old descriptor/ and re-encrypted with its + // new descriptor, if necessary. This is included because the saved object is being converted to `namespaceType: 'multiple-isolated'` + // in 8.0.0 (see the `convertToMultiNamespaceTypeVersion` field in the saved object type registration process). + '8.0.0': deps.encryptedSavedObjects.createMigration({ + isMigrationNeededPredicate: (doc): doc is SavedObjectUnsanitizedDoc => true, + migration: (doc) => doc, // no-op + }), }, }); } diff --git a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/data.json b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/data.json index 88ec54cdf3a54..71ac4dfc974d4 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/data.json +++ b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/data.json @@ -223,6 +223,70 @@ } } +{ + "type": "doc", + "value": { + "id": "custom-space:saved-object-with-migration:a67c6950-eed8-11eb-9a62-032b4e4049d1", + "index": ".kibana_1", + "source": { + "saved-object-with-migration": { + "encryptedAttribute": "BIOBsx5SjLq3ZQdOJv06XeCAMY9ZrYj8K5bcGa5+wpd3TeT2sqln1+9AGblnfxT7LXRI3sLWQ900+wRQzBhJYx8PNKH+Yw+GdeESpu73PFHdWt/52cJKr+b4EPALFc00tIMEDHdT9FyQhqJ7nV8UpwtjcuTp9SA=", + "nonEncryptedAttribute": "elastic" + }, + "type": "saved-object-with-migration", + "references": [], + "namespace": "custom-space", + "migrationVersion": { + "saved-object-with-migration": "7.7.0" + }, + "updated_at": "2021-07-27T12:46:23.881Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "saved-object-with-migration:362828f0-eef2-11eb-9073-11359682300a", + "index": ".kibana_1", + "source": { + "saved-object-with-migration": { + "encryptedAttribute": "wWDAtF/5PkCb5BxjfWyRxoIoHbJXlb5cGAKg9ztZ1Bz9Zwo0/xf2yTa3Gq/CbYrvey/F9FZkZOUk03USPaqa5mfFO8FhORkfmNLQaPhgCIDNd6SbIhN8RYkqWVTYSVgcZrwes+VwiTUZ29mCJprVSHwXdyAOy4g=", + "nonEncryptedAttribute": "elastic-migrated", + "additionalEncryptedAttribute": "mszSQj0+Wv7G6kZJQsqf7CWwjJwwyriMlBcUjSHTLlj+tljbLTb7PI7gR07S9l7BXd3Lquc5PeOJifl2HvnTh8s871d/WdtIvt2K/ggwA2ae9NH6ui8A15cuPlXiGO612qccsIyBzhsftFyWJNuLBApmqeEy7HFe" + }, + "type": "saved-object-with-migration", + "references": [], + "migrationVersion": { + "saved-object-with-migration": "7.9.0" + }, + "updated_at": "2021-07-27T15:49:22.324Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "custom-space:saved-object-with-migration:36448a90-eef2-11eb-9073-11359682300a", + "index": ".kibana_1", + "source": { + "saved-object-with-migration": { + "encryptedAttribute": "33lfpnBI136UfkdcQLzovzBXdUaeDouN0Z32qkVutgZJ5SU60hMtaHWXNkaU9DGy9jtr0ptwm6FCYmZbyDrlGMwyZP2n0PzMhwW9fRcBh7he12Cm1mImWTrxgYoRtc1MX20/orbINx5VnuNl1Ide7htAm1oPRjM=", + "nonEncryptedAttribute": "elastic-migrated", + "additionalEncryptedAttribute": "e2rsxBijtMGcdw7A+WAWJNlLOhQCZnEP1sdcHxVO5aQouiUVeI1OTFcOY3h/+iZBlSGvZdGRURgimrSNc0HRicemZx3o4v1gVw0JX3RRatzdl02v3GJoFzBWfQGyf3xhNNWmkweGJrFQqr2kfdKjIHbdVmMt4LZj" + }, + "type": "saved-object-with-migration", + "references": [], + "namespace": "custom-space", + "migrationVersion": { + "saved-object-with-migration": "7.9.0" + }, + "updated_at": "2021-07-27T15:49:22.509Z" + } + } +} + { "type": "doc", "value": { @@ -367,4 +431,4 @@ "updated_at": "2020-06-17T16:29:27.563Z" } } -} \ No newline at end of file +} diff --git a/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts index 0b01f4f385da6..311228424afe3 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts @@ -527,20 +527,103 @@ export default function ({ getService }: FtrProviderContext) { ); }); - it('migrates unencrypted fields on saved objects', async () => { - const { body: decryptedResponse } = await supertest - .get( - `/api/saved_objects/get-decrypted-as-internal-user/saved-object-with-migration/74f3e6d7-b7bb-477d-ac28-92ee22728e6e` - ) - .expect(200); + function getGetApiUrl({ objectId, spaceId }: { objectId: string; spaceId?: string }) { + const spacePrefix = spaceId ? `/s/${spaceId}` : ''; + return `${spacePrefix}/api/saved_objects/get-decrypted-as-internal-user/saved-object-with-migration/${objectId}`; + } + + // For brevity, each encrypted saved object has the same decrypted attributes after migrations/conversion. + // An assertion based on this ensures all encrypted fields can still be decrypted after migrations/conversion have been applied. + const expectedDecryptedAttributes = { + encryptedAttribute: 'this is my secret api key', + nonEncryptedAttribute: 'elastic-migrated', // this field was migrated in 7.8.0 + additionalEncryptedAttribute: 'elastic-migrated-encrypted', // this field was added in 7.9.0 + }; + + // In these test cases, we simulate a scenario where some existing objects that are migrated when Kibana starts up. Note that when a + // document migration is triggered, the saved object "convert" transform is also applied by the Core migration algorithm. + describe('handles index migration correctly', () => { + describe('in the default space', () => { + it('for a saved object that needs to be migrated before it is converted', async () => { + const getApiUrl = getGetApiUrl({ objectId: '74f3e6d7-b7bb-477d-ac28-92ee22728e6e' }); + const { body: decryptedResponse } = await supertest.get(getApiUrl).expect(200); + expect(decryptedResponse.attributes).to.eql(expectedDecryptedAttributes); + }); + + it('for a saved object that does not need to be migrated before it is converted', async () => { + const getApiUrl = getGetApiUrl({ objectId: '362828f0-eef2-11eb-9073-11359682300a' }); + const { body: decryptedResponse } = await supertest.get(getApiUrl).expect(200); + expect(decryptedResponse.attributes).to.eql(expectedDecryptedAttributes); + }); + }); + + describe('in a custom space', () => { + const spaceId = 'custom-space'; + + it('for a saved object that needs to be migrated before it is converted', async () => { + const getApiUrl = getGetApiUrl({ + objectId: 'a98e22f8-530e-5d69-baf7-97526796f3a6', // This ID is not found in the data.json file, it is dynamically generated when the object is converted; the original ID is a67c6950-eed8-11eb-9a62-032b4e4049d1 + spaceId, + }); + const { body: decryptedResponse } = await supertest.get(getApiUrl).expect(200); + expect(decryptedResponse.attributes).to.eql(expectedDecryptedAttributes); + }); + + it('for a saved object that does not need to be migrated before it is converted', async () => { + const getApiUrl = getGetApiUrl({ + objectId: '41395c74-da7a-5679-9535-412d550a6cf7', // This ID is not found in the data.json file, it is dynamically generated when the object is converted; the original ID is 36448a90-eef2-11eb-9073-11359682300a + spaceId, + }); + const { body: decryptedResponse } = await supertest.get(getApiUrl).expect(200); + expect(decryptedResponse.attributes).to.eql(expectedDecryptedAttributes); + }); + }); + }); + + // In these test cases, we simulate a scenario where new objects are migrated upon creation. This happens because an outdated + // `migrationVersion` field is included below. Note that when a document migration is triggered, the saved object "convert" transform + // is *not* applied by the Core migration algorithm. + describe('handles document migration correctly', () => { + function getCreateApiUrl({ spaceId }: { spaceId?: string } = {}) { + const spacePrefix = spaceId ? `/s/${spaceId}` : ''; + return `${spacePrefix}/api/saved_objects/saved-object-with-migration`; + } + + const objectToCreate = { + attributes: { + encryptedAttribute: 'this is my secret api key', + nonEncryptedAttribute: 'elastic', + }, + migrationVersion: { 'saved-object-with-migration': '7.7.0' }, + }; + + it('in the default space', async () => { + const createApiUrl = getCreateApiUrl(); + const { body: savedObject } = await supertest + .post(createApiUrl) + .set('kbn-xsrf', 'xxx') + .send(objectToCreate) + .expect(200); + const { id: objectId } = savedObject; + + const getApiUrl = getGetApiUrl({ objectId }); + const { body: decryptedResponse } = await supertest.get(getApiUrl).expect(200); + expect(decryptedResponse.attributes).to.eql(expectedDecryptedAttributes); + }); + + it('in a custom space', async () => { + const spaceId = 'custom-space'; + const createApiUrl = getCreateApiUrl({ spaceId }); + const { body: savedObject } = await supertest + .post(createApiUrl) + .set('kbn-xsrf', 'xxx') + .send(objectToCreate) + .expect(200); + const { id: objectId } = savedObject; - expect(decryptedResponse.attributes).to.eql({ - // ensures the encrypted field can still be decrypted after the migration - encryptedAttribute: 'this is my secret api key', - // ensures the non-encrypted field has been migrated in 7.8.0 - nonEncryptedAttribute: 'elastic-migrated', - // ensures the non-encrypted field has been migrated into a new encrypted field in 7.9.0 - additionalEncryptedAttribute: 'elastic-migrated-encrypted', + const getApiUrl = getGetApiUrl({ objectId, spaceId }); + const { body: decryptedResponse } = await supertest.get(getApiUrl).expect(200); + expect(decryptedResponse.attributes).to.eql(expectedDecryptedAttributes); }); }); }); diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts b/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts index 182838f21dbda..06130775ec3cb 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts @@ -93,6 +93,7 @@ export default function (providerContext: FtrProviderContext) { delete packageInfoRes.body.response.savedObject.version; delete packageInfoRes.body.response.savedObject.updated_at; delete packageInfoRes.body.response.savedObject.coreMigrationVersion; + delete packageInfoRes.body.response.savedObject.migrationVersion; expectSnapshot(packageInfoRes.body.response).toMatch(); }); diff --git a/x-pack/test/functional/apps/dashboard/index.ts b/x-pack/test/functional/apps/dashboard/index.ts index d5aded01fce7b..5979ae378c22b 100644 --- a/x-pack/test/functional/apps/dashboard/index.ts +++ b/x-pack/test/functional/apps/dashboard/index.ts @@ -21,6 +21,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./dashboard_maps_by_value')); loadTestFile(require.resolve('./migration_smoke_tests/lens_migration_smoke_test')); + loadTestFile(require.resolve('./migration_smoke_tests/visualize_migration_smoke_test')); loadTestFile(require.resolve('./migration_smoke_tests/tsvb_migration_smoke_test')); }); } diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/visualize_dashboard_migration_test_7_12_1.ndjson b/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/visualize_dashboard_migration_test_7_12_1.ndjson new file mode 100644 index 0000000000000..9b7ce6ffdd999 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/visualize_dashboard_migration_test_7_12_1.ndjson @@ -0,0 +1,7 @@ +{"attributes":{"fieldAttrs":"{}","fields":"[]","runtimeFieldMap":"{}","title":"shakespeare"},"coreMigrationVersion":"7.12.1","id":"7e9d4c70-e667-11eb-86e8-1ffd09dc5582","migrationVersion":{"index-pattern":"7.11.0"},"references":[],"type":"index-pattern","updated_at":"2021-07-16T18:56:14.524Z","version":"WzM4LDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"Speaker Count (Area Chart By-Ref)","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"Speaker Count (Area Chart By-Ref)\",\"type\":\"area\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{\"customLabel\":\"Line Count\"},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"speaker\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":10,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"Speaker\"},\"schema\":\"segment\"}],\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{},\"style\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Line Count\"},\"style\":{}}],\"seriesParams\":[{\"show\":true,\"type\":\"histogram\",\"mode\":\"stacked\",\"data\":{\"label\":\"Line Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"detailedTooltip\":true,\"palette\":{\"type\":\"palette\",\"name\":\"default\"},\"addLegend\":true,\"legendPosition\":\"right\",\"fittingFunction\":\"linear\",\"times\":[],\"addTimeMarker\":false,\"radiusRatio\":9,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{}}}"},"coreMigrationVersion":"7.12.1","id":"9f90fae0-e66b-11eb-86e8-1ffd09dc5582","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"7e9d4c70-e667-11eb-86e8-1ffd09dc5582","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"visualization","updated_at":"2021-07-19T15:32:03.822Z","version":"WzU0NSwxXQ=="} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"Tag Cloud","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"Tag Cloud\",\"type\":\"tagcloud\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"text_entry.keyword\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":100,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"segment\"}],\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":72,\"showLabel\":true}}"},"coreMigrationVersion":"7.12.1","id":"954e2df0-e66b-11eb-86e8-1ffd09dc5582","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"7e9d4c70-e667-11eb-86e8-1ffd09dc5582","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"visualization","updated_at":"2021-07-16T19:25:30.580Z","version":"WzIwMSwxXQ=="} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"Unique Speakers by Play (Pie chart By-Ref)","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"Unique Speakers by Play (Pie chart By-Ref)\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"cardinality\",\"params\":{\"field\":\"speaker\"},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"play_name\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":5,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"segment\"}]}"},"coreMigrationVersion":"7.12.1","id":"6b818220-e66f-11eb-86e8-1ffd09dc5582","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"7e9d4c70-e667-11eb-86e8-1ffd09dc5582","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"visualization","updated_at":"2021-07-16T19:52:58.437Z","version":"WzMxMiwxXQ=="} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"speaker : \\\"HAMLET\\\" \",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"Hamlet Speaking Overtime (Area chart By-Ref)","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"Hamlet Speaking Overtime (Area chart By-Ref)\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{},\"style\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"},\"style\":{}}],\"seriesParams\":[{\"show\":true,\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"detailedTooltip\":true,\"palette\":{\"type\":\"palette\",\"name\":\"default\"},\"addLegend\":true,\"legendPosition\":\"right\",\"fittingFunction\":\"linear\",\"times\":[],\"addTimeMarker\":false,\"radiusRatio\":9,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"speech_number\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":100,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"segment\"}]}"},"coreMigrationVersion":"7.12.1","id":"3b53b990-e8a6-11eb-86e8-1ffd09dc5582","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"7e9d4c70-e667-11eb-86e8-1ffd09dc5582","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"visualization","updated_at":"2021-07-19T15:30:22.251Z","version":"WzUxMSwxXQ=="} +{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.12.1\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"b05dcb4e-d866-43cd-a6af-d26def3f6231\"},\"panelIndex\":\"b05dcb4e-d866-43cd-a6af-d26def3f6231\",\"embeddableConfig\":{\"savedVis\":{\"title\":\"\",\"description\":\"\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{},\"style\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Line Count\"},\"style\":{}}],\"seriesParams\":[{\"show\":true,\"type\":\"histogram\",\"mode\":\"stacked\",\"data\":{\"label\":\"Line Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"detailedTooltip\":true,\"palette\":{\"type\":\"palette\",\"name\":\"default\"},\"addLegend\":true,\"legendPosition\":\"right\",\"fittingFunction\":\"linear\",\"times\":[],\"addTimeMarker\":false,\"radiusRatio\":9,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{}},\"uiState\":{},\"data\":{\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{\"customLabel\":\"Line Count\"},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"speaker\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":10,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"Speaker\"},\"schema\":\"segment\"}],\"searchSource\":{\"index\":\"7e9d4c70-e667-11eb-86e8-1ffd09dc5582\",\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}},\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"Speaker Count (Area Chart By-Value)\"},{\"version\":\"7.12.1\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"52747d95-33a3-4c36-b870-4a23f3e4dfec\"},\"panelIndex\":\"52747d95-33a3-4c36-b870-4a23f3e4dfec\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"7.12.1\",\"gridData\":{\"x\":24,\"y\":15,\"w\":24,\"h\":15,\"i\":\"d7df311d-e0cf-45f2-82cf-80b5a891fa9a\"},\"panelIndex\":\"d7df311d-e0cf-45f2-82cf-80b5a891fa9a\",\"embeddableConfig\":{\"savedVis\":{\"title\":\"Word Cloud\",\"description\":\"\",\"type\":\"tagcloud\",\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":72,\"showLabel\":true},\"uiState\":{},\"data\":{\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"text_entry.keyword\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":100,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"segment\"}],\"searchSource\":{\"index\":\"7e9d4c70-e667-11eb-86e8-1ffd09dc5582\",\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}},\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"Words (Tag Cloud By-Ref)\",\"panelRefName\":\"panel_2\"},{\"version\":\"7.12.1\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"ab307583-e63e-41fe-adc6-5be5f9b8b053\"},\"panelIndex\":\"ab307583-e63e-41fe-adc6-5be5f9b8b053\",\"embeddableConfig\":{\"savedVis\":{\"title\":\"\",\"description\":\"\",\"type\":\"tagcloud\",\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":72,\"showLabel\":true},\"uiState\":{},\"data\":{\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"text_entry.keyword\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":100,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"segment\"}],\"searchSource\":{\"index\":\"7e9d4c70-e667-11eb-86e8-1ffd09dc5582\",\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}},\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"Words (Tag Cloud By-Value)\"},{\"version\":\"7.12.1\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":30,\"w\":24,\"h\":15,\"i\":\"d6be9fb1-7f48-4175-a29a-fb80420cceb9\"},\"panelIndex\":\"d6be9fb1-7f48-4175-a29a-fb80420cceb9\",\"embeddableConfig\":{\"savedVis\":{\"title\":\"\",\"description\":\"\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100}},\"uiState\":{},\"data\":{\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"cardinality\",\"params\":{\"field\":\"speaker\"},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"play_name\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":5,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"segment\"}],\"searchSource\":{\"index\":\"7e9d4c70-e667-11eb-86e8-1ffd09dc5582\",\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}},\"hidePanelTitles\":false,\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"b8203dae-afe5-4d64-9c13-8ad62206b8e1\",\"triggers\":[\"VALUE_CLICK_TRIGGER\"],\"action\":{\"name\":\"Shakespeare Search\",\"config\":{\"url\":{\"template\":\"https://shakespeare.folger.edu/search/?search_text={{event.value}}\"},\"openInNewTab\":true,\"encodeUrl\":true},\"factoryId\":\"URL_DRILLDOWN\"}}]}}},\"title\":\"Unique Speakers by Play (Pie chart By-Value)\"},{\"version\":\"7.12.1\",\"gridData\":{\"x\":24,\"y\":30,\"w\":24,\"h\":15,\"i\":\"5098cff8-0d4f-4a71-8d1b-18d19a018b1f\"},\"panelIndex\":\"5098cff8-0d4f-4a71-8d1b-18d19a018b1f\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"910c4965-8781-49b9-9d6d-18ea8241a96a\",\"triggers\":[\"VALUE_CLICK_TRIGGER\"],\"action\":{\"name\":\"Shakespeare Search\",\"config\":{\"url\":{\"template\":\"https://shakespeare.folger.edu/search/?search_text={{event.value}}\"},\"openInNewTab\":true,\"encodeUrl\":true},\"factoryId\":\"URL_DRILLDOWN\"}}]}}},\"panelRefName\":\"panel_5\"},{\"version\":\"7.12.1\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":45,\"w\":24,\"h\":15,\"i\":\"2fc385e6-dfe5-49d6-8302-62c17d7a50a4\"},\"panelIndex\":\"2fc385e6-dfe5-49d6-8302-62c17d7a50a4\",\"embeddableConfig\":{\"savedVis\":{\"title\":\"\",\"description\":\"\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{},\"style\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"},\"style\":{}}],\"seriesParams\":[{\"show\":true,\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"detailedTooltip\":true,\"palette\":{\"type\":\"palette\",\"name\":\"default\"},\"addLegend\":true,\"legendPosition\":\"right\",\"fittingFunction\":\"linear\",\"times\":[],\"addTimeMarker\":false,\"radiusRatio\":9,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{}},\"uiState\":{},\"data\":{\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"speech_number\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":100,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"segment\"}],\"searchSource\":{\"index\":\"7e9d4c70-e667-11eb-86e8-1ffd09dc5582\",\"query\":{\"query\":\"speaker : \\\"HAMLET\\\" \",\"language\":\"kuery\"},\"filter\":[]}}},\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"Hamlet Speaking Overtime (Area chart By-Value)\"},{\"version\":\"7.12.1\",\"gridData\":{\"x\":24,\"y\":45,\"w\":24,\"h\":15,\"i\":\"964c5ffe-aae3-40b1-8240-14c5a218bbe2\"},\"panelIndex\":\"964c5ffe-aae3-40b1-8240-14c5a218bbe2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_7\"}]","timeRestore":false,"title":"[7.12.1] Visualize Test Dashboard","version":1},"coreMigrationVersion":"7.12.1","id":"8a8f5a90-e668-11eb-86e8-1ffd09dc5582","migrationVersion":{"dashboard":"7.11.0"},"references":[{"id":"7e9d4c70-e667-11eb-86e8-1ffd09dc5582","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"},{"id":"7e9d4c70-e667-11eb-86e8-1ffd09dc5582","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"},{"id":"7e9d4c70-e667-11eb-86e8-1ffd09dc5582","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"},{"id":"7e9d4c70-e667-11eb-86e8-1ffd09dc5582","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"},{"id":"7e9d4c70-e667-11eb-86e8-1ffd09dc5582","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"},{"id":"9f90fae0-e66b-11eb-86e8-1ffd09dc5582","name":"panel_1","type":"visualization"},{"id":"954e2df0-e66b-11eb-86e8-1ffd09dc5582","name":"panel_2","type":"visualization"},{"id":"6b818220-e66f-11eb-86e8-1ffd09dc5582","name":"panel_5","type":"visualization"},{"id":"3b53b990-e8a6-11eb-86e8-1ffd09dc5582","name":"panel_7","type":"visualization"}],"type":"dashboard","updated_at":"2021-07-19T15:49:46.191Z","version":"WzU4NywxXQ=="} +{"exportedCount":6,"missingRefCount":0,"missingReferences":[]} \ No newline at end of file diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/visualize_migration_smoke_test.ts b/x-pack/test/functional/apps/dashboard/migration_smoke_tests/visualize_migration_smoke_test.ts new file mode 100644 index 0000000000000..d3d6ca46cd227 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/migration_smoke_tests/visualize_migration_smoke_test.ts @@ -0,0 +1,83 @@ +/* + * 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. + */ + +/* This test is importing saved objects from 7.13.0 to 8.0 and the backported version + * will import from 6.8.x to 7.x.x + */ + +import expect from '@kbn/expect'; +import path from 'path'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); + const dashboardPanelActions = getService('dashboardPanelActions'); + + const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects', 'dashboard']); + + describe('Export import saved objects between versions', () => { + before(async () => { + await esArchiver.loadIfNeeded( + 'x-pack/test/functional/es_archives/getting_started/shakespeare' + ); + await kibanaServer.uiSettings.replace({}); + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSavedObjects(); + await PageObjects.savedObjects.importFile( + path.join(__dirname, 'exports', 'visualize_dashboard_migration_test_7_12_1.ndjson') + ); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/getting_started/shakespeare'); + await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); + }); + + it('should be able to import dashboard with various Visualize panels from 7.12.1', async () => { + // this will catch cases where there is an error in the migrations. + await PageObjects.savedObjects.checkImportSucceeded(); + await PageObjects.savedObjects.clickImportDone(); + }); + + it('should render all panels on the dashboard', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('[7.12.1] Visualize Test Dashboard'); + + // dashboard should load properly + await PageObjects.dashboard.expectOnDashboard('[7.12.1] Visualize Test Dashboard'); + await PageObjects.dashboard.waitForRenderComplete(); + + // There should be 0 error embeddables on the dashboard + const errorEmbeddables = await testSubjects.findAll('embeddableStackError'); + expect(errorEmbeddables.length).to.be(0); + }); + + it('should show the edit action for all panels', async () => { + await PageObjects.dashboard.switchToEditMode(); + + // All panels should be editable. This will catch cases where an error does not create an error embeddable. + const panelTitles = await PageObjects.dashboard.getPanelTitles(); + for (const title of panelTitles) { + await dashboardPanelActions.expectExistsEditPanelAction(title); + } + }); + + it('should retain all panel drilldowns from 7.12.1', async () => { + // Both panels configured with drilldowns in 7.12.1 should still have drilldowns. + const totalPanels = await PageObjects.dashboard.getPanelCount(); + let panelsWithDrilldowns = 0; + for (let panelIndex = 0; panelIndex < totalPanels; panelIndex++) { + if ((await PageObjects.dashboard.getPanelDrilldownCount(panelIndex)) === 1) { + panelsWithDrilldowns++; + } + } + expect(panelsWithDrilldowns).to.be(2); + }); + }); +} diff --git a/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts b/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts index bd70a50724a9c..c51e2968baee0 100644 --- a/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts +++ b/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts @@ -52,13 +52,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.indexLifecycleManagement.increasePolicyListPageSize(); - const allPolicies = await pageObjects.indexLifecycleManagement.getPolicyList(); + const createdPolicy = await pageObjects.indexLifecycleManagement.getPolicyRow(policyName); - const filteredPolicies = allPolicies.filter(function (policy) { - return policy.name === policyName; - }); - - expect(filteredPolicies.length).to.be(1); + expect(createdPolicy.length).to.be(1); }); }); }; diff --git a/x-pack/test/functional/apps/reporting_management/report_listing.ts b/x-pack/test/functional/apps/reporting_management/report_listing.ts index 0b1fce3700986..eb2e339e9be66 100644 --- a/x-pack/test/functional/apps/reporting_management/report_listing.ts +++ b/x-pack/test/functional/apps/reporting_management/report_listing.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import { REPORT_TABLE_ID } from '../../../../plugins/reporting/common/constants'; import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { @@ -37,7 +38,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // to reset the data after deletion testing await esArchiver.load('x-pack/test/functional/es_archives/reporting/archived_reports'); await pageObjects.common.navigateToApp('reporting'); - await testSubjects.existOrFail('reportJobListing', { timeout: 200000 }); + await testSubjects.existOrFail(REPORT_TABLE_ID, { timeout: 200000 }); }); after(async () => { @@ -52,7 +53,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('Confirm single report deletion works', async () => { log.debug('Checking for reports.'); await retry.try(async () => { - await testSubjects.click('checkboxSelectRow-k9a9xlwl0gpe1457b10rraq3'); + await testSubjects.click('checkboxSelectRow-krb7arhe164k0763b50bjm29'); }); const deleteButton = await testSubjects.find('deleteReportButton'); await retry.waitFor('delete button to become enabled', async () => { @@ -62,7 +63,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.exists('confirmModalBodyText'); await testSubjects.click('confirmModalConfirmButton'); await retry.try(async () => { - await testSubjects.waitForDeleted('checkboxSelectRow-k9a9xlwl0gpe1457b10rraq3'); + await testSubjects.waitForDeleted('checkboxSelectRow-krb7arhe164k0763b50bjm29'); }); }); @@ -71,10 +72,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const previousButton = await testSubjects.find('pagination-button-previous'); expect(await previousButton.getAttribute('disabled')).to.be('true'); - await testSubjects.find('checkboxSelectRow-k9a9xlwl0gpe1457b10rraq3'); // find first row of page 1 + await testSubjects.find('checkboxSelectRow-krb7arhe164k0763b50bjm29'); // find first row of page 1 await testSubjects.click('pagination-button-1'); // click page 2 - await testSubjects.find('checkboxSelectRow-k9a9uc4x0gpe1457b16wthc8'); // wait for first row of page 2 + await testSubjects.find('checkboxSelectRow-kraz0qle154g0763b569zz83'); // wait for first row of page 2 await testSubjects.click('pagination-button-2'); // click page 3 await testSubjects.find('checkboxSelectRow-k9a9p1840gpe1457b1ghfxw5'); // wait for first row of page 3 @@ -82,5 +83,73 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // previous CAN be clicked expect(await previousButton.getAttribute('disabled')).to.be(null); }); + + it('Displays types of report jobs', async () => { + const list = await pageObjects.reporting.getManagementList(); + expectSnapshot(list).toMatchInline(` + Array [ + Object { + "actions": "", + "createdAt": "2021-07-19 @ 10:29 PMtest_user", + "report": "Automated reportsearch", + "status": "Completed at 2021-07-19 @ 10:29 PMSee report info for warnings.", + }, + Object { + "actions": "", + "createdAt": "2021-07-19 @ 06:47 PMtest_user", + "report": "Discover search [2021-07-19T11:47:35.995-07:00]search", + "status": "Completed at 2021-07-19 @ 06:47 PM", + }, + Object { + "actions": "", + "createdAt": "2021-07-19 @ 06:46 PMtest_user", + "report": "Discover search [2021-07-19T11:46:00.132-07:00]search", + "status": "Completed at 2021-07-19 @ 06:46 PMSee report info for warnings.", + }, + Object { + "actions": "", + "createdAt": "2021-07-19 @ 06:44 PMtest_user", + "report": "Discover search [2021-07-19T11:44:48.670-07:00]search", + "status": "Completed at 2021-07-19 @ 06:44 PMSee report info for warnings.", + }, + Object { + "actions": "", + "createdAt": "2021-07-19 @ 06:41 PMtest_user", + "report": "[Flights] Global Flight Dashboarddashboard", + "status": "Pending at 2021-07-19 @ 06:41 PMWaiting for job to be processed.", + }, + Object { + "actions": "", + "createdAt": "2021-07-19 @ 06:41 PMtest_user", + "report": "[Flights] Global Flight Dashboarddashboard", + "status": "Failed at 2021-07-19 @ 06:43 PMSee report info for error details.", + }, + Object { + "actions": "", + "createdAt": "2021-07-19 @ 06:41 PMtest_user", + "report": "[Flights] Global Flight Dashboarddashboard", + "status": "Completed at 2021-07-19 @ 06:41 PMSee report info for warnings.", + }, + Object { + "actions": "", + "createdAt": "2021-07-19 @ 06:38 PMtest_user", + "report": "[Flights] Global Flight Dashboarddashboard", + "status": "Completed at 2021-07-19 @ 06:39 PMSee report info for warnings.", + }, + Object { + "actions": "", + "createdAt": "2021-07-19 @ 06:38 PMtest_user", + "report": "[Flights] Global Flight Dashboarddashboard", + "status": "Completed at 2021-07-19 @ 06:39 PM", + }, + Object { + "actions": "", + "createdAt": "2021-07-19 @ 06:38 PMtest_user", + "report": "[Flights] Global Flight Dashboarddashboard", + "status": "Completed at 2021-07-19 @ 06:38 PM", + }, + ] + `); + }); }); }; diff --git a/x-pack/test/functional/apps/security/field_level_security.js b/x-pack/test/functional/apps/security/field_level_security.js index f58e6837b9441..1f8ecb0df202c 100644 --- a/x-pack/test/functional/apps/security/field_level_security.js +++ b/x-pack/test/functional/apps/security/field_level_security.js @@ -14,11 +14,14 @@ export default function ({ getService, getPageObjects }) { const retry = getService('retry'); const log = getService('log'); const PageObjects = getPageObjects(['security', 'settings', 'common', 'discover', 'header']); + const kibanaServer = getService('kibanaServer'); describe('field_level_security', () => { before('initialize tests', async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/security/flstest/data'); //( data) - await esArchiver.load('x-pack/test/functional/es_archives/security/flstest/kibana'); //(savedobject) + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/security/flstest/index_pattern' + ); await browser.setWindowSize(1600, 1000); }); @@ -125,6 +128,9 @@ export default function ({ getService, getPageObjects }) { after(async function () { await PageObjects.security.forceLogout(); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/security/flstest/index_pattern' + ); }); }); } diff --git a/x-pack/test/functional/apps/security/secure_roles_perm.js b/x-pack/test/functional/apps/security/secure_roles_perm.js index 027d7027d46ee..0c1dbfd5f826a 100644 --- a/x-pack/test/functional/apps/security/secure_roles_perm.js +++ b/x-pack/test/functional/apps/security/secure_roles_perm.js @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import { keyBy } from 'lodash'; + export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects([ 'security', @@ -29,7 +30,9 @@ export default function ({ getService, getPageObjects }) { log.debug('users'); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); log.debug('load kibana index with default index pattern'); - await esArchiver.load('x-pack/test/functional/es_archives/security/discover'); + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/security/discover' + ); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); await PageObjects.settings.navigateTo(); }); @@ -87,6 +90,9 @@ export default function ({ getService, getPageObjects }) { after(async function () { await PageObjects.security.forceLogout(); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/security/discover' + ); }); }); } diff --git a/x-pack/test/functional/apps/security/user_email.js b/x-pack/test/functional/apps/security/user_email.js index bb9b0a865ee6b..84566c1a6f5ff 100644 --- a/x-pack/test/functional/apps/security/user_email.js +++ b/x-pack/test/functional/apps/security/user_email.js @@ -7,14 +7,17 @@ import expect from '@kbn/expect'; import { keyBy } from 'lodash'; + export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['security', 'settings', 'common', 'accountSetting']); const log = getService('log'); - const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); describe('useremail', function () { before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/security/discover'); + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/security/discover' + ); await PageObjects.settings.navigateTo(); await PageObjects.security.clickElasticsearchUsers(); }); @@ -55,6 +58,9 @@ export default function ({ getService, getPageObjects }) { after(async function () { await PageObjects.security.forceLogout(); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/security/discover' + ); }); }); } diff --git a/x-pack/test/functional/apps/transform/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation_index_pattern.ts index d8867527ba2ac..a6b3e8e41be99 100644 --- a/x-pack/test/functional/apps/transform/creation_index_pattern.ts +++ b/x-pack/test/functional/apps/transform/creation_index_pattern.ts @@ -578,6 +578,7 @@ export default function ({ getService }: FtrProviderContext) { await transform.table.assertTransformRowFields(testData.transformId, { id: testData.transformId, description: testData.transformDescription, + type: testData.type, status: testData.expected.row.status, mode: testData.expected.row.mode, progress: testData.expected.row.progress, diff --git a/x-pack/test/functional/apps/transform/creation_saved_search.ts b/x-pack/test/functional/apps/transform/creation_saved_search.ts index e5944c7f12578..b79bef82267d7 100644 --- a/x-pack/test/functional/apps/transform/creation_saved_search.ts +++ b/x-pack/test/functional/apps/transform/creation_saved_search.ts @@ -279,6 +279,7 @@ export default function ({ getService }: FtrProviderContext) { await transform.table.assertTransformRowFields(testData.transformId, { id: testData.transformId, description: testData.transformDescription, + type: testData.type, status: testData.expected.row.status, mode: testData.expected.row.mode, progress: testData.expected.row.progress, diff --git a/x-pack/test/functional/apps/transform/deleting.ts b/x-pack/test/functional/apps/transform/deleting.ts index 68530c586b6e2..c86171cdb1d6f 100644 --- a/x-pack/test/functional/apps/transform/deleting.ts +++ b/x-pack/test/functional/apps/transform/deleting.ts @@ -24,6 +24,7 @@ export default function ({ getService }: FtrProviderContext) { expected: { row: { status: TRANSFORM_STATE.STOPPED, + type: 'pivot', mode: 'batch', progress: 100, }, @@ -35,6 +36,7 @@ export default function ({ getService }: FtrProviderContext) { expected: { row: { status: TRANSFORM_STATE.STOPPED, + type: 'pivot', mode: 'continuous', progress: undefined, }, @@ -50,6 +52,7 @@ export default function ({ getService }: FtrProviderContext) { messageText: 'updated transform.', row: { status: TRANSFORM_STATE.STOPPED, + type: 'latest', mode: 'batch', progress: 100, }, @@ -106,6 +109,7 @@ export default function ({ getService }: FtrProviderContext) { await transform.table.assertTransformRowFields(testData.originalConfig.id, { id: testData.originalConfig.id, description: testData.originalConfig.description, + type: testData.expected.row.type, status: testData.expected.row.status, mode: testData.expected.row.mode, progress: testData.expected.row.progress, diff --git a/x-pack/test/functional/apps/transform/editing.ts b/x-pack/test/functional/apps/transform/editing.ts index 01f93d9e8c0a8..993c239a04304 100644 --- a/x-pack/test/functional/apps/transform/editing.ts +++ b/x-pack/test/functional/apps/transform/editing.ts @@ -70,6 +70,7 @@ export default function ({ getService }: FtrProviderContext) { messageText: 'updated transform.', row: { status: TRANSFORM_STATE.STOPPED, + type: 'pivot', mode: 'batch', progress: '100', }, @@ -85,6 +86,7 @@ export default function ({ getService }: FtrProviderContext) { messageText: 'updated transform.', row: { status: TRANSFORM_STATE.STOPPED, + type: 'latest', mode: 'batch', progress: '100', }, @@ -170,6 +172,7 @@ export default function ({ getService }: FtrProviderContext) { await transform.table.assertTransformRowFields(testData.originalConfig.id, { id: testData.originalConfig.id, description: testData.transformDescription, + type: testData.expected.row.type, status: testData.expected.row.status, mode: testData.expected.row.mode, progress: testData.expected.row.progress, diff --git a/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts b/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts index 3bced4fca9b40..d50943fad991a 100644 --- a/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts +++ b/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts @@ -13,8 +13,7 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const transform = getService('transform'); - // FLAKY: https://github.com/elastic/kibana/issues/107043 - describe.skip('for user with full transform access', function () { + describe('for user with full transform access', function () { describe('with no data loaded', function () { before(async () => { await transform.securityUI.loginAsTransformPowerUser(); diff --git a/x-pack/test/functional/apps/transform/permissions/read_transform_access.ts b/x-pack/test/functional/apps/transform/permissions/read_transform_access.ts index 5634ed4736e4d..6a04d33ff152d 100644 --- a/x-pack/test/functional/apps/transform/permissions/read_transform_access.ts +++ b/x-pack/test/functional/apps/transform/permissions/read_transform_access.ts @@ -13,8 +13,7 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const transform = getService('transform'); - // FLAKY: https://github.com/elastic/kibana/issues/107043 - describe.skip('for user with full transform access', function () { + describe('for user with read only transform access', function () { describe('with no data loaded', function () { before(async () => { await transform.securityUI.loginAsTransformViewer(); diff --git a/x-pack/test/functional/es_archives/reporting/archived_reports/data.json.gz b/x-pack/test/functional/es_archives/reporting/archived_reports/data.json.gz index 34a30bd84a592..22423aa1fc99f 100644 Binary files a/x-pack/test/functional/es_archives/reporting/archived_reports/data.json.gz and b/x-pack/test/functional/es_archives/reporting/archived_reports/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/reporting/archived_reports/mappings.json b/x-pack/test/functional/es_archives/reporting/archived_reports/mappings.json index 3c3225a70d47f..b2363174e92ec 100644 --- a/x-pack/test/functional/es_archives/reporting/archived_reports/mappings.json +++ b/x-pack/test/functional/es_archives/reporting/archived_reports/mappings.json @@ -4,6 +4,561 @@ "aliases": { }, "index": ".reporting-2020.04.19", + "mappings": { + "properties": { + "attempts": { + "type": "long" + }, + "browser_type": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "completed_at": { + "type": "date" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "jobtype": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "kibana_id": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "kibana_name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "max_attempts": { + "type": "long" + }, + "meta": { + "properties": { + "layout": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "objectType": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "output": { + "properties": { + "content": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "content_type": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "csv_contains_formulas": { + "type": "boolean" + }, + "max_size_reached": { + "type": "boolean" + }, + "size": { + "type": "long" + } + } + }, + "payload": { + "properties": { + "basePath": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "browserTimezone": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "fields": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "forceNow": { + "type": "date" + }, + "headers": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "indexPatternId": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "indexPatternSavedObject": { + "properties": { + "attributes": { + "properties": { + "fieldFormatMap": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "fields": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "timeFieldName": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "title": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "id": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "migrationVersion": { + "properties": { + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "type": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "updated_at": { + "type": "date" + }, + "version": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "layout": { + "properties": { + "dimensions": { + "properties": { + "height": { + "type": "long" + }, + "width": { + "type": "long" + } + } + }, + "id": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "metaFields": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "objectType": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "relativeUrl": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "relativeUrls": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "searchRequest": { + "properties": { + "body": { + "properties": { + "_source": { + "type": "object" + }, + "docvalue_fields": { + "properties": { + "field": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "format": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "query": { + "properties": { + "bool": { + "properties": { + "filter": { + "properties": { + "match_all": { + "type": "object" + }, + "range": { + "properties": { + "order_date": { + "properties": { + "format": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "gte": { + "type": "date" + }, + "lte": { + "type": "date" + } + } + }, + "timestamp": { + "properties": { + "format": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "gte": { + "type": "date" + }, + "lte": { + "type": "date" + } + } + } + } + } + } + } + } + } + } + }, + "script_fields": { + "properties": { + "hour_of_day": { + "properties": { + "script": { + "properties": { + "lang": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "source": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + } + } + } + } + }, + "sort": { + "properties": { + "order_date": { + "properties": { + "order": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "unmapped_type": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "timestamp": { + "properties": { + "order": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "unmapped_type": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + } + } + }, + "stored_fields": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "version": { + "type": "boolean" + } + } + }, + "index": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "title": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "type": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "priority": { + "type": "long" + }, + "process_expiration": { + "type": "date" + }, + "started_at": { + "type": "date" + }, + "status": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "timeout": { + "type": "long" + } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + }, + "index": ".reporting-2021-07-18", "mappings": { "properties": { "attempts": { @@ -55,6 +610,9 @@ } } }, + "migration_version": { + "type": "keyword" + }, "output": { "properties": { "content": { @@ -72,6 +630,15 @@ }, "size": { "type": "long" + }, + "warnings": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" } } }, @@ -104,4 +671,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/security/discover/data.json.gz b/x-pack/test/functional/es_archives/security/discover/data.json.gz deleted file mode 100644 index cce1558566be5..0000000000000 Binary files a/x-pack/test/functional/es_archives/security/discover/data.json.gz and /dev/null differ diff --git a/x-pack/test/functional/es_archives/security/discover/mappings.json b/x-pack/test/functional/es_archives/security/discover/mappings.json deleted file mode 100644 index 0f58add932b0c..0000000000000 --- a/x-pack/test/functional/es_archives/security/discover/mappings.json +++ /dev/null @@ -1,308 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - ".kibana": {} - }, - "index": ".kibana_1", - "mappings": { - "properties": { - "config": { - "dynamic": "true", - "properties": { - "buildNum": { - "type": "keyword" - } - } - }, - "dashboard": { - "dynamic": "strict", - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "graph-workspace": { - "dynamic": "strict", - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "dynamic": "strict", - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - } - } - }, - "search": { - "dynamic": "strict", - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "dynamic": "strict", - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "initials": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "spaceId": { - "type": "keyword" - }, - "timelion-sheet": { - "dynamic": "strict", - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "timelion_chart_height": { - "type": "integer" - }, - "timelion_columns": { - "type": "integer" - }, - "timelion_interval": { - "type": "keyword" - }, - "timelion_other_interval": { - "type": "keyword" - }, - "timelion_rows": { - "type": "integer" - }, - "timelion_sheet": { - "type": "text" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "type": { - "type": "keyword" - }, - "url": { - "dynamic": "strict", - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "visualization": { - "dynamic": "strict", - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchId": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - }, - "settings": { - "index": { - "number_of_replicas": "1", - "number_of_shards": "1" - } - } - } -} diff --git a/x-pack/test/functional/es_archives/security/flstest/kibana/data.json b/x-pack/test/functional/es_archives/security/flstest/kibana/data.json deleted file mode 100644 index ad52bd61c2ea5..0000000000000 --- a/x-pack/test/functional/es_archives/security/flstest/kibana/data.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "doc", - "value": { - "id": "index-pattern:845f1970-6853-11e9-a554-c17eedfbe970", - "index": ".kibana", - "source": { - "index-pattern" : { - "title" : "flstest", - "fields" : "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"customer_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"customer_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"customer_name\"}}},{\"name\":\"customer_region\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"customer_region.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"customer_region\"}}},{\"name\":\"customer_ssn\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"customer_ssn.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"customer_ssn\"}}}]" - }, - "type" : "index-pattern", - "references" : [ ], - "migrationVersion" : { - "index-pattern" : "6.5.0" - }, - "updated_at" : "2019-04-26T18:45:54.517Z" - } - } -} diff --git a/x-pack/test/functional/es_archives/security/flstest/kibana/mappings.json b/x-pack/test/functional/es_archives/security/flstest/kibana/mappings.json deleted file mode 100644 index ca97d0505f5b1..0000000000000 --- a/x-pack/test/functional/es_archives/security/flstest/kibana/mappings.json +++ /dev/null @@ -1,413 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - ".kibana": {} - }, - "index": ".kibana_1", - "mappings": { - "dynamic": "strict", - "properties": { - "canvas-workpad": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "id": { - "index": false, - "type": "text" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "config": { - "dynamic": "true", - "properties": { - "buildNum": { - "type": "keyword" - }, - "defaultIndex": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "typeMeta": { - "type": "keyword" - } - } - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "map": { - "properties": { - "bounds": { - "dynamic": false, - "properties": {} - }, - "description": { - "type": "text" - }, - "layerListJSON": { - "type": "text" - }, - "mapStateJSON": { - "type": "text" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "index-pattern": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "namespace": { - "type": "keyword" - }, - "references": { - "type": "nested", - "properties": { - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "id": { - "type": "keyword" - } - } - }, - "search": { - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "initials": { - "type": "keyword" - }, - "disabledFeatures": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "telemetry": { - "properties": { - "enabled": { - "type": "boolean" - } - } - }, - "timelion-sheet": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "timelion_chart_height": { - "type": "integer" - }, - "timelion_columns": { - "type": "integer" - }, - "timelion_interval": { - "type": "keyword" - }, - "timelion_other_interval": { - "type": "keyword" - }, - "timelion_rows": { - "type": "integer" - }, - "timelion_sheet": { - "type": "text" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchId": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - }, - "settings": { - "index": { - "number_of_replicas": "0", - "number_of_shards": "1" - } - } - } -} diff --git a/x-pack/test/functional/fixtures/kbn_archiver/security/discover.json b/x-pack/test/functional/fixtures/kbn_archiver/security/discover.json new file mode 100644 index 0000000000000..a7fc27c8dd2e8 --- /dev/null +++ b/x-pack/test/functional/fixtures/kbn_archiver/security/discover.json @@ -0,0 +1,50 @@ +{ + "attributes": { + "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "coreMigrationVersion": "7.15.0", + "id": "logstash-*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzQsMl0=" +} + +{ + "attributes": { + "columns": [ + "_source" + ], + "description": "A Saved Search Description", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"highlightAll\":true,\"filter\":[],\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "sort": [ + [ + "@timestamp", + "desc" + ] + ], + "title": "A Saved Search", + "version": 1 + }, + "coreMigrationVersion": "7.15.0", + "id": "ab12e3c0-f231-11e6-9486-733b1ac9221a", + "migrationVersion": { + "search": "7.9.3" + }, + "references": [ + { + "id": "logstash-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "search", + "version": "WzUsMl0=" +} \ No newline at end of file diff --git a/x-pack/test/functional/fixtures/kbn_archiver/security/flstest/index_pattern.json b/x-pack/test/functional/fixtures/kbn_archiver/security/flstest/index_pattern.json new file mode 100644 index 0000000000000..4715eec9ea7cc --- /dev/null +++ b/x-pack/test/functional/fixtures/kbn_archiver/security/flstest/index_pattern.json @@ -0,0 +1,15 @@ +{ + "attributes": { + "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"customer_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"customer_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"customer_name\"}}},{\"name\":\"customer_region\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"customer_region.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"customer_region\"}}},{\"name\":\"customer_ssn\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"customer_ssn.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"customer_ssn\"}}}]", + "title": "flstest" + }, + "coreMigrationVersion": "7.15.0", + "id": "845f1970-6853-11e9-a554-c17eedfbe970", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2019-04-26T18:45:54.517Z", + "version": "WzEsMl0=" +} \ No newline at end of file diff --git a/x-pack/test/functional/page_objects/index_lifecycle_management_page.ts b/x-pack/test/functional/page_objects/index_lifecycle_management_page.ts index 1d12da202e051..6706579d5c399 100644 --- a/x-pack/test/functional/page_objects/index_lifecycle_management_page.ts +++ b/x-pack/test/functional/page_objects/index_lifecycle_management_page.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { map as mapAsync } from 'bluebird'; import { FtrProviderContext } from '../ftr_provider_context'; interface Policy { @@ -83,29 +82,8 @@ export function IndexLifecycleManagementPageProvider({ getService }: FtrProvider await testSubjects.click(`tablePagination-100-rows`); }, - async getPolicyList() { - const policies = await testSubjects.findAll('policyTableRow'); - return mapAsync(policies, async (policy) => { - const policyNameElement = await policy.findByTestSubject('policyTableCell-name'); - const policyLinkedIndicesElement = await policy.findByTestSubject( - 'policyTableCell-indices' - ); - const policyVersionElement = await policy.findByTestSubject('policyTableCell-version'); - const policyModifiedDateElement = await policy.findByTestSubject( - 'policyTableCell-modifiedDate' - ); - const policyActionsButtonElement = await policy.findByTestSubject( - 'policyActionsContextMenuButton' - ); - - return { - name: await policyNameElement.getVisibleText(), - indices: await policyLinkedIndicesElement.getVisibleText(), - version: await policyVersionElement.getVisibleText(), - modifiedDate: await policyModifiedDateElement.getVisibleText(), - actionsButton: policyActionsButtonElement, - }; - }); + async getPolicyRow(name: string) { + return await testSubjects.findAll(`policyTableRow-${name}`); }, }; } diff --git a/x-pack/test/functional/page_objects/monitoring_page.ts b/x-pack/test/functional/page_objects/monitoring_page.ts index acd9a443eb7ce..259af2c917f02 100644 --- a/x-pack/test/functional/page_objects/monitoring_page.ts +++ b/x-pack/test/functional/page_objects/monitoring_page.ts @@ -17,7 +17,7 @@ export class MonitoringPageObject extends FtrService { } async closeAlertsModal() { - return this.testSubjects.click('alerts-modal-button'); + return this.testSubjects.click('alerts-modal-remind-later-button'); } async clickBreadcrumb(subj: string) { diff --git a/x-pack/test/functional/page_objects/reporting_page.ts b/x-pack/test/functional/page_objects/reporting_page.ts index 742d41031004b..302e71304869b 100644 --- a/x-pack/test/functional/page_objects/reporting_page.ts +++ b/x-pack/test/functional/page_objects/reporting_page.ts @@ -10,6 +10,7 @@ import { format as formatUrl } from 'url'; import supertestAsPromised from 'supertest-as-promised'; import { FtrService } from '../ftr_provider_context'; +import { REPORT_TABLE_ID, REPORT_TABLE_ROW_ID } from '../../../plugins/reporting/common/constants'; export class ReportingPageObject extends FtrService { private readonly browser = this.ctx.getService('browser'); @@ -157,4 +158,21 @@ export class ReportingPageObject extends FtrService { const toTime = 'Sep 23, 1999 @ 18:31:44.000'; await this.timePicker.setAbsoluteRange(fromTime, toTime); } + + async getManagementList() { + const table = await this.testSubjects.find(REPORT_TABLE_ID); + const allRows = await table.findAllByTestSubject(REPORT_TABLE_ROW_ID); + + return await Promise.all( + allRows.map(async (row) => { + const $ = await row.parseDomContent(); + return { + report: $.findTestSubject('reportingListItemObjectTitle').text().trim(), + createdAt: $.findTestSubject('reportJobCreatedAt').text().trim(), + status: $.findTestSubject('reportJobStatus').text().trim(), + actions: $.findTestSubject('reportJobActions').text().trim(), + }; + }) + ); + } } diff --git a/x-pack/test/functional/page_objects/security_page.ts b/x-pack/test/functional/page_objects/security_page.ts index 0c9fcf1ca76d6..37fe0eccea31a 100644 --- a/x-pack/test/functional/page_objects/security_page.ts +++ b/x-pack/test/functional/page_objects/security_page.ts @@ -218,7 +218,7 @@ export class SecurityPageObject extends FtrService { } if (expectedResult === 'chrome') { - await this.find.byCssSelector('[data-test-subj="userMenuButton"]', 20000); + await this.find.byCssSelector('[data-test-subj="userMenuAvatar"]', 20000); this.log.debug(`Finished login process currentUrl = ${await this.browser.getCurrentUrl()}`); } diff --git a/x-pack/test/functional/page_objects/synthetics_integration_page.ts b/x-pack/test/functional/page_objects/synthetics_integration_page.ts index 81ddaf06febd9..daebb1d2a2f99 100644 --- a/x-pack/test/functional/page_objects/synthetics_integration_page.ts +++ b/x-pack/test/functional/page_objects/synthetics_integration_page.ts @@ -178,7 +178,7 @@ export function SyntheticsIntegrationPageProvider({ */ async configureHeaders(testSubj: string, headers: Record) { const headersContainer = await testSubjects.find(testSubj); - const addHeaderButton = await headersContainer.findByCssSelector('button'); + const addHeaderButton = await testSubjects.find(`${testSubj}__button`); const keys = Object.keys(headers); await Promise.all( diff --git a/x-pack/test/functional/services/monitoring/cluster_list.js b/x-pack/test/functional/services/monitoring/cluster_list.js index f63e7b6cd125e..bcf0e18ef4dd7 100644 --- a/x-pack/test/functional/services/monitoring/cluster_list.js +++ b/x-pack/test/functional/services/monitoring/cluster_list.js @@ -15,7 +15,7 @@ export function MonitoringClusterListProvider({ getService, getPageObjects }) { const SUBJ_SEARCH_BAR = `${SUBJ_TABLE_CONTAINER} > monitoringTableToolBar`; const SUBJ_CLUSTER_ROW_PREFIX = `${SUBJ_TABLE_CONTAINER} > clusterRow_`; - const ALERTS_MODAL_BUTTON = 'alerts-modal-button'; + const ALERTS_MODAL_BUTTON = 'alerts-modal-remind-later-button'; return new (class ClusterList { async assertDefaults() { diff --git a/x-pack/test/functional/services/monitoring/cluster_overview.js b/x-pack/test/functional/services/monitoring/cluster_overview.js index 5128cbffd34cf..7e888d6233ff7 100644 --- a/x-pack/test/functional/services/monitoring/cluster_overview.js +++ b/x-pack/test/functional/services/monitoring/cluster_overview.js @@ -76,6 +76,10 @@ export function MonitoringClusterOverviewProvider({ getService }) { } closeAlertsModal() { + return testSubjects.click('alerts-modal-remind-later-button'); + } + + acceptAlertsModal() { return testSubjects.click('alerts-modal-button'); } diff --git a/x-pack/test/functional/services/transform/transform_table.ts b/x-pack/test/functional/services/transform/transform_table.ts index 7c4a45fb601ea..9e3ffcdfd8095 100644 --- a/x-pack/test/functional/services/transform/transform_table.ts +++ b/x-pack/test/functional/services/transform/transform_table.ts @@ -37,6 +37,11 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { .find('.euiTableCellContent') .text() .trim(), + type: $tr + .findTestSubject('transformListColumnType') + .find('.euiTableCellContent') + .text() + .trim(), status: $tr .findTestSubject('transformListColumnStatus') .find('.euiTableCellContent') @@ -190,38 +195,54 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { }); } - public async assertTransformExpandedRow() { - await testSubjects.click('transformListRowDetailsToggle'); + public async ensureDetailsOpen() { + await retry.tryForTime(30 * 1000, async () => { + if (!(await testSubjects.exists('transformExpandedRowTabbedContent'))) { + await testSubjects.click('transformListRowDetailsToggle'); + await testSubjects.existOrFail('transformExpandedRowTabbedContent', { timeout: 1000 }); + } + }); + } - // The expanded row should show the details tab content by default - await testSubjects.existOrFail('transformDetailsTab'); - await testSubjects.existOrFail('~transformDetailsTabContent'); + public async ensureDetailsClosed() { + await retry.tryForTime(30 * 1000, async () => { + if (await testSubjects.exists('transformExpandedRowTabbedContent')) { + await testSubjects.click('transformListRowDetailsToggle'); + await testSubjects.missingOrFail('transformExpandedRowTabbedContent', { timeout: 1000 }); + } + }); + } - // Walk through the rest of the tabs and check if the corresponding content shows up - await testSubjects.existOrFail('transformJsonTab'); - await testSubjects.click('transformJsonTab'); - await testSubjects.existOrFail('~transformJsonTabContent'); + public async switchToExpandedRowTab(tabSubject: string, contentSubject: string) { + await retry.tryForTime(30 * 1000, async () => { + await testSubjects.click(tabSubject); + await testSubjects.existOrFail(contentSubject, { timeout: 1000 }); + }); + } - await testSubjects.existOrFail('transformMessagesTab'); - await testSubjects.click('transformMessagesTab'); - await testSubjects.existOrFail('~transformMessagesTabContent'); + public async assertTransformExpandedRow() { + await this.ensureDetailsOpen(); + await retry.tryForTime(30 * 1000, async () => { + // The expanded row should show the details tab content by default + await testSubjects.existOrFail('transformDetailsTab', { timeout: 1000 }); + await testSubjects.existOrFail('~transformDetailsTabContent', { timeout: 1000 }); + }); - await testSubjects.existOrFail('transformPreviewTab'); - await testSubjects.click('transformPreviewTab'); - await testSubjects.existOrFail('~transformPivotPreview'); + // Walk through the rest of the tabs and check if the corresponding content shows up + await this.switchToExpandedRowTab('transformJsonTab', '~transformJsonTabContent'); + await this.switchToExpandedRowTab('transformMessagesTab', '~transformMessagesTabContent'); + await this.switchToExpandedRowTab('transformPreviewTab', '~transformPivotPreview'); } public async assertTransformExpandedRowMessages(expectedText: string) { - await testSubjects.click('transformListRowDetailsToggle'); + await this.ensureDetailsOpen(); // The expanded row should show the details tab content by default await testSubjects.existOrFail('transformDetailsTab'); await testSubjects.existOrFail('~transformDetailsTabContent'); // Click on the messages tab and assert the messages - await testSubjects.existOrFail('transformMessagesTab'); - await testSubjects.click('transformMessagesTab'); - await testSubjects.existOrFail('~transformMessagesTabContent'); + await this.switchToExpandedRowTab('transformMessagesTab', '~transformMessagesTabContent'); await retry.tryForTime(30 * 1000, async () => { const actualText = await testSubjects.getVisibleText('~transformMessagesTabContent'); expect(actualText.toLowerCase()).to.contain( diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/security_roles_privileges.ts b/x-pack/test/reporting_api_integration/reporting_and_security/security_roles_privileges.ts index 4dbf1b6fa5ebb..f260ada7fe15d 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/security_roles_privileges.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/security_roles_privileges.ts @@ -35,6 +35,7 @@ export default function ({ getService }: FtrProviderContext) { }, browserTimezone: 'UTC', title: 'testfooyu78yt90-', + version: '7.13.0', } as any )) as supertest.Response; expect(res.status).to.eql(403); @@ -52,6 +53,7 @@ export default function ({ getService }: FtrProviderContext) { }, browserTimezone: 'UTC', title: 'testfooyu78yt90-', + version: '7.13.0', } as any )) as supertest.Response; expect(res.status).to.eql(200); @@ -69,6 +71,7 @@ export default function ({ getService }: FtrProviderContext) { layout: { id: 'preserve' }, relativeUrls: ['/fooyou'], objectType: 'dashboard', + version: '7.14.0', } ); expect(res.status).to.eql(403); @@ -84,6 +87,7 @@ export default function ({ getService }: FtrProviderContext) { layout: { id: 'preserve' }, relativeUrls: ['/fooyou'], objectType: 'dashboard', + version: '7.14.0', } ); expect(res.status).to.eql(200); @@ -101,6 +105,7 @@ export default function ({ getService }: FtrProviderContext) { layout: { id: 'preserve' }, relativeUrls: ['/fooyou'], objectType: 'visualization', + version: '7.14.0', } ); expect(res.status).to.eql(403); @@ -116,6 +121,7 @@ export default function ({ getService }: FtrProviderContext) { layout: { id: 'preserve' }, relativeUrls: ['/fooyou'], objectType: 'visualization', + version: '7.14.0', } ); expect(res.status).to.eql(200); @@ -133,6 +139,7 @@ export default function ({ getService }: FtrProviderContext) { layout: { id: 'preserve' }, relativeUrls: ['/fooyou'], objectType: 'canvas', + version: '7.14.0', } ); expect(res.status).to.eql(403); @@ -148,6 +155,7 @@ export default function ({ getService }: FtrProviderContext) { layout: { id: 'preserve' }, relativeUrls: ['/fooyou'], objectType: 'canvas', + version: '7.14.0', } ); expect(res.status).to.eql(200); @@ -164,6 +172,7 @@ export default function ({ getService }: FtrProviderContext) { searchSource: {}, objectType: 'search', title: 'test disallowed', + version: '7.14.0', } ); expect(res.status).to.eql(403); @@ -183,6 +192,7 @@ export default function ({ getService }: FtrProviderContext) { index: '5193f870-d861-11e9-a311-0fa548c5f953', } as any, columns: [], + version: '7.13.0', } ); expect(res.status).to.eql(200); diff --git a/x-pack/test/stack_functional_integration/apps/monitoring/_monitoring_metricbeat.js b/x-pack/test/stack_functional_integration/apps/monitoring/_monitoring_metricbeat.js index 9dcc18b3c3f20..79b3b98aafddd 100644 --- a/x-pack/test/stack_functional_integration/apps/monitoring/_monitoring_metricbeat.js +++ b/x-pack/test/stack_functional_integration/apps/monitoring/_monitoring_metricbeat.js @@ -26,7 +26,7 @@ export default ({ getService, getPageObjects }) => { } // navigateToApp without a username and password will default to the superuser await PageObjects.common.navigateToApp('monitoring', { insertTimestamp: false }); - await clusterOverview.closeAlertsModal(); + await clusterOverview.acceptAlertsModal(); }); it('should have Monitoring already enabled', async () => { diff --git a/yarn.lock b/yarn.lock index f0e4fc1df484c..60aa5ebbd48cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1389,10 +1389,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@33.0.0": - version "33.0.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-33.0.0.tgz#45428b792300e363ecd3454465be49d42788a1fd" - integrity sha512-HUt0oBaO/PSRZcYHmdLL1lzFWMBtP/9umGIAQgW785qv+y/Hv2cjjfYNckGfnr5RUDtx0KCc3v2UBkRq6n93EA== +"@elastic/charts@33.1.0": + version "33.1.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-33.1.0.tgz#3d32a0cf2d07a8df381a8a218104f0f554d392f0" + integrity sha512-m/Qvs2xixzkYa7LeNCKajHjfRIkT2ZTlEQ+Sw52eRt0enCXjuS8Pp6PvLz0a9Ye37aLCZhEJMAS1EGJxHsX/pQ== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" @@ -2798,6 +2798,10 @@ version "0.0.0" uid "" +"@kbn/field-types@link:bazel-bin/packages/kbn-field-types": + version "0.0.0" + uid "" + "@kbn/i18n@link:bazel-bin/packages/kbn-i18n": version "0.0.0" uid ""