diff --git a/docs/getting-started/quick-start-guide.asciidoc b/docs/getting-started/quick-start-guide.asciidoc index d614aece5b425..2bddd9bf61452 100644 --- a/docs/getting-started/quick-start-guide.asciidoc +++ b/docs/getting-started/quick-start-guide.asciidoc @@ -138,7 +138,7 @@ image::images/dashboard_sampleDataAddFilter_7.15.0.png[The [eCommerce] Revenue D [[quick-start-whats-next]] == What's next? -*Add your own data.* Ready to add your own data? Go to {fleet-guide}/fleet-quick-start.html[Quick start: Get logs and metrics into the Elastic Stack] to learn how to ingest your data, or go to <> and learn about all the other ways you can add data. +*Add your own data.* Ready to add your own data? Go to {observability-guide}/ingest-logs-metrics-uptime.html[Ingest logs, metrics, and uptime data with {agent}], or go to <> and learn about all the other ways you can add data. *Explore your own data in Discover.* Ready to learn more about exploring your data in *Discover*? Go to <>. diff --git a/docs/osquery/osquery.asciidoc b/docs/osquery/osquery.asciidoc index 1e4e6604a7c70..a4f3c80463143 100644 --- a/docs/osquery/osquery.asciidoc +++ b/docs/osquery/osquery.asciidoc @@ -365,7 +365,7 @@ The following is an example of an **error response** for an undefined action que == System requirements * {fleet-guide}/fleet-overview.html[Fleet] is enabled on your cluster, and -one or more {fleet-guide}/elastic-agent-installation-configuration.html[Elastic Agents] is enrolled. +one or more {fleet-guide}/elastic-agent-installation.html[Elastic Agents] is enrolled. * The https://docs.elastic.co/en/integrations/osquery_manager[*Osquery Manager*] integration has been added and configured for an agent policy through Fleet. diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index c7f745648cbe9..ad38ac1710fd5 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -47,7 +47,7 @@ so you can quickly get insights into your data, and {fleet} mode offers several image::images/addData_fleet_7.15.0.png[Add data using Fleet] To get started, refer to -{fleet-guide}/fleet-quick-start.html[Quick start: Get logs and metrics into the Elastic Stack]. +{observability-guide}/ingest-logs-metrics-uptime.html[Ingest logs, metrics, and uptime data with {agent}]. [discrete] [[upload-data-kibana]] diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 16fa8eb734204..4802a4da8182c 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -275,7 +275,7 @@ that the {kib} server uses to perform maintenance on the {kib} index at startup. is an alternative to `elasticsearch.username` and `elasticsearch.password`. | `enterpriseSearch.host` - | The URL of your Enterprise Search instance + | The http(s) URL of your Enterprise Search instance. For example, in a local self-managed setup, set this to `http://localhost:3002`. Authentication between Kibana and the Enterprise Search host URL, such as via OAuth, is not supported. You can also {enterprise-search-ref}/configure-ssl-tls.html#configure-ssl-tls-in-kibana[configure Kibana to trust your Enterprise Search TLS certificate authority]. | `interpreter.enableInVisualize` | Enables use of interpreter in Visualize. *Default: `true`* diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index ad590865b9e14..ccc0e17b655b1 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -362,7 +362,7 @@ export function CollapsibleNav({ iconType="plusInCircleFilled" > {i18n.translate('core.ui.primaryNav.addData', { - defaultMessage: 'Add data', + defaultMessage: 'Add integrations', })} diff --git a/src/plugins/custom_integrations/common/index.ts b/src/plugins/custom_integrations/common/index.ts index 944ac6ba3e6ee..98148bb22c816 100755 --- a/src/plugins/custom_integrations/common/index.ts +++ b/src/plugins/custom_integrations/common/index.ts @@ -49,6 +49,7 @@ export const INTEGRATION_CATEGORY_DISPLAY = { project_management: 'Project Management', software_development: 'Software Development', upload_file: 'Upload a file', + website_search: 'Website Search', }; /** diff --git a/src/plugins/home/public/application/components/add_data/__snapshots__/add_data.test.tsx.snap b/src/plugins/home/public/application/components/add_data/__snapshots__/add_data.test.tsx.snap index 26b5697f008b6..de6beab31247a 100644 --- a/src/plugins/home/public/application/components/add_data/__snapshots__/add_data.test.tsx.snap +++ b/src/plugins/home/public/application/components/add_data/__snapshots__/add_data.test.tsx.snap @@ -17,7 +17,7 @@ exports[`AddData render 1`] = ` id="homDataAdd__title" > @@ -43,17 +43,25 @@ exports[`AddData render 1`] = ` grow={false} > diff --git a/src/plugins/home/public/application/components/add_data/add_data.test.tsx b/src/plugins/home/public/application/components/add_data/add_data.test.tsx index 4018ae67c19ee..3aa51f89c7d67 100644 --- a/src/plugins/home/public/application/components/add_data/add_data.test.tsx +++ b/src/plugins/home/public/application/components/add_data/add_data.test.tsx @@ -27,7 +27,9 @@ beforeEach(() => { jest.clearAllMocks(); }); -const applicationStartMock = {} as unknown as ApplicationStart; +const applicationStartMock = { + capabilities: { navLinks: { integrations: true } }, +} as unknown as ApplicationStart; const addBasePathMock = jest.fn((path: string) => (path ? path : 'path')); diff --git a/src/plugins/home/public/application/components/add_data/add_data.tsx b/src/plugins/home/public/application/components/add_data/add_data.tsx index 97ba28a04a07e..50d6079dd8df3 100644 --- a/src/plugins/home/public/application/components/add_data/add_data.tsx +++ b/src/plugins/home/public/application/components/add_data/add_data.tsx @@ -22,8 +22,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { METRIC_TYPE } from '@kbn/analytics'; import { ApplicationStart } from 'kibana/public'; import { createAppNavigationHandler } from '../app_navigation_handler'; -// @ts-expect-error untyped component -import { Synopsis } from '../synopsis'; import { getServices } from '../../kibana_services'; import { RedirectAppLinks } from '../../../../../kibana_react/public'; @@ -35,87 +33,91 @@ interface Props { export const AddData: FC = ({ addBasePath, application, isDarkMode }) => { const { trackUiMetric } = getServices(); + const canAccessIntegrations = application.capabilities.navLinks.integrations; + if (canAccessIntegrations) { + return ( + <> +
+ + + +

+ +

+
- return ( - <> -
- - - -

- -

-
+ - + +

+ +

+
- -

- -

-
+ - + + + + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + { + trackUiMetric(METRIC_TYPE.CLICK, 'home_tutorial_directory'); + createAppNavigationHandler('/app/integrations/browse')(event); + }} + > + + + + - - - - {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} - { - trackUiMetric(METRIC_TYPE.CLICK, 'home_tutorial_directory'); - createAppNavigationHandler('/app/home#/tutorial_directory')(event); - }} + + - - - - - - - - - - -
+ + +
+ - - - - -
+ + + +
+
- - - ); + + + ); + } else { + return null; + } }; diff --git a/src/plugins/home/public/application/components/sample_data/index.tsx b/src/plugins/home/public/application/components/sample_data/index.tsx index d6b9328f57e9b..b65fbb5d002b0 100644 --- a/src/plugins/home/public/application/components/sample_data/index.tsx +++ b/src/plugins/home/public/application/components/sample_data/index.tsx @@ -40,7 +40,7 @@ export function SampleDataCard({ urlBasePath, onDecline, onConfirm }: Props) { image={cardGraphicURL} textAlign="left" title={ - + } description={ - + { - const notices = getServices().tutorialService.getDirectoryNotices(); - return notices.length ? ( - - {notices.map((DirectoryNotice, index) => ( - - - - ))} - - ) : null; - }; - renderHeaderLinks = () => { const headerLinks = getServices().tutorialService.getDirectoryHeaderLinks(); return headerLinks.length ? ( @@ -245,7 +203,6 @@ class TutorialDirectoryUi extends React.Component { render() { const headerLinks = this.renderHeaderLinks(); const tabs = this.getTabs(); - const notices = this.renderNotices(); return ( + ), tabs, rightSideItems: headerLinks ? [headerLinks] : [], }} > - {notices && ( - <> - {notices} - - - )} {this.renderTabContent()} ); diff --git a/src/plugins/home/public/application/components/welcome.tsx b/src/plugins/home/public/application/components/welcome.tsx index ca7e6874c75c2..03dff22c7b33f 100644 --- a/src/plugins/home/public/application/components/welcome.tsx +++ b/src/plugins/home/public/application/components/welcome.tsx @@ -48,8 +48,7 @@ export class Welcome extends React.Component { }; private redirecToAddData() { - const path = this.services.addBasePath('#/tutorial_directory'); - window.location.href = path; + this.services.application.navigateToApp('integrations', { path: '/browse' }); } private onSampleDataDecline = () => { diff --git a/src/plugins/home/public/index.ts b/src/plugins/home/public/index.ts index dd02bf65dd8b0..7abaf5d19f008 100644 --- a/src/plugins/home/public/index.ts +++ b/src/plugins/home/public/index.ts @@ -23,7 +23,6 @@ export type { FeatureCatalogueSolution, Environment, TutorialVariables, - TutorialDirectoryNoticeComponent, TutorialDirectoryHeaderLinkComponent, TutorialModuleNoticeComponent, } from './services'; diff --git a/src/plugins/home/public/services/index.ts b/src/plugins/home/public/services/index.ts index 65913df6310b1..2ee68a9eef0c2 100644 --- a/src/plugins/home/public/services/index.ts +++ b/src/plugins/home/public/services/index.ts @@ -22,7 +22,6 @@ export { TutorialService } from './tutorials'; export type { TutorialVariables, TutorialServiceSetup, - TutorialDirectoryNoticeComponent, TutorialDirectoryHeaderLinkComponent, TutorialModuleNoticeComponent, } from './tutorials'; diff --git a/src/plugins/home/public/services/tutorials/index.ts b/src/plugins/home/public/services/tutorials/index.ts index 8de12c31249d8..e007a5ea4d552 100644 --- a/src/plugins/home/public/services/tutorials/index.ts +++ b/src/plugins/home/public/services/tutorials/index.ts @@ -11,7 +11,6 @@ export { TutorialService } from './tutorial_service'; export type { TutorialVariables, TutorialServiceSetup, - TutorialDirectoryNoticeComponent, TutorialDirectoryHeaderLinkComponent, TutorialModuleNoticeComponent, } from './tutorial_service'; diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts b/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts index 0c109d61912ca..ab38a32a1a5b3 100644 --- a/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts +++ b/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts @@ -25,7 +25,6 @@ const createMock = (): jest.Mocked> => { const service = { setup: jest.fn(), getVariables: jest.fn(() => ({})), - getDirectoryNotices: jest.fn(() => []), getDirectoryHeaderLinks: jest.fn(() => []), getModuleNotices: jest.fn(() => []), getCustomStatusCheck: jest.fn(), diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx b/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx index a88cf526e3716..b90165aafb45f 100644 --- a/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx +++ b/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx @@ -27,22 +27,6 @@ describe('TutorialService', () => { }).toThrow(); }); - test('allows multiple register directory notice calls', () => { - const setup = new TutorialService().setup(); - expect(() => { - setup.registerDirectoryNotice('abc', () =>
); - setup.registerDirectoryNotice('def', () => ); - }).not.toThrow(); - }); - - test('throws when same directory notice is registered twice', () => { - const setup = new TutorialService().setup(); - expect(() => { - setup.registerDirectoryNotice('abc', () =>
); - setup.registerDirectoryNotice('abc', () => ); - }).toThrow(); - }); - test('allows multiple register directory header link calls', () => { const setup = new TutorialService().setup(); expect(() => { @@ -91,22 +75,6 @@ describe('TutorialService', () => { }); }); - describe('getDirectoryNotices', () => { - test('returns empty array', () => { - const service = new TutorialService(); - expect(service.getDirectoryNotices()).toEqual([]); - }); - - test('returns last state of register calls', () => { - const service = new TutorialService(); - const setup = service.setup(); - const notices = [() =>
, () => ]; - setup.registerDirectoryNotice('abc', notices[0]); - setup.registerDirectoryNotice('def', notices[1]); - expect(service.getDirectoryNotices()).toEqual(notices); - }); - }); - describe('getDirectoryHeaderLinks', () => { test('returns empty array', () => { const service = new TutorialService(); diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.ts b/src/plugins/home/public/services/tutorials/tutorial_service.ts index 839b0702a499e..81b6bbe72e3e9 100644 --- a/src/plugins/home/public/services/tutorials/tutorial_service.ts +++ b/src/plugins/home/public/services/tutorials/tutorial_service.ts @@ -11,9 +11,6 @@ import React from 'react'; /** @public */ export type TutorialVariables = Partial>; -/** @public */ -export type TutorialDirectoryNoticeComponent = React.FC; - /** @public */ export type TutorialDirectoryHeaderLinkComponent = React.FC; @@ -27,7 +24,6 @@ type CustomComponent = () => Promise; export class TutorialService { private tutorialVariables: TutorialVariables = {}; - private tutorialDirectoryNotices: { [key: string]: TutorialDirectoryNoticeComponent } = {}; private tutorialDirectoryHeaderLinks: { [key: string]: TutorialDirectoryHeaderLinkComponent; } = {}; @@ -47,16 +43,6 @@ export class TutorialService { this.tutorialVariables[key] = value; }, - /** - * Registers a component that will be rendered at the top of tutorial directory page. - */ - registerDirectoryNotice: (id: string, component: TutorialDirectoryNoticeComponent) => { - if (this.tutorialDirectoryNotices[id]) { - throw new Error(`directory notice ${id} already set`); - } - this.tutorialDirectoryNotices[id] = component; - }, - /** * Registers a component that will be rendered next to tutorial directory title/header area. */ @@ -94,10 +80,6 @@ export class TutorialService { return this.tutorialVariables; } - public getDirectoryNotices() { - return Object.values(this.tutorialDirectoryNotices); - } - public getDirectoryHeaderLinks() { return Object.values(this.tutorialDirectoryHeaderLinks); } diff --git a/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_index_list_prompt/empty_index_list_prompt.tsx b/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_index_list_prompt/empty_index_list_prompt.tsx index 1331eb9b7c4ac..d00f9e2368e21 100644 --- a/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_index_list_prompt/empty_index_list_prompt.tsx +++ b/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_index_list_prompt/empty_index_list_prompt.tsx @@ -91,7 +91,7 @@ export const EmptyIndexListPrompt = ({ { - navigateToApp('home', { path: '#/tutorial_directory' }); + navigateToApp('home', { path: '/app/integrations/browse' }); closeFlyout(); }} icon={} diff --git a/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap b/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap index 6da2f95fa394d..babcab15a4974 100644 --- a/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap +++ b/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap @@ -226,10 +226,7 @@ exports[`Overview render 1`] = ` [MockFunction] { "calls": Array [ Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], Array [ "kibana_landing_page", @@ -259,11 +256,7 @@ exports[`Overview render 1`] = ` "results": Array [ Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, Object { "type": "return", @@ -533,10 +526,7 @@ exports[`Overview without features 1`] = ` [MockFunction] { "calls": Array [ Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], Array [ "kibana_landing_page", @@ -563,16 +553,10 @@ exports[`Overview without features 1`] = ` "/plugins/kibanaReact/assets/solutions_solution_4.svg", ], Array [ - "/app/home#/tutorial_directory", + "/app/integrations/browse", ], Array [ - "home#/tutorial_directory", - ], - Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], Array [ "kibana_landing_page", @@ -602,11 +586,7 @@ exports[`Overview without features 1`] = ` "results": Array [ Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, Object { "type": "return", @@ -642,19 +622,11 @@ exports[`Overview without features 1`] = ` }, Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "/app/home#/tutorial_directory", + "value": "/app/integrations/browse", }, Object { "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, Object { "type": "return", @@ -801,10 +773,7 @@ exports[`Overview without solutions 1`] = ` [MockFunction] { "calls": Array [ Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], Array [ "kibana_landing_page", @@ -831,20 +800,13 @@ exports[`Overview without solutions 1`] = ` "/plugins/kibanaReact/assets/solutions_solution_4.svg", ], Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], ], "results": Array [ Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, Object { "type": "return", @@ -880,11 +842,7 @@ exports[`Overview without solutions 1`] = ` }, Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, ], } @@ -898,10 +856,7 @@ exports[`Overview without solutions 1`] = ` [MockFunction] { "calls": Array [ Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], Array [ "kibana_landing_page", @@ -928,20 +883,13 @@ exports[`Overview without solutions 1`] = ` "/plugins/kibanaReact/assets/solutions_solution_4.svg", ], Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], ], "results": Array [ Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, Object { "type": "return", @@ -977,11 +925,7 @@ exports[`Overview without solutions 1`] = ` }, Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, ], } @@ -1001,10 +945,7 @@ exports[`Overview without solutions 1`] = ` [MockFunction] { "calls": Array [ Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], Array [ "kibana_landing_page", @@ -1031,20 +972,13 @@ exports[`Overview without solutions 1`] = ` "/plugins/kibanaReact/assets/solutions_solution_4.svg", ], Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], ], "results": Array [ Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, Object { "type": "return", @@ -1080,11 +1014,7 @@ exports[`Overview without solutions 1`] = ` }, Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, ], } diff --git a/src/plugins/kibana_overview/public/components/overview/overview.tsx b/src/plugins/kibana_overview/public/components/overview/overview.tsx index 07769e2f3c474..6a0279bd12465 100644 --- a/src/plugins/kibana_overview/public/components/overview/overview.tsx +++ b/src/plugins/kibana_overview/public/components/overview/overview.tsx @@ -61,7 +61,7 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => const IS_DARK_THEME = uiSettings.get('theme:darkMode'); // Home does not have a locator implemented, so hard-code it here. - const addDataHref = addBasePath('/app/home#/tutorial_directory'); + const addDataHref = addBasePath('/app/integrations/browse'); const devToolsHref = share.url.locators.get('CONSOLE_APP_LOCATOR')?.useUrl({}); const managementHref = share.url.locators .get('MANAGEMENT_APP_LOCATOR') @@ -86,8 +86,14 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => }), logo: 'logoKibana', actions: { - beats: { - href: addBasePath(`home#/tutorial_directory`), + elasticAgent: { + title: i18n.translate('kibanaOverview.noDataConfig.title', { + defaultMessage: 'Add integrations', + }), + description: i18n.translate('kibanaOverview.noDataConfig.description', { + defaultMessage: + 'Use Elastic Agent or Beats to collect data and build out Analytics solutions.', + }), }, }, docsLink: docLinks.links.kibana, diff --git a/src/plugins/kibana_react/public/assets/elastic_beats_card_dark.svg b/src/plugins/kibana_react/public/assets/elastic_beats_card_dark.svg deleted file mode 100644 index 8652d8d921506..0000000000000 --- a/src/plugins/kibana_react/public/assets/elastic_beats_card_dark.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/plugins/kibana_react/public/assets/elastic_beats_card_light.svg b/src/plugins/kibana_react/public/assets/elastic_beats_card_light.svg deleted file mode 100644 index f54786c1b950c..0000000000000 --- a/src/plugins/kibana_react/public/assets/elastic_beats_card_light.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap index d8bc5745ec8e5..8842a3c9f5842 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap +++ b/src/plugins/kibana_react/public/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap @@ -73,9 +73,9 @@ exports[`NoDataPage render 1`] = ` - diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap index 3f72ae5597a98..f66d05140b2e9 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap @@ -13,7 +13,36 @@ exports[`ElasticAgentCard props button 1`] = ` href="/app/integrations/browse" image="/plugins/kibanaReact/assets/elastic_agent_card.svg" paddingSize="l" - title="Add Elastic Agent" + title={ + + + Add Elastic Agent + + + } +/> +`; + +exports[`ElasticAgentCard props category 1`] = ` + + Add Elastic Agent + + } + href="/app/integrations/browse/custom" + image="/plugins/kibanaReact/assets/elastic_agent_card.svg" + paddingSize="l" + title={ + + + Add Elastic Agent + + + } /> `; @@ -30,7 +59,13 @@ exports[`ElasticAgentCard props href 1`] = ` href="#" image="/plugins/kibanaReact/assets/elastic_agent_card.svg" paddingSize="l" - title="Add Elastic Agent" + title={ + + + Add Elastic Agent + + + } /> `; @@ -48,7 +83,13 @@ exports[`ElasticAgentCard props recommended 1`] = ` href="/app/integrations/browse" image="/plugins/kibanaReact/assets/elastic_agent_card.svg" paddingSize="l" - title="Add Elastic Agent" + title={ + + + Add Elastic Agent + + + } /> `; @@ -65,6 +106,12 @@ exports[`ElasticAgentCard renders 1`] = ` href="/app/integrations/browse" image="/plugins/kibanaReact/assets/elastic_agent_card.svg" paddingSize="l" - title="Add Elastic Agent" + title={ + + + Add Elastic Agent + + + } /> `; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_beats_card.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_beats_card.test.tsx.snap deleted file mode 100644 index af26f9e93ebac..0000000000000 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_beats_card.test.tsx.snap +++ /dev/null @@ -1,70 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ElasticBeatsCard props button 1`] = ` - - Button - - } - href="/app/home#/tutorial_directory" - image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg" - paddingSize="l" - title="Add data" -/> -`; - -exports[`ElasticBeatsCard props href 1`] = ` - - Button - - } - href="#" - image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg" - paddingSize="l" - title="Add data" -/> -`; - -exports[`ElasticBeatsCard props recommended 1`] = ` - - Add data - - } - href="/app/home#/tutorial_directory" - image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg" - paddingSize="l" - title="Add data" -/> -`; - -exports[`ElasticBeatsCard renders 1`] = ` - - Add data - - } - href="/app/home#/tutorial_directory" - image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg" - paddingSize="l" - title="Add data" -/> -`; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx index 45cc32cae06d6..b971abf06a437 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx @@ -14,7 +14,10 @@ jest.mock('../../../context', () => ({ ...jest.requireActual('../../../context'), useKibana: jest.fn().mockReturnValue({ services: { - http: { basePath: { prepend: jest.fn((path: string) => (path ? path : 'path')) } }, + http: { + basePath: { prepend: jest.fn((path: string) => (path ? path : 'path')) }, + }, + application: { capabilities: { navLinks: { integrations: true } } }, uiSettings: { get: jest.fn() }, }, }), @@ -41,5 +44,10 @@ describe('ElasticAgentCard', () => { const component = shallow(); expect(component).toMatchSnapshot(); }); + + test('category', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); }); }); diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx index f071bd9fab25a..5a91e568471d1 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx @@ -9,7 +9,7 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; import { CoreStart } from 'kibana/public'; -import { EuiButton, EuiCard } from '@elastic/eui'; +import { EuiButton, EuiCard, EuiTextColor, EuiScreenReaderOnly } from '@elastic/eui'; import { useKibana } from '../../../context'; import { NoDataPageActions, NO_DATA_RECOMMENDED } from '../no_data_page'; @@ -27,13 +27,40 @@ export const ElasticAgentCard: FunctionComponent = ({ href, button, layout, + category, ...cardRest }) => { const { - services: { http }, + services: { http, application }, } = useKibana(); const addBasePath = http.basePath.prepend; - const basePathUrl = '/plugins/kibanaReact/assets/'; + const image = addBasePath(`/plugins/kibanaReact/assets/elastic_agent_card.svg`); + const canAccessFleet = application.capabilities.navLinks.integrations; + const hasCategory = category ? `/${category}` : ''; + + if (!canAccessFleet) { + return ( + + {i18n.translate('kibana-react.noDataPage.elasticAgentCard.noPermission.title', { + defaultMessage: `Contact your administrator`, + })} + + } + description={ + + {i18n.translate('kibana-react.noDataPage.elasticAgentCard.noPermission.description', { + defaultMessage: `This integration is not yet enabled. Your administrator has the required permissions to turn it on.`, + })} + + } + isDisabled + /> + ); + } const defaultCTAtitle = i18n.translate('kibana-react.noDataPage.elasticAgentCard.title', { defaultMessage: 'Add Elastic Agent', @@ -51,12 +78,17 @@ export const ElasticAgentCard: FunctionComponent = ({ return ( + {defaultCTAtitle} + + } description={i18n.translate('kibana-react.noDataPage.elasticAgentCard.description', { defaultMessage: `Use Elastic Agent for a simple, unified way to collect data from your machines.`, })} - image={addBasePath(`${basePathUrl}elastic_agent_card.svg`)} betaBadgeLabel={recommended ? NO_DATA_RECOMMENDED : undefined} footer={footer} layout={layout as 'vertical' | undefined} diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.test.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.test.tsx deleted file mode 100644 index 6ea41bf6b3e1f..0000000000000 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.test.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { shallow } from 'enzyme'; -import React from 'react'; -import { ElasticBeatsCard } from './elastic_beats_card'; - -jest.mock('../../../context', () => ({ - ...jest.requireActual('../../../context'), - useKibana: jest.fn().mockReturnValue({ - services: { - http: { basePath: { prepend: jest.fn((path: string) => (path ? path : 'path')) } }, - uiSettings: { get: jest.fn() }, - }, - }), -})); - -describe('ElasticBeatsCard', () => { - test('renders', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - - describe('props', () => { - test('recommended', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - - test('button', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - - test('href', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - }); -}); diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.tsx deleted file mode 100644 index 0372d12096489..0000000000000 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { FunctionComponent } from 'react'; -import { i18n } from '@kbn/i18n'; -import { CoreStart } from 'kibana/public'; -import { EuiButton, EuiCard } from '@elastic/eui'; -import { useKibana } from '../../../context'; -import { NoDataPageActions, NO_DATA_RECOMMENDED } from '../no_data_page'; - -export type ElasticBeatsCardProps = NoDataPageActions & { - solution: string; -}; - -export const ElasticBeatsCard: FunctionComponent = ({ - recommended, - title, - button, - href, - solution, // unused for now - layout, - ...cardRest -}) => { - const { - services: { http, uiSettings }, - } = useKibana(); - const addBasePath = http.basePath.prepend; - const basePathUrl = '/plugins/kibanaReact/assets/'; - const IS_DARK_THEME = uiSettings.get('theme:darkMode'); - - const defaultCTAtitle = i18n.translate('kibana-react.noDataPage.elasticBeatsCard.title', { - defaultMessage: 'Add data', - }); - - const footer = - typeof button !== 'string' && typeof button !== 'undefined' ? ( - button - ) : ( - // The href and/or onClick are attached to the whole Card, so the button is just for show. - // Do not add the behavior here too or else it will propogate through - {button || title || defaultCTAtitle} - ); - - return ( - - ); -}; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/index.ts b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/index.ts index 3744239d9a472..e05d4d9675ca9 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/index.ts +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/index.ts @@ -7,5 +7,4 @@ */ export * from './elastic_agent_card'; -export * from './elastic_beats_card'; export * from './no_data_card'; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx index 56eb0f34617d6..b2d9ef6ca5008 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { KibanaPageTemplateProps } from '../page_template'; -import { ElasticAgentCard, ElasticBeatsCard, NoDataCard } from './no_data_card'; +import { ElasticAgentCard, NoDataCard } from './no_data_card'; import { KibanaPageTemplateSolutionNavAvatar } from '../solution_nav'; export const NO_DATA_PAGE_MAX_WIDTH = 950; @@ -55,6 +55,10 @@ export type NoDataPageActions = Partial & { * Remapping `onClick` to any element */ onClick?: MouseEventHandler; + /** + * Category to auto-select within Fleet + */ + category?: string; }; export type NoDataPageActionsProps = Record; @@ -107,18 +111,12 @@ export const NoDataPage: FunctionComponent = ({ const actionsKeys = Object.keys(sortedData); const renderActions = useMemo(() => { return Object.values(sortedData).map((action, i) => { - if (actionsKeys[i] === 'elasticAgent') { + if (actionsKeys[i] === 'elasticAgent' || actionsKeys[i] === 'beats') { return ( ); - } else if (actionsKeys[i] === 'beats') { - return ( - - - - ); } else { return ( {isSwitchActive && ( + + {i18n.translate( + 'xpack.apm.settings.schema.success.viewIntegrationInFleet.buttonText', + { defaultMessage: 'View the APM integration in Fleet' } + )} + + + +

+ + {i18n.translate( + 'xpack.apm.settings.schema.success.returnText.serviceInventoryLink', + { defaultMessage: 'Service inventory' } + )} + + ), + }} + /> +

+
+
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/successful_migration_card.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/successful_migration_card.tsx new file mode 100644 index 0000000000000..839479fbbf652 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/successful_migration_card.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiCard, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { CardFooterContent } from './card_footer_content'; + +export function SuccessfulMigrationCard() { + return ( + } + title={i18n.translate('xpack.apm.settings.schema.success.title', { + defaultMessage: 'Elastic Agent successfully setup!', + })} + description={i18n.translate( + 'xpack.apm.settings.schema.success.description', + { + defaultMessage: + 'Your APM integration is now setup and ready to receive data from your currently instrumented agents. Feel free to review the policies applied to your integtration.', + } + )} + footer={} + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/upgrade_available_card.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/upgrade_available_card.tsx new file mode 100644 index 0000000000000..8c10236335961 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/upgrade_available_card.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiCard, EuiIcon, EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import { useUpgradeApmPackagePolicyHref } from '../../../../shared/Links/kibana'; +import { CardFooterContent } from './card_footer_content'; + +export function UpgradeAvailableCard({ + apmPackagePolicyId, +}: { + apmPackagePolicyId: string | undefined; +}) { + const upgradeApmPackagePolicyHref = + useUpgradeApmPackagePolicyHref(apmPackagePolicyId); + + return ( + } + title={i18n.translate( + 'xpack.apm.settings.schema.upgradeAvailable.title', + { + defaultMessage: 'APM integration upgrade available!', + } + )} + description={ + + {i18n.translate( + 'xpack.apm.settings.schema.upgradeAvailable.upgradePackagePolicyLink', + { defaultMessage: 'Upgrade your APM integration' } + )} + + ), + }} + /> + } + footer={} + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx index 0031c102e8ae5..cead6cd8a6fb4 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx @@ -19,11 +19,14 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { APMLink } from '../../../shared/Links/apm/APMLink'; +import semverLt from 'semver/functions/lt'; +import { SUPPORTED_APM_PACKAGE_VERSION } from '../../../../../common/fleet'; +import { PackagePolicy } from '../../../../../../fleet/common/types'; import { ElasticDocsLink } from '../../../shared/Links/ElasticDocsLink'; -import { useFleetCloudAgentPolicyHref } from '../../../shared/Links/kibana'; import rocketLaunchGraphic from './blog-rocket-720x420.png'; import { MigrationInProgressPanel } from './migration_in_progress_panel'; +import { UpgradeAvailableCard } from './migrated/upgrade_available_card'; +import { SuccessfulMigrationCard } from './migrated/successful_migration_card'; interface Props { onSwitch: () => void; @@ -34,6 +37,7 @@ interface Props { cloudApmMigrationEnabled: boolean; hasCloudAgentPolicy: boolean; hasRequiredRole: boolean; + cloudApmPackagePolicy: PackagePolicy | undefined; } export function SchemaOverview({ onSwitch, @@ -44,10 +48,13 @@ export function SchemaOverview({ cloudApmMigrationEnabled, hasCloudAgentPolicy, hasRequiredRole, + cloudApmPackagePolicy, }: Props) { - const fleetCloudAgentPolicyHref = useFleetCloudAgentPolicyHref(); const isDisabled = !cloudApmMigrationEnabled || !hasCloudAgentPolicy || !hasRequiredRole; + const packageVersion = cloudApmPackagePolicy?.package?.version; + const isUpgradeAvailable = + packageVersion && semverLt(packageVersion, SUPPORTED_APM_PACKAGE_VERSION); if (isLoading) { return ( @@ -76,54 +83,13 @@ export function SchemaOverview({ - - } - title={i18n.translate('xpack.apm.settings.schema.success.title', { - defaultMessage: 'Elastic Agent successfully setup!', - })} - description={i18n.translate( - 'xpack.apm.settings.schema.success.description', - { - defaultMessage: - 'Your APM integration is now setup and ready to receive data from your currently instrumented agents. Feel free to review the policies applied to your integtration.', - } - )} - footer={ -
- - {i18n.translate( - 'xpack.apm.settings.schema.success.viewIntegrationInFleet.buttonText', - { defaultMessage: 'View the APM integration in Fleet' } - )} - - - -

- - {i18n.translate( - 'xpack.apm.settings.schema.success.returnText.serviceInventoryLink', - { defaultMessage: 'Service inventory' } - )} - - ), - }} - /> -

-
-
- } - /> + {isUpgradeAvailable ? ( + + ) : ( + + )}
diff --git a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx index 9956452c565b3..918f94e64ef09 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx @@ -19,6 +19,7 @@ import type { IKibanaSearchResponse } from 'src/plugins/data/public'; import { EuiThemeProvider } from 'src/plugins/kibana_react/common'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; import type { LatencyCorrelationsRawResponse } from '../../../../common/search_strategies/latency_correlations/types'; +import type { RawResponseBase } from '../../../../common/search_strategies/types'; import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; import { @@ -34,7 +35,9 @@ function Wrapper({ dataSearchResponse, }: { children?: ReactNode; - dataSearchResponse: IKibanaSearchResponse; + dataSearchResponse: IKibanaSearchResponse< + LatencyCorrelationsRawResponse & RawResponseBase + >; }) { const mockDataSearch = jest.fn(() => of(dataSearchResponse)); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx index bd0ff4c87c3be..0e9639de4aa74 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx @@ -8,43 +8,24 @@ import { render, screen, waitFor } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import React, { ReactNode } from 'react'; -import { of } from 'rxjs'; import { CoreStart } from 'kibana/public'; import { merge } from 'lodash'; -import { dataPluginMock } from 'src/plugins/data/public/mocks'; -import type { IKibanaSearchResponse } from 'src/plugins/data/public'; import { EuiThemeProvider } from 'src/plugins/kibana_react/common'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; -import type { LatencyCorrelationsRawResponse } from '../../../../../common/search_strategies/latency_correlations/types'; import { MockUrlParamsContextProvider } from '../../../../context/url_params_context/mock_url_params_context_provider'; import { ApmPluginContextValue } from '../../../../context/apm_plugin/apm_plugin_context'; import { mockApmPluginContextValue, MockApmPluginContextWrapper, } from '../../../../context/apm_plugin/mock_apm_plugin_context'; +import * as useFetcherModule from '../../../../hooks/use_fetcher'; import { fromQuery } from '../../../shared/Links/url_helpers'; import { getFormattedSelection, TransactionDistribution } from './index'; -function Wrapper({ - children, - dataSearchResponse, -}: { - children?: ReactNode; - dataSearchResponse: IKibanaSearchResponse; -}) { - const mockDataSearch = jest.fn(() => of(dataSearchResponse)); - - const dataPluginMockStart = dataPluginMock.createStartContract(); +function Wrapper({ children }: { children?: ReactNode }) { const KibanaReactContext = createKibanaReactContext({ - data: { - ...dataPluginMockStart, - search: { - ...dataPluginMockStart.search, - search: mockDataSearch, - }, - }, usageCollection: { reportUiCounter: () => {} }, } as Partial); @@ -105,18 +86,14 @@ describe('transaction_details/distribution', () => { describe('TransactionDistribution', () => { it('shows loading indicator when the service is running and returned no results yet', async () => { + jest.spyOn(useFetcherModule, 'useFetcher').mockImplementation(() => ({ + data: {}, + refetch: () => {}, + status: useFetcherModule.FETCH_STATUS.LOADING, + })); + render( - + { }); it("doesn't show loading indicator when the service isn't running", async () => { + jest.spyOn(useFetcherModule, 'useFetcher').mockImplementation(() => ({ + data: { percentileThresholdValue: 1234, overallHistogram: [] }, + refetch: () => {}, + status: useFetcherModule.FETCH_STATUS.SUCCESS, + })); + render( - + { + if (serviceName && environment && start && end) { + return callApmApi({ + endpoint: 'GET /internal/apm/latency/overall_distribution', + params: { + query: { + serviceName, + transactionName, + transactionType, + kuery, + environment, + start, + end, + percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, + }, + }, + }); + } + }, + [ + serviceName, + transactionName, + transactionType, + kuery, + environment, + start, + end, + ] ); + const overallHistogram = + data.overallHistogram === undefined && status !== FETCH_STATUS.LOADING + ? [] + : data.overallHistogram; + const hasData = + Array.isArray(overallHistogram) && overallHistogram.length > 0; + useEffect(() => { - if (isErrorMessage(progress.error)) { + if (isErrorMessage(error)) { notifications.toasts.addDanger({ title: i18n.translate( 'xpack.apm.transactionDetails.distribution.errorTitle', @@ -119,10 +156,10 @@ export function TransactionDistribution({ defaultMessage: 'An error occurred fetching the distribution', } ), - text: progress.error.toString(), + text: error.toString(), }); } - }, [progress.error, notifications.toasts]); + }, [error, notifications.toasts]); const trackApmEvent = useUiTracker({ app: 'apm' }); @@ -213,7 +250,7 @@ export function TransactionDistribution({ data={transactionDistributionChartData} markerCurrentTransaction={markerCurrentTransaction} markerPercentile={DEFAULT_PERCENTILE_THRESHOLD} - markerValue={response.percentileThresholdValue ?? 0} + markerValue={data.percentileThresholdValue ?? 0} onChartSelection={onTrackedChartSelection as BrushEndListener} hasData={hasData} selection={selection} diff --git a/x-pack/plugins/apm/public/components/routing/templates/no_data_config.ts b/x-pack/plugins/apm/public/components/routing/templates/no_data_config.ts index 868db57a0efdc..92b2b1bc3d229 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/no_data_config.ts +++ b/x-pack/plugins/apm/public/components/routing/templates/no_data_config.ts @@ -24,18 +24,18 @@ export function getNoDataConfig({ defaultMessage: 'Observability', }), actions: { - beats: { - title: i18n.translate('xpack.apm.noDataConfig.beatsCard.title', { - defaultMessage: 'Add data with APM agents', + elasticAgent: { + title: i18n.translate('xpack.apm.ux.overview.agent.title', { + defaultMessage: 'Add the APM integration', }), description: i18n.translate( - 'xpack.apm.noDataConfig.beatsCard.description', + 'xpack.apm.ux.overview.agent.description', { defaultMessage: 'Use APM agents to collect APM data. We make it easy with agents for many popular languages.', } ), - href: basePath + `/app/home#/tutorial/apm`, + href: basePath + `/app/integrations/detail/apm`, }, }, docsLink, diff --git a/x-pack/plugins/apm/public/components/shared/Links/kibana.ts b/x-pack/plugins/apm/public/components/shared/Links/kibana.ts index bfb7cf849f567..c0bdf3a98aa31 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/kibana.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/kibana.ts @@ -26,3 +26,14 @@ export function useFleetCloudAgentPolicyHref() { } = useApmPluginContext(); return basePath.prepend('/app/fleet#/policies/policy-elastic-agent-on-cloud'); } + +export function useUpgradeApmPackagePolicyHref(packagePolicyId = '') { + const { + core: { + http: { basePath }, + }, + } = useApmPluginContext(); + return basePath.prepend( + `/app/fleet/policies/policy-elastic-agent-on-cloud/upgrade-package-policy/${packagePolicyId}?from=integrations-policy-list` + ); +} diff --git a/x-pack/plugins/apm/public/hooks/use_search_strategy.ts b/x-pack/plugins/apm/public/hooks/use_search_strategy.ts index ca8d28b106f84..275eddb68ae00 100644 --- a/x-pack/plugins/apm/public/hooks/use_search_strategy.ts +++ b/x-pack/plugins/apm/public/hooks/use_search_strategy.ts @@ -16,7 +16,7 @@ import { } from '../../../../../src/plugins/data/public'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; -import type { SearchStrategyClientParams } from '../../common/search_strategies/types'; +import type { RawSearchStrategyClientParams } from '../../common/search_strategies/types'; import type { RawResponseBase } from '../../common/search_strategies/types'; import type { LatencyCorrelationsParams, @@ -77,13 +77,15 @@ interface SearchStrategyReturnBase { export function useSearchStrategy( searchStrategyName: typeof APM_SEARCH_STRATEGIES.APM_LATENCY_CORRELATIONS, searchStrategyParams: LatencyCorrelationsParams -): SearchStrategyReturnBase; +): SearchStrategyReturnBase; // Function overload for Failed Transactions Correlations export function useSearchStrategy( searchStrategyName: typeof APM_SEARCH_STRATEGIES.APM_FAILED_TRANSACTIONS_CORRELATIONS, searchStrategyParams: FailedTransactionsCorrelationsParams -): SearchStrategyReturnBase; +): SearchStrategyReturnBase< + FailedTransactionsCorrelationsRawResponse & RawResponseBase +>; export function useSearchStrategy< TRawResponse extends RawResponseBase, @@ -145,7 +147,7 @@ export function useSearchStrategy< // Submit the search request using the `data.search` service. searchSubscription$.current = data.search .search< - IKibanaSearchRequest, + IKibanaSearchRequest, IKibanaSearchResponse >(request, { strategy: searchStrategyName, diff --git a/x-pack/plugins/apm/server/deprecations/deprecations.test.ts b/x-pack/plugins/apm/server/deprecations/deprecations.test.ts index d706146faf212..43e8140fb9b3c 100644 --- a/x-pack/plugins/apm/server/deprecations/deprecations.test.ts +++ b/x-pack/plugins/apm/server/deprecations/deprecations.test.ts @@ -19,7 +19,7 @@ const deprecationContext = { describe('getDeprecations', () => { describe('when fleet is disabled', () => { it('returns no deprecations', async () => { - const deprecationsCallback = getDeprecations({}); + const deprecationsCallback = getDeprecations({ branch: 'master' }); const deprecations = await deprecationsCallback(deprecationContext); expect(deprecations).toEqual([]); }); @@ -28,6 +28,7 @@ describe('getDeprecations', () => { describe('when running on cloud with legacy apm-server', () => { it('returns deprecations', async () => { const deprecationsCallback = getDeprecations({ + branch: 'master', cloudSetup: { isCloudEnabled: true } as unknown as CloudSetup, fleet: { start: () => ({ @@ -43,6 +44,7 @@ describe('getDeprecations', () => { describe('when running on cloud with fleet', () => { it('returns no deprecations', async () => { const deprecationsCallback = getDeprecations({ + branch: 'master', cloudSetup: { isCloudEnabled: true } as unknown as CloudSetup, fleet: { start: () => ({ @@ -58,6 +60,7 @@ describe('getDeprecations', () => { describe('when running on prem', () => { it('returns no deprecations', async () => { const deprecationsCallback = getDeprecations({ + branch: 'master', cloudSetup: { isCloudEnabled: false } as unknown as CloudSetup, fleet: { start: () => ({ agentPolicyService: { get: () => undefined } }), diff --git a/x-pack/plugins/apm/server/deprecations/index.ts b/x-pack/plugins/apm/server/deprecations/index.ts index b592a2bf13268..76c90270abb8f 100644 --- a/x-pack/plugins/apm/server/deprecations/index.ts +++ b/x-pack/plugins/apm/server/deprecations/index.ts @@ -15,9 +15,11 @@ import { APMRouteHandlerResources } from '../'; export function getDeprecations({ cloudSetup, fleet, + branch, }: { cloudSetup?: CloudSetup; fleet?: APMRouteHandlerResources['plugins']['fleet']; + branch: string; }) { return async ({ savedObjectsClient, @@ -46,8 +48,7 @@ export function getDeprecations({ defaultMessage: 'Running the APM Server binary directly is considered a legacy option and is deprecated since 7.16. Switch to APM Server managed by an Elastic Agent instead. Read our documentation to learn more.', }), - documentationUrl: - 'https://www.elastic.co/guide/en/apm/server/current/apm-integration.html', + documentationUrl: `https://www.elastic.co/guide/en/apm/server/${branch}/apm-integration.html`, level: 'warning', correctiveActions: { manualSteps: [ diff --git a/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts b/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts index 64b071b67d2bd..98b6a6489c47b 100644 --- a/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts +++ b/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts @@ -5,7 +5,10 @@ * 2.0. */ -import { POLICY_ELASTIC_AGENT_ON_CLOUD } from '../../../common/fleet'; +import { + POLICY_ELASTIC_AGENT_ON_CLOUD, + SUPPORTED_APM_PACKAGE_VERSION, +} from '../../../common/fleet'; import { APMPluginSetupDependencies } from '../../types'; import { APM_PACKAGE_NAME } from './get_cloud_apm_package_policy'; @@ -36,7 +39,7 @@ export function getApmPackagePolicyDefinition( ], package: { name: APM_PACKAGE_NAME, - version: '0.4.0', + version: SUPPORTED_APM_PACKAGE_VERSION, title: 'Elastic APM', }, }; diff --git a/x-pack/plugins/apm/server/lib/latency/get_overall_latency_distribution.ts b/x-pack/plugins/apm/server/lib/latency/get_overall_latency_distribution.ts new file mode 100644 index 0000000000000..39470869488c3 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/latency/get_overall_latency_distribution.ts @@ -0,0 +1,121 @@ +/* + * 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 { estypes } from '@elastic/elasticsearch'; + +import { ProcessorEvent } from '../../../common/processor_event'; + +import { withApmSpan } from '../../utils/with_apm_span'; + +import { + getHistogramIntervalRequest, + getHistogramRangeSteps, +} from '../search_strategies/queries/query_histogram_range_steps'; +import { getTransactionDurationRangesRequest } from '../search_strategies/queries/query_ranges'; + +import { getPercentileThresholdValue } from './get_percentile_threshold_value'; +import type { + OverallLatencyDistributionOptions, + OverallLatencyDistributionResponse, +} from './types'; + +export async function getOverallLatencyDistribution( + options: OverallLatencyDistributionOptions +) { + return withApmSpan('get_overall_latency_distribution', async () => { + const overallLatencyDistribution: OverallLatencyDistributionResponse = { + log: [], + }; + + const { setup, ...rawParams } = options; + const { apmEventClient } = setup; + const params = { + // pass on an empty index because we're using only the body attribute + // of the request body getters we're reusing from search strategies. + index: '', + ...rawParams, + }; + + // #1: get 95th percentile to be displayed as a marker in the log log chart + overallLatencyDistribution.percentileThresholdValue = + await getPercentileThresholdValue(options); + + // finish early if we weren't able to identify the percentileThresholdValue. + if (!overallLatencyDistribution.percentileThresholdValue) { + return overallLatencyDistribution; + } + + // #2: get histogram range steps + const steps = 100; + + const { body: histogramIntervalRequestBody } = + getHistogramIntervalRequest(params); + + const histogramIntervalResponse = (await apmEventClient.search( + 'get_histogram_interval', + { + // TODO: add support for metrics + apm: { events: [ProcessorEvent.transaction] }, + body: histogramIntervalRequestBody, + } + )) as { + aggregations?: { + transaction_duration_min: estypes.AggregationsValueAggregate; + transaction_duration_max: estypes.AggregationsValueAggregate; + }; + hits: { total: estypes.SearchTotalHits }; + }; + + if ( + !histogramIntervalResponse.aggregations || + histogramIntervalResponse.hits.total.value === 0 + ) { + return overallLatencyDistribution; + } + + const min = + histogramIntervalResponse.aggregations.transaction_duration_min.value; + const max = + histogramIntervalResponse.aggregations.transaction_duration_max.value * 2; + + const histogramRangeSteps = getHistogramRangeSteps(min, max, steps); + + // #3: get histogram chart data + const { body: transactionDurationRangesRequestBody } = + getTransactionDurationRangesRequest(params, histogramRangeSteps); + + const transactionDurationRangesResponse = (await apmEventClient.search( + 'get_transaction_duration_ranges', + { + // TODO: add support for metrics + apm: { events: [ProcessorEvent.transaction] }, + body: transactionDurationRangesRequestBody, + } + )) as { + aggregations?: { + logspace_ranges: estypes.AggregationsMultiBucketAggregate<{ + from: number; + doc_count: number; + }>; + }; + }; + + if (!transactionDurationRangesResponse.aggregations) { + return overallLatencyDistribution; + } + + overallLatencyDistribution.overallHistogram = + transactionDurationRangesResponse.aggregations.logspace_ranges.buckets + .map((d) => ({ + key: d.from, + doc_count: d.doc_count, + })) + .filter((d) => d.key !== undefined); + + return overallLatencyDistribution; + }); +} diff --git a/x-pack/plugins/apm/server/lib/latency/get_percentile_threshold_value.ts b/x-pack/plugins/apm/server/lib/latency/get_percentile_threshold_value.ts new file mode 100644 index 0000000000000..0d417a370e0b6 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/latency/get_percentile_threshold_value.ts @@ -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 type { estypes } from '@elastic/elasticsearch'; + +import { ProcessorEvent } from '../../../common/processor_event'; + +import { getTransactionDurationPercentilesRequest } from '../search_strategies/queries/query_percentiles'; + +import type { OverallLatencyDistributionOptions } from './types'; + +export async function getPercentileThresholdValue( + options: OverallLatencyDistributionOptions +) { + const { setup, percentileThreshold, ...rawParams } = options; + const { apmEventClient } = setup; + const params = { + // pass on an empty index because we're using only the body attribute + // of the request body getters we're reusing from search strategies. + index: '', + ...rawParams, + }; + + const { body: transactionDurationPercentilesRequestBody } = + getTransactionDurationPercentilesRequest(params, [percentileThreshold]); + + const transactionDurationPercentilesResponse = (await apmEventClient.search( + 'get_transaction_duration_percentiles', + { + // TODO: add support for metrics + apm: { events: [ProcessorEvent.transaction] }, + body: transactionDurationPercentilesRequestBody, + } + )) as { + aggregations?: { + transaction_duration_percentiles: estypes.AggregationsTDigestPercentilesAggregate; + }; + }; + + if (!transactionDurationPercentilesResponse.aggregations) { + return; + } + + const percentilesResponseThresholds = + transactionDurationPercentilesResponse.aggregations + .transaction_duration_percentiles?.values ?? {}; + + return percentilesResponseThresholds[`${percentileThreshold}.0`]; +} diff --git a/x-pack/plugins/apm/server/lib/latency/types.ts b/x-pack/plugins/apm/server/lib/latency/types.ts new file mode 100644 index 0000000000000..8dad1a39bd159 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/latency/types.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Setup } from '../helpers/setup_request'; +import { CorrelationsOptions } from '../search_strategies/queries/get_filters'; + +export interface OverallLatencyDistributionOptions extends CorrelationsOptions { + percentileThreshold: number; + setup: Setup; +} + +export interface OverallLatencyDistributionResponse { + log: string[]; + percentileThresholdValue?: number; + overallHistogram?: Array<{ + key: number; + doc_count: number; + }>; +} diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts index af5e535abdc3f..efc28ce98e5e0 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts @@ -9,17 +9,15 @@ import { chunk } from 'lodash'; import type { ElasticsearchClient } from 'src/core/server'; -import type { ISearchStrategy } from '../../../../../../../src/plugins/data/server'; -import { - IKibanaSearchRequest, - IKibanaSearchResponse, -} from '../../../../../../../src/plugins/data/common'; - import { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames'; import { EventOutcome } from '../../../../common/event_outcome'; -import type { SearchStrategyServerParams } from '../../../../common/search_strategies/types'; import type { - FailedTransactionsCorrelationsRequestParams, + SearchStrategyClientParams, + SearchStrategyServerParams, + RawResponseBase, +} from '../../../../common/search_strategies/types'; +import type { + FailedTransactionsCorrelationsParams, FailedTransactionsCorrelationsRawResponse, } from '../../../../common/search_strategies/failed_transactions_correlations/types'; import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; @@ -38,22 +36,18 @@ import { failedTransactionsCorrelationsSearchServiceStateProvider } from './fail import { ERROR_CORRELATION_THRESHOLD } from '../constants'; import { fetchFieldsStats } from '../queries/field_stats/get_fields_stats'; -export type FailedTransactionsCorrelationsSearchServiceProvider = +type FailedTransactionsCorrelationsSearchServiceProvider = SearchServiceProvider< - FailedTransactionsCorrelationsRequestParams, - FailedTransactionsCorrelationsRawResponse + FailedTransactionsCorrelationsParams & SearchStrategyClientParams, + FailedTransactionsCorrelationsRawResponse & RawResponseBase >; -export type FailedTransactionsCorrelationsSearchStrategy = ISearchStrategy< - IKibanaSearchRequest, - IKibanaSearchResponse ->; - export const failedTransactionsCorrelationsSearchServiceProvider: FailedTransactionsCorrelationsSearchServiceProvider = ( esClient: ElasticsearchClient, getApmIndices: () => Promise, - searchServiceParams: FailedTransactionsCorrelationsRequestParams, + searchServiceParams: FailedTransactionsCorrelationsParams & + SearchStrategyClientParams, includeFrozen: boolean ) => { const { addLogMessage, getLogMessages } = searchServiceLogProvider(); @@ -63,7 +57,8 @@ export const failedTransactionsCorrelationsSearchServiceProvider: FailedTransact async function fetchErrorCorrelations() { try { const indices = await getApmIndices(); - const params: FailedTransactionsCorrelationsRequestParams & + const params: FailedTransactionsCorrelationsParams & + SearchStrategyClientParams & SearchStrategyServerParams = { ...searchServiceParams, index: indices.transaction, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts index ec91165cb481b..4763cd994d309 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts @@ -5,8 +5,4 @@ * 2.0. */ -export { - failedTransactionsCorrelationsSearchServiceProvider, - FailedTransactionsCorrelationsSearchServiceProvider, - FailedTransactionsCorrelationsSearchStrategy, -} from './failed_transactions_correlations_search_service'; +export { failedTransactionsCorrelationsSearchServiceProvider } from './failed_transactions_correlations_search_service'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/index.ts b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/index.ts index 073bb122896ff..040aa5a7e424e 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/index.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/index.ts @@ -5,8 +5,4 @@ * 2.0. */ -export { - latencyCorrelationsSearchServiceProvider, - LatencyCorrelationsSearchServiceProvider, - LatencyCorrelationsSearchStrategy, -} from './latency_correlations_search_service'; +export { latencyCorrelationsSearchServiceProvider } from './latency_correlations_search_service'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts index 4862f7dd1de1a..f170818d018d4 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts @@ -8,15 +8,13 @@ import { range } from 'lodash'; import type { ElasticsearchClient } from 'src/core/server'; -import type { ISearchStrategy } from '../../../../../../../src/plugins/data/server'; -import { - IKibanaSearchRequest, - IKibanaSearchResponse, -} from '../../../../../../../src/plugins/data/common'; - -import type { SearchStrategyServerParams } from '../../../../common/search_strategies/types'; import type { - LatencyCorrelationsRequestParams, + RawResponseBase, + SearchStrategyClientParams, + SearchStrategyServerParams, +} from '../../../../common/search_strategies/types'; +import type { + LatencyCorrelationsParams, LatencyCorrelationsRawResponse, } from '../../../../common/search_strategies/latency_correlations/types'; @@ -38,21 +36,16 @@ import type { SearchServiceProvider } from '../search_strategy_provider'; import { latencyCorrelationsSearchServiceStateProvider } from './latency_correlations_search_service_state'; import { fetchFieldsStats } from '../queries/field_stats/get_fields_stats'; -export type LatencyCorrelationsSearchServiceProvider = SearchServiceProvider< - LatencyCorrelationsRequestParams, - LatencyCorrelationsRawResponse ->; - -export type LatencyCorrelationsSearchStrategy = ISearchStrategy< - IKibanaSearchRequest, - IKibanaSearchResponse +type LatencyCorrelationsSearchServiceProvider = SearchServiceProvider< + LatencyCorrelationsParams & SearchStrategyClientParams, + LatencyCorrelationsRawResponse & RawResponseBase >; export const latencyCorrelationsSearchServiceProvider: LatencyCorrelationsSearchServiceProvider = ( esClient: ElasticsearchClient, getApmIndices: () => Promise, - searchServiceParams: LatencyCorrelationsRequestParams, + searchServiceParams: LatencyCorrelationsParams & SearchStrategyClientParams, includeFrozen: boolean ) => { const { addLogMessage, getLogMessages } = searchServiceLogProvider(); @@ -61,7 +54,9 @@ export const latencyCorrelationsSearchServiceProvider: LatencyCorrelationsSearch async function fetchCorrelations() { let params: - | (LatencyCorrelationsRequestParams & SearchStrategyServerParams) + | (LatencyCorrelationsParams & + SearchStrategyClientParams & + SearchStrategyServerParams) | undefined; try { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_field_stats.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_field_stats.test.ts index deb89ace47c5d..d3cee1c4ca596 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_field_stats.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_field_stats.test.ts @@ -15,8 +15,8 @@ import { fetchFieldsStats } from './get_fields_stats'; const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.test.ts index c77b4df78f865..9d0441e513198 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.test.ts @@ -14,8 +14,8 @@ describe('correlations', () => { const query = getQueryWithParams({ params: { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', @@ -45,8 +45,8 @@ describe('correlations', () => { index: 'apm-*', serviceName: 'actualServiceName', transactionName: 'actualTransactionName', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, environment: 'dev', kuery: '', includeFrozen: false, @@ -93,8 +93,8 @@ describe('correlations', () => { const query = getQueryWithParams({ params: { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts index f00c89503f103..31a98b0a6bb18 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts @@ -6,15 +6,10 @@ */ import type { estypes } from '@elastic/elasticsearch'; -import { getOrElse } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; -import * as t from 'io-ts'; -import { failure } from 'io-ts/lib/PathReporter'; import type { FieldValuePair, SearchStrategyParams, } from '../../../../common/search_strategies/types'; -import { rangeRt } from '../../../routes/default_api_types'; import { getCorrelationsFilters } from './get_filters'; export const getTermsQuery = ({ fieldName, fieldValue }: FieldValuePair) => { @@ -36,22 +31,14 @@ export const getQueryWithParams = ({ params, termFilters }: QueryParams) => { transactionName, } = params; - // converts string based start/end to epochmillis - const decodedRange = pipe( - rangeRt.decode({ start, end }), - getOrElse((errors) => { - throw new Error(failure(errors).join('\n')); - }) - ); - const correlationFilters = getCorrelationsFilters({ environment, kuery, serviceName, transactionType, transactionName, - start: decodedRange.start, - end: decodedRange.end, + start, + end, }); return { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.test.ts index fd5f52207d4c5..eb771e1e1aaf4 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.test.ts @@ -16,6 +16,8 @@ describe('correlations', () => { includeFrozen: true, environment: ENVIRONMENT_ALL.value, kuery: '', + start: 1577836800000, + end: 1609459200000, }); expect(requestBase).toEqual({ index: 'apm-*', @@ -29,6 +31,8 @@ describe('correlations', () => { index: 'apm-*', environment: ENVIRONMENT_ALL.value, kuery: '', + start: 1577836800000, + end: 1609459200000, }); expect(requestBase).toEqual({ index: 'apm-*', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.test.ts index fc2dacce61a73..40fcc17444492 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.test.ts @@ -18,8 +18,8 @@ import { const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.test.ts index 6e0521ac1a008..bae42666e6db0 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.test.ts @@ -20,8 +20,8 @@ import { const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.test.ts index 9ffbf6b2ce18d..ab7a0b4e02072 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.test.ts @@ -20,8 +20,8 @@ import { const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.test.ts index daf6b368c78b1..9c704ef7b489a 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.test.ts @@ -17,8 +17,8 @@ import { const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.test.ts index 7ecb1d2d8a333..7cc6106f671a7 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.test.ts @@ -17,8 +17,8 @@ import { const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.test.ts index ffc86c7ef6c32..41a2fa9a5039e 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.test.ts @@ -17,8 +17,8 @@ import { const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.ts index 790919d193028..439bb9e4b9cd6 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.ts @@ -17,7 +17,11 @@ import type { SearchStrategyParams } from '../../../../common/search_strategies/ import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; -const getHistogramRangeSteps = (min: number, max: number, steps: number) => { +export const getHistogramRangeSteps = ( + min: number, + max: number, + steps: number +) => { // A d3 based scale function as a helper to get equally distributed bins on a log scale. // We round the final values because the ES range agg we use won't accept numbers with decimals for `transaction.duration.us`. const logFn = scaleLog().domain([min, max]).range([1, steps]); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.test.ts index 375e32b1472c6..00e8c26497eb2 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.test.ts @@ -17,8 +17,8 @@ import { fetchTransactionDurationHistograms } from './query_histograms_generator const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.test.ts index ce86ffd9654e6..57e3e6cadb9bc 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.test.ts @@ -17,8 +17,8 @@ import { const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.test.ts index e210eb7d41e78..7d67e80ae3398 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.test.ts @@ -17,8 +17,8 @@ import { const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts index 8a9d04df32036..034bd2a60ad19 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts @@ -13,7 +13,7 @@ import { IKibanaSearchRequest } from '../../../../../../src/plugins/data/common' import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; import type { LatencyCorrelationsParams } from '../../../common/search_strategies/latency_correlations/types'; -import type { SearchStrategyClientParams } from '../../../common/search_strategies/types'; +import type { RawSearchStrategyClientParams } from '../../../common/search_strategies/types'; import type { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; @@ -112,7 +112,7 @@ describe('APM Correlations search strategy', () => { let mockDeps: SearchStrategyDependencies; let params: Required< IKibanaSearchRequest< - LatencyCorrelationsParams & SearchStrategyClientParams + LatencyCorrelationsParams & RawSearchStrategyClientParams > >['params']; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts index cec10294460b0..8035e9e4d97ca 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts @@ -7,6 +7,10 @@ import uuid from 'uuid'; import { of } from 'rxjs'; +import { getOrElse } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import * as t from 'io-ts'; +import { failure } from 'io-ts/lib/PathReporter'; import type { ElasticsearchClient } from 'src/core/server'; @@ -16,18 +20,21 @@ import { IKibanaSearchResponse, } from '../../../../../../src/plugins/data/common'; -import type { SearchStrategyClientParams } from '../../../common/search_strategies/types'; -import type { RawResponseBase } from '../../../common/search_strategies/types'; -import type { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; - import type { - LatencyCorrelationsSearchServiceProvider, - LatencyCorrelationsSearchStrategy, -} from './latency_correlations'; + RawResponseBase, + RawSearchStrategyClientParams, + SearchStrategyClientParams, +} from '../../../common/search_strategies/types'; +import type { + LatencyCorrelationsParams, + LatencyCorrelationsRawResponse, +} from '../../../common/search_strategies/latency_correlations/types'; import type { - FailedTransactionsCorrelationsSearchServiceProvider, - FailedTransactionsCorrelationsSearchStrategy, -} from './failed_transactions_correlations'; + FailedTransactionsCorrelationsParams, + FailedTransactionsCorrelationsRawResponse, +} from '../../../common/search_strategies/failed_transactions_correlations/types'; +import { rangeRt } from '../../routes/default_api_types'; +import type { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; interface SearchServiceState { cancel: () => void; @@ -56,35 +63,50 @@ export type SearchServiceProvider< // Failed Transactions Correlations function overload export function searchStrategyProvider( - searchServiceProvider: FailedTransactionsCorrelationsSearchServiceProvider, + searchServiceProvider: SearchServiceProvider< + FailedTransactionsCorrelationsParams & SearchStrategyClientParams, + FailedTransactionsCorrelationsRawResponse & RawResponseBase + >, getApmIndices: () => Promise, includeFrozen: boolean -): FailedTransactionsCorrelationsSearchStrategy; +): ISearchStrategy< + IKibanaSearchRequest< + FailedTransactionsCorrelationsParams & RawSearchStrategyClientParams + >, + IKibanaSearchResponse< + FailedTransactionsCorrelationsRawResponse & RawResponseBase + > +>; // Latency Correlations function overload export function searchStrategyProvider( - searchServiceProvider: LatencyCorrelationsSearchServiceProvider, + searchServiceProvider: SearchServiceProvider< + LatencyCorrelationsParams & SearchStrategyClientParams, + LatencyCorrelationsRawResponse & RawResponseBase + >, getApmIndices: () => Promise, includeFrozen: boolean -): LatencyCorrelationsSearchStrategy; +): ISearchStrategy< + IKibanaSearchRequest< + LatencyCorrelationsParams & RawSearchStrategyClientParams + >, + IKibanaSearchResponse +>; -export function searchStrategyProvider< - TSearchStrategyClientParams extends SearchStrategyClientParams, - TRawResponse extends RawResponseBase ->( +export function searchStrategyProvider( searchServiceProvider: SearchServiceProvider< - TSearchStrategyClientParams, - TRawResponse + TRequestParams & SearchStrategyClientParams, + TResponseParams & RawResponseBase >, getApmIndices: () => Promise, includeFrozen: boolean ): ISearchStrategy< - IKibanaSearchRequest, - IKibanaSearchResponse + IKibanaSearchRequest, + IKibanaSearchResponse > { const searchServiceMap = new Map< string, - GetSearchServiceState + GetSearchServiceState >(); return { @@ -93,9 +115,21 @@ export function searchStrategyProvider< throw new Error('Invalid request parameters.'); } + const { start: startString, end: endString } = request.params; + + // converts string based start/end to epochmillis + const decodedRange = pipe( + rangeRt.decode({ start: startString, end: endString }), + getOrElse((errors) => { + throw new Error(failure(errors).join('\n')); + }) + ); + // The function to fetch the current state of the search service. // This will be either an existing service for a follow up fetch or a new one for new requests. - let getSearchServiceState: GetSearchServiceState; + let getSearchServiceState: GetSearchServiceState< + TResponseParams & RawResponseBase + >; // If the request includes an ID, we require that the search service already exists // otherwise we throw an error. The client should never poll a service that's been cancelled or finished. @@ -111,10 +145,30 @@ export function searchStrategyProvider< getSearchServiceState = existingGetSearchServiceState; } else { + const { + start, + end, + environment, + kuery, + serviceName, + transactionName, + transactionType, + ...requestParams + } = request.params; + getSearchServiceState = searchServiceProvider( deps.esClient.asCurrentUser, getApmIndices, - request.params as TSearchStrategyClientParams, + { + environment, + kuery, + serviceName, + transactionName, + transactionType, + start: decodedRange.start, + end: decodedRange.end, + ...(requestParams as unknown as TRequestParams), + }, includeFrozen ); } diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index d2d8dbf602364..72a1bc483015e 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -215,10 +215,12 @@ export class APMPlugin ); })(); }); + core.deprecations.registerDeprecations({ getDeprecations: getDeprecations({ cloudSetup: plugins.cloud, fleet: resourcePlugins.fleet, + branch: this.initContext.env.packageInfo.branch, }), }); diff --git a/x-pack/plugins/apm/server/routes/fleet.ts b/x-pack/plugins/apm/server/routes/fleet.ts index 2884c08ceb9a1..e18aefcd6e0d8 100644 --- a/x-pack/plugins/apm/server/routes/fleet.ts +++ b/x-pack/plugins/apm/server/routes/fleet.ts @@ -92,7 +92,7 @@ const fleetAgentsRoute = createApmServerRoute({ }); const saveApmServerSchemaRoute = createApmServerRoute({ - endpoint: 'POST /internal/apm/fleet/apm_server_schema', + endpoint: 'POST /api/apm/fleet/apm_server_schema', options: { tags: ['access:apm', 'access:apm_write'] }, params: t.type({ body: t.type({ @@ -143,11 +143,13 @@ const getMigrationCheckRoute = createApmServerRoute({ fleetPluginStart, }) : undefined; + const apmPackagePolicy = getApmPackagePolicy(cloudAgentPolicy); return { has_cloud_agent_policy: !!cloudAgentPolicy, - has_cloud_apm_package_policy: !!getApmPackagePolicy(cloudAgentPolicy), + has_cloud_apm_package_policy: !!apmPackagePolicy, cloud_apm_migration_enabled: cloudApmMigrationEnabled, has_required_role: hasRequiredRole, + cloud_apm_package_policy: apmPackagePolicy, }; }, }); diff --git a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts index 472e46fecfa10..3fa6152d953f3 100644 --- a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts +++ b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts @@ -17,6 +17,7 @@ import { environmentsRouteRepository } from './environments'; import { errorsRouteRepository } from './errors'; import { apmFleetRouteRepository } from './fleet'; import { indexPatternRouteRepository } from './index_pattern'; +import { latencyDistributionRouteRepository } from './latency_distribution'; import { metricsRouteRepository } from './metrics'; import { observabilityOverviewRouteRepository } from './observability_overview'; import { rumRouteRepository } from './rum_client'; @@ -41,6 +42,7 @@ const getTypedGlobalApmServerRouteRepository = () => { .merge(indexPatternRouteRepository) .merge(environmentsRouteRepository) .merge(errorsRouteRepository) + .merge(latencyDistributionRouteRepository) .merge(metricsRouteRepository) .merge(observabilityOverviewRouteRepository) .merge(rumRouteRepository) diff --git a/x-pack/plugins/apm/server/routes/latency_distribution.ts b/x-pack/plugins/apm/server/routes/latency_distribution.ts new file mode 100644 index 0000000000000..ea921a7f4838d --- /dev/null +++ b/x-pack/plugins/apm/server/routes/latency_distribution.ts @@ -0,0 +1,63 @@ +/* + * 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 t from 'io-ts'; +import { toNumberRt } from '@kbn/io-ts-utils'; +import { getOverallLatencyDistribution } from '../lib/latency/get_overall_latency_distribution'; +import { setupRequest } from '../lib/helpers/setup_request'; +import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { environmentRt, kueryRt, rangeRt } from './default_api_types'; + +const latencyOverallDistributionRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/latency/overall_distribution', + params: t.type({ + query: t.intersection([ + t.partial({ + serviceName: t.string, + transactionName: t.string, + transactionType: t.string, + }), + environmentRt, + kueryRt, + rangeRt, + t.type({ + percentileThreshold: toNumberRt, + }), + ]), + }), + options: { tags: ['access:apm'] }, + handler: async (resources) => { + const setup = await setupRequest(resources); + + const { + environment, + kuery, + serviceName, + transactionType, + transactionName, + start, + end, + percentileThreshold, + } = resources.params.query; + + return getOverallLatencyDistribution({ + environment, + kuery, + serviceName, + transactionType, + transactionName, + start, + end, + percentileThreshold, + setup, + }); + }, +}); + +export const latencyDistributionRouteRepository = + createApmServerRouteRepository().add(latencyOverallDistributionRoute); diff --git a/x-pack/plugins/enterprise_search/server/integrations.ts b/x-pack/plugins/enterprise_search/server/integrations.ts index 48909261243e8..eee5cdc3aaec3 100644 --- a/x-pack/plugins/enterprise_search/server/integrations.ts +++ b/x-pack/plugins/enterprise_search/server/integrations.ts @@ -301,4 +301,67 @@ export const registerEnterpriseSearchIntegrations = ( ...integration, }); }); + + customIntegrations.registerCustomIntegration({ + id: 'app_search_web_crawler', + title: i18n.translate('xpack.enterpriseSearch.appSearch.integrations.webCrawlerName', { + defaultMessage: 'Web Crawler', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.appSearch.integrations.webCrawlerDescription', + { + defaultMessage: "Add search to your website with App Search's web crawler.", + } + ), + categories: ['website_search'], + uiInternalPath: '/app/enterprise_search/app_search/engines/new?method=crawler', + icons: [ + { + type: 'eui', + src: 'globe', + }, + ], + shipper: 'enterprise_search', + isBeta: false, + }); + + customIntegrations.registerCustomIntegration({ + id: 'app_search_json', + title: i18n.translate('xpack.enterpriseSearch.appSearch.integrations.jsonName', { + defaultMessage: 'JSON', + }), + description: i18n.translate('xpack.enterpriseSearch.appSearch.integrations.jsonDescription', { + defaultMessage: 'Search over your JSON data with App Search.', + }), + categories: ['upload_file'], + uiInternalPath: '/app/enterprise_search/app_search/engines/new?method=json', + icons: [ + { + type: 'eui', + src: 'exportAction', + }, + ], + shipper: 'enterprise_search', + isBeta: false, + }); + + customIntegrations.registerCustomIntegration({ + id: 'app_search_api', + title: i18n.translate('xpack.enterpriseSearch.appSearch.integrations.apiName', { + defaultMessage: 'API', + }), + description: i18n.translate('xpack.enterpriseSearch.appSearch.integrations.apiDescription', { + defaultMessage: "Add search to your application with App Search's robust APIs.", + }), + categories: ['custom'], + uiInternalPath: '/app/enterprise_search/app_search/engines/new?method=api', + icons: [ + { + type: 'eui', + src: 'editorCodeBlock', + }, + ], + shipper: 'enterprise_search', + isBeta: false, + }); }; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx index 00adb2a7b4ffb..d2d361cd212a6 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx @@ -240,7 +240,7 @@ function MissingIntegrationContent({ ), discussForumLink: ( - + !!c), }; }; diff --git a/x-pack/plugins/fleet/public/components/home_integration/index.tsx b/x-pack/plugins/fleet/public/components/home_integration/index.tsx index 27509d9c45e6e..56123e5f32dee 100644 --- a/x-pack/plugins/fleet/public/components/home_integration/index.tsx +++ b/x-pack/plugins/fleet/public/components/home_integration/index.tsx @@ -7,19 +7,11 @@ import React from 'react'; import type { - TutorialDirectoryNoticeComponent, TutorialDirectoryHeaderLinkComponent, TutorialModuleNoticeComponent, } from 'src/plugins/home/public'; import { EuiLoadingSpinner } from '@elastic/eui'; -const TutorialDirectoryNoticeLazy = React.lazy(() => import('./tutorial_directory_notice')); -export const TutorialDirectoryNotice: TutorialDirectoryNoticeComponent = () => ( - }> - - -); - const TutorialDirectoryHeaderLinkLazy = React.lazy( () => import('./tutorial_directory_header_link') ); diff --git a/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_header_link.tsx b/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_header_link.tsx index 074a1c40bdb19..18fdd875c7379 100644 --- a/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_header_link.tsx +++ b/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_header_link.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo, useState, useEffect } from 'react'; +import React, { memo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty } from '@elastic/eui'; import type { TutorialDirectoryHeaderLinkComponent } from 'src/plugins/home/public'; @@ -13,25 +13,15 @@ import type { TutorialDirectoryHeaderLinkComponent } from 'src/plugins/home/publ import { RedirectAppLinks } from '../../../../../../src/plugins/kibana_react/public'; import { useLink, useCapabilities, useStartServices } from '../../hooks'; -import { tutorialDirectoryNoticeState$ } from './tutorial_directory_notice'; - const TutorialDirectoryHeaderLink: TutorialDirectoryHeaderLinkComponent = memo(() => { const { getHref } = useLink(); const { application } = useStartServices(); const { show: hasIngestManager } = useCapabilities(); - const [noticeState, setNoticeState] = useState({ + const [noticeState] = useState({ settingsDataLoaded: false, - hasSeenNotice: false, }); - useEffect(() => { - const subscription = tutorialDirectoryNoticeState$.subscribe((value) => setNoticeState(value)); - return () => { - subscription.unsubscribe(); - }; - }, []); - - return hasIngestManager && noticeState.settingsDataLoaded && noticeState.hasSeenNotice ? ( + return hasIngestManager && noticeState.settingsDataLoaded ? ( { - const { getHref } = useLink(); - const { application } = useStartServices(); - const { show: hasIngestManager } = useCapabilities(); - const { data: settingsData, isLoading } = useGetSettings(); - const [dismissedNotice, setDismissedNotice] = useState(false); - - const dismissNotice = useCallback(async () => { - setDismissedNotice(true); - await sendPutSettings({ - has_seen_add_data_notice: true, - }); - }, []); - - useEffect(() => { - tutorialDirectoryNoticeState$.next({ - settingsDataLoaded: !isLoading, - hasSeenNotice: Boolean(dismissedNotice || settingsData?.item?.has_seen_add_data_notice), - }); - }, [isLoading, settingsData, dismissedNotice]); - - const hasSeenNotice = - isLoading || settingsData?.item?.has_seen_add_data_notice || dismissedNotice; - - return hasIngestManager && !hasSeenNotice ? ( - <> - - - - ), - }} - /> - } - > -

- - - - ), - }} - /> -

- - -
- - - - - -
-
- -
- { - dismissNotice(); - }} - > - - -
-
-
-
- - - ) : null; -}); - -// Needed for React.lazy -// eslint-disable-next-line import/no-default-export -export default TutorialDirectoryNotice; diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts index e1f263b0763e8..4a2a6900cc78c 100644 --- a/x-pack/plugins/fleet/public/plugin.ts +++ b/x-pack/plugins/fleet/public/plugin.ts @@ -44,11 +44,7 @@ import { CUSTOM_LOGS_INTEGRATION_NAME, INTEGRATIONS_BASE_PATH } from './constant import { licenseService } from './hooks'; import { setHttpClient } from './hooks/use_request'; import { createPackageSearchProvider } from './search_provider'; -import { - TutorialDirectoryNotice, - TutorialDirectoryHeaderLink, - TutorialModuleNotice, -} from './components/home_integration'; +import { TutorialDirectoryHeaderLink, TutorialModuleNotice } from './components/home_integration'; import { createExtensionRegistrationCallback } from './services/ui_extensions'; import type { UIExtensionRegistrationCallback, UIExtensionsStorage } from './types'; import { LazyCustomLogsAssetsExtension } from './lazy_custom_logs_assets_extension'; @@ -197,7 +193,6 @@ export class FleetPlugin implements Plugin { ]); }; + const setReloadIndicesResponse = (response: HttpResponse = []) => { + server.respondWith('POST', `${API_BASE_PATH}/indices/reload`, [ + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify(response), + ]); + }; + const setLoadDataStreamsResponse = (response: HttpResponse = []) => { server.respondWith('GET', `${API_BASE_PATH}/data_streams`, [ 200, @@ -118,6 +126,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { return { setLoadTemplatesResponse, setLoadIndicesResponse, + setReloadIndicesResponse, setLoadDataStreamsResponse, setLoadDataStreamResponse, setDeleteDataStreamResponse, diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts index 900f7ddbf084b..2576b5f92b7b2 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts @@ -47,9 +47,12 @@ export const setup = async (overridingDependencies: any = {}): Promise { - const { find } = testBed; - const contextMenu = find('indexContextMenu'); - contextMenu.find(`button[data-test-subj="${optionDataTestSubject}"]`).simulate('click'); + const { find, component } = testBed; + + await act(async () => { + find(`indexContextMenu.${optionDataTestSubject}`).simulate('click'); + }); + component.update(); }; const clickIncludeHiddenIndicesToggle = () => { @@ -57,9 +60,13 @@ export const setup = async (overridingDependencies: any = {}): Promise { - const { find } = testBed; - find('indexActionsContextMenuButton').simulate('click'); + const clickManageContextMenuButton = async () => { + const { find, component } = testBed; + + await act(async () => { + find('indexActionsContextMenuButton').simulate('click'); + }); + component.update(); }; const getIncludeHiddenIndicesToggleStatus = () => { diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts index e23c1a59eb135..f95ea373d58b4 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts @@ -10,7 +10,7 @@ import { act } from 'react-dom/test-utils'; import { API_BASE_PATH } from '../../../common/constants'; import { setupEnvironment, nextTick } from '../helpers'; import { IndicesTestBed, setup } from './indices_tab.helpers'; -import { createDataStreamPayload } from './data_streams_tab.helpers'; +import { createDataStreamPayload, createNonDataStreamIndex } from './data_streams_tab.helpers'; /** * The below import is required to avoid a console error warn from the "brace" package @@ -23,9 +23,14 @@ import { createMemoryHistory } from 'history'; stubWebWorker(); // unhandled promise rejection https://github.com/elastic/kibana/issues/112699 -describe.skip('', () => { - const { server, httpRequestsMockHelpers } = setupEnvironment(); +describe('', () => { let testBed: IndicesTestBed; + let server: ReturnType['server']; + let httpRequestsMockHelpers: ReturnType['httpRequestsMockHelpers']; + + beforeEach(() => { + ({ server, httpRequestsMockHelpers } = setupEnvironment()); + }); afterAll(() => { server.restore(); @@ -108,19 +113,9 @@ describe.skip('', () => { describe('index detail panel with % character in index name', () => { const indexName = 'test%'; + beforeEach(async () => { - const index = { - health: 'green', - status: 'open', - primary: 1, - replica: 1, - documents: 10000, - documents_deleted: 100, - size: '156kb', - primary_size: '156kb', - name: indexName, - }; - httpRequestsMockHelpers.setLoadIndicesResponse([index]); + httpRequestsMockHelpers.setLoadIndicesResponse([createNonDataStreamIndex(indexName)]); testBed = await setup(); const { component, find } = testBed; @@ -165,20 +160,11 @@ describe.skip('', () => { describe('index actions', () => { const indexName = 'testIndex'; + beforeEach(async () => { - const index = { - health: 'green', - status: 'open', - primary: 1, - replica: 1, - documents: 10000, - documents_deleted: 100, - size: '156kb', - primary_size: '156kb', - name: indexName, - }; - - httpRequestsMockHelpers.setLoadIndicesResponse([index]); + httpRequestsMockHelpers.setLoadIndicesResponse([createNonDataStreamIndex(indexName)]); + httpRequestsMockHelpers.setReloadIndicesResponse({ indexNames: [indexName] }); + testBed = await setup(); const { find, component } = testBed; component.update(); @@ -188,11 +174,15 @@ describe.skip('', () => { test('should be able to flush index', async () => { const { actions } = testBed; + await actions.clickManageContextMenuButton(); await actions.clickContextMenuOption('flushIndexMenuButton'); - const latestRequest = server.requests[server.requests.length - 1]; - expect(latestRequest.url).toBe(`${API_BASE_PATH}/indices/flush`); + const requestsCount = server.requests.length; + expect(server.requests[requestsCount - 2].url).toBe(`${API_BASE_PATH}/indices/flush`); + // After the indices are flushed, we imediately reload them. So we need to expect to see + // a reload server call also. + expect(server.requests[requestsCount - 1].url).toBe(`${API_BASE_PATH}/indices/reload`); }); }); }); diff --git a/x-pack/plugins/infra/public/pages/logs/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/page_content.tsx index d8b5667e60d04..a82cb55dc7241 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_content.tsx @@ -84,7 +84,7 @@ export const LogsPageContent: React.FunctionComponent = () => { diff --git a/x-pack/plugins/infra/public/pages/logs/page_template.tsx b/x-pack/plugins/infra/public/pages/logs/page_template.tsx index 7ee60ab84bf25..6de13b495f0ba 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_template.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_template.tsx @@ -44,13 +44,13 @@ export const LogsPageTemplate: React.FC = ({ actions: { beats: { title: i18n.translate('xpack.infra.logs.noDataConfig.beatsCard.title', { - defaultMessage: 'Add logs with Beats', + defaultMessage: 'Add a logging integration', }), description: i18n.translate('xpack.infra.logs.noDataConfig.beatsCard.description', { defaultMessage: - 'Use Beats to send logs to Elasticsearch. We make it easy with modules for many popular systems and apps.', + 'Use the Elastic Agent or Beats to send logs to Elasticsearch. We make it easy with integrations for many popular systems and apps.', }), - href: basePath + `/app/home#/tutorial_directory/logging`, + href: basePath + `/app/integrations/browse`, }, }, docsLink: docLinks.links.observability.guide, diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx index bc3bc22f3f1b2..2259a8d3528af 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx @@ -22,8 +22,8 @@ export const LogsPageNoIndicesContent = () => { const canConfigureSource = application?.capabilities?.logs?.configureSource ? true : false; const tutorialLinkProps = useLinkProps({ - app: 'home', - hash: '/tutorial_directory/logging', + app: 'integrations', + hash: '/browse', }); return ( diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index ae375dc504e7a..1a79cd996087d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -93,9 +93,7 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx index 2a436eac30b2c..17e6382ce65cc 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx @@ -18,8 +18,8 @@ interface InvalidNodeErrorProps { export const InvalidNodeError: React.FunctionComponent = ({ nodeName }) => { const tutorialLinkProps = useLinkProps({ - app: 'home', - hash: '/tutorial_directory/metrics', + app: 'integrations', + hash: '/browse', }); return ( diff --git a/x-pack/plugins/infra/public/pages/metrics/page_template.tsx b/x-pack/plugins/infra/public/pages/metrics/page_template.tsx index 41ea12c280841..4da671283644d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/page_template.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/page_template.tsx @@ -10,7 +10,6 @@ import { i18n } from '@kbn/i18n'; import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; import type { LazyObservabilityPageTemplateProps } from '../../../../observability/public'; import { KibanaPageTemplateProps } from '../../../../../../src/plugins/kibana_react/public'; -import { useLinkProps } from '../../hooks/use_link_props'; interface MetricsPageTemplateProps extends LazyObservabilityPageTemplateProps { hasData?: boolean; @@ -30,11 +29,6 @@ export const MetricsPageTemplate: React.FC = ({ }, } = useKibanaContextForPlugin(); - const tutorialLinkProps = useLinkProps({ - app: 'home', - hash: '/tutorial_directory/metrics', - }); - const noDataConfig: KibanaPageTemplateProps['noDataConfig'] = hasData ? undefined : { @@ -44,13 +38,12 @@ export const MetricsPageTemplate: React.FC = ({ actions: { beats: { title: i18n.translate('xpack.infra.metrics.noDataConfig.beatsCard.title', { - defaultMessage: 'Add metrics with Beats', + defaultMessage: 'Add a metrics integration', }), description: i18n.translate('xpack.infra.metrics.noDataConfig.beatsCard.description', { defaultMessage: 'Use Beats to send metrics data to Elasticsearch. We make it easy with modules for many popular systems and apps.', }), - ...tutorialLinkProps, }, }, docsLink: docLinks.links.observability.guide, diff --git a/x-pack/plugins/lists/server/saved_objects/exception_list.ts b/x-pack/plugins/lists/server/saved_objects/exception_list.ts index 8354e64d64a6e..3d31bdd561140 100644 --- a/x-pack/plugins/lists/server/saved_objects/exception_list.ts +++ b/x-pack/plugins/lists/server/saved_objects/exception_list.ts @@ -177,11 +177,12 @@ const combinedMappings: SavedObjectsType['mappings'] = { }; export const exceptionListType: SavedObjectsType = { + convertToMultiNamespaceTypeVersion: '8.0.0', hidden: false, mappings: combinedMappings, migrations, name: exceptionListSavedObjectType, - namespaceType: 'single', + namespaceType: 'multiple-isolated', }; export const exceptionListAgnosticType: SavedObjectsType = { diff --git a/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx b/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx index 8752a4118fb6a..1a44beab260cb 100644 --- a/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx +++ b/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx @@ -50,17 +50,13 @@ const initLegacyShims = () => { ruleTypeRegistry: ruleTypeRegistryMock.create(), }; const data = { query: { timefilter: { timefilter: {} } } } as any; - const ngInjector = {} as angular.auto.IInjectorService; - Legacy.init( - { - core: coreMock.createStart(), - data, - isCloud: false, - triggersActionsUi, - usageCollection: {}, - } as any, - ngInjector - ); + Legacy.init({ + core: coreMock.createStart(), + data, + isCloud: false, + triggersActionsUi, + usageCollection: {}, + } as any); }; const ALERTS_FEATURE_ID = 'alerts'; diff --git a/x-pack/plugins/monitoring/public/angular/angular_i18n/directive.ts b/x-pack/plugins/monitoring/public/angular/angular_i18n/directive.ts deleted file mode 100644 index 1aaff99a6a5c1..0000000000000 --- a/x-pack/plugins/monitoring/public/angular/angular_i18n/directive.ts +++ /dev/null @@ -1,103 +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 { IDirective, IRootElementService, IScope } from 'angular'; - -import { I18nServiceType } from './provider'; - -interface I18nScope extends IScope { - values?: Record; - defaultMessage: string; - id: string; -} - -const HTML_KEY_PREFIX = 'html_'; -const PLACEHOLDER_SEPARATOR = '@I18N@'; - -export const i18nDirective: [string, string, typeof i18nDirectiveFn] = [ - 'i18n', - '$sanitize', - i18nDirectiveFn, -]; - -function i18nDirectiveFn( - i18n: I18nServiceType, - $sanitize: (html: string) => string -): IDirective { - return { - restrict: 'A', - scope: { - id: '@i18nId', - defaultMessage: '@i18nDefaultMessage', - values: ' { - setContent($element, $scope, $sanitize, i18n); - }); - } else { - setContent($element, $scope, $sanitize, i18n); - } - }, - }; -} - -function setContent( - $element: IRootElementService, - $scope: I18nScope, - $sanitize: (html: string) => string, - i18n: I18nServiceType -) { - const originalValues = $scope.values; - const valuesWithPlaceholders = {} as Record; - let hasValuesWithPlaceholders = false; - - // If we have values with the keys that start with HTML_KEY_PREFIX we should replace - // them with special placeholders that later on will be inserted as HTML - // into the DOM, the rest of the content will be treated as text. We don't - // sanitize values at this stage as some of the values can be excluded from - // the translated string (e.g. not used by ICU conditional statements). - if (originalValues) { - for (const [key, value] of Object.entries(originalValues)) { - if (key.startsWith(HTML_KEY_PREFIX)) { - valuesWithPlaceholders[ - key.slice(HTML_KEY_PREFIX.length) - ] = `${PLACEHOLDER_SEPARATOR}${key}${PLACEHOLDER_SEPARATOR}`; - - hasValuesWithPlaceholders = true; - } else { - valuesWithPlaceholders[key] = value; - } - } - } - - const label = i18n($scope.id, { - values: valuesWithPlaceholders, - defaultMessage: $scope.defaultMessage, - }); - - // If there are no placeholders to replace treat everything as text, otherwise - // insert label piece by piece replacing every placeholder with corresponding - // sanitized HTML content. - if (!hasValuesWithPlaceholders) { - $element.text(label); - } else { - $element.empty(); - for (const contentOrPlaceholder of label.split(PLACEHOLDER_SEPARATOR)) { - if (!contentOrPlaceholder) { - continue; - } - - $element.append( - originalValues!.hasOwnProperty(contentOrPlaceholder) - ? $sanitize(originalValues![contentOrPlaceholder]) - : document.createTextNode(contentOrPlaceholder) - ); - } - } -} diff --git a/x-pack/plugins/monitoring/public/angular/angular_i18n/filter.ts b/x-pack/plugins/monitoring/public/angular/angular_i18n/filter.ts deleted file mode 100644 index e4e553fa47b6f..0000000000000 --- a/x-pack/plugins/monitoring/public/angular/angular_i18n/filter.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { I18nServiceType } from './provider'; - -export const i18nFilter: [string, typeof i18nFilterFn] = ['i18n', i18nFilterFn]; - -function i18nFilterFn(i18n: I18nServiceType) { - return (id: string, { defaultMessage = '', values = {} } = {}) => { - return i18n(id, { - values, - defaultMessage, - }); - }; -} diff --git a/x-pack/plugins/monitoring/public/angular/angular_i18n/index.ts b/x-pack/plugins/monitoring/public/angular/angular_i18n/index.ts deleted file mode 100644 index 8915c96e59be0..0000000000000 --- a/x-pack/plugins/monitoring/public/angular/angular_i18n/index.ts +++ /dev/null @@ -1,15 +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 { I18nProvider } from './provider'; - -export { i18nFilter } from './filter'; -export { i18nDirective } from './directive'; - -// re-export types: https://github.com/babel/babel-loader/issues/603 -import { I18nServiceType as _I18nServiceType } from './provider'; -export type I18nServiceType = _I18nServiceType; diff --git a/x-pack/plugins/monitoring/public/angular/angular_i18n/provider.ts b/x-pack/plugins/monitoring/public/angular/angular_i18n/provider.ts deleted file mode 100644 index b1da1bad6e399..0000000000000 --- a/x-pack/plugins/monitoring/public/angular/angular_i18n/provider.ts +++ /dev/null @@ -1,25 +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 type I18nServiceType = ReturnType; - -export class I18nProvider implements angular.IServiceProvider { - public addTranslation = i18n.addTranslation; - public getTranslation = i18n.getTranslation; - public setLocale = i18n.setLocale; - public getLocale = i18n.getLocale; - public setDefaultLocale = i18n.setDefaultLocale; - public getDefaultLocale = i18n.getDefaultLocale; - public setFormats = i18n.setFormats; - public getFormats = i18n.getFormats; - public getRegisteredLocales = i18n.getRegisteredLocales; - public init = i18n.init; - public load = i18n.load; - public $get = () => i18n.translate; -} diff --git a/x-pack/plugins/monitoring/public/angular/helpers/format_angular_http_error.ts b/x-pack/plugins/monitoring/public/angular/helpers/format_angular_http_error.ts deleted file mode 100644 index abdcf157a3c86..0000000000000 --- a/x-pack/plugins/monitoring/public/angular/helpers/format_angular_http_error.ts +++ /dev/null @@ -1,43 +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'; -import type { IHttpResponse } from 'angular'; - -type AngularHttpError = IHttpResponse<{ message: string }>; - -export function isAngularHttpError(error: any): error is AngularHttpError { - return ( - error && - typeof error.status === 'number' && - typeof error.statusText === 'string' && - error.data && - typeof error.data.message === 'string' - ); -} - -export function formatAngularHttpError(error: AngularHttpError) { - // is an Angular $http "error object" - if (error.status === -1) { - // status = -1 indicates that the request was failed to reach the server - return i18n.translate('xpack.monitoring.notify.fatalError.unavailableServerErrorMessage', { - defaultMessage: - 'An HTTP request has failed to connect. ' + - 'Please check if the Kibana server is running and that your browser has a working connection, ' + - 'or contact your system administrator.', - }); - } - - return i18n.translate('xpack.monitoring.notify.fatalError.errorStatusMessage', { - defaultMessage: 'Error {errStatus} {errStatusText}: {errMessage}', - values: { - errStatus: error.status, - errStatusText: error.statusText, - errMessage: error.data.message, - }, - }); -} diff --git a/x-pack/plugins/monitoring/public/angular/top_nav/angular_config.tsx b/x-pack/plugins/monitoring/public/angular/top_nav/angular_config.tsx deleted file mode 100644 index 9c2e931d24a94..0000000000000 --- a/x-pack/plugins/monitoring/public/angular/top_nav/angular_config.tsx +++ /dev/null @@ -1,349 +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 { - ICompileProvider, - IHttpProvider, - IHttpService, - ILocationProvider, - IModule, - IRootScopeService, - IRequestConfig, -} from 'angular'; -import $ from 'jquery'; -import { set } from '@elastic/safer-lodash-set'; -import { get } from 'lodash'; -import * as Rx from 'rxjs'; -import { ChromeBreadcrumb, EnvironmentMode, PackageInfo } from 'kibana/public'; -import { History } from 'history'; - -import { CoreStart } from 'kibana/public'; -import { formatAngularHttpError, isAngularHttpError } from '../helpers/format_angular_http_error'; - -export interface RouteConfiguration { - controller?: string | ((...args: any[]) => void); - redirectTo?: string; - resolveRedirectTo?: (...args: any[]) => void; - reloadOnSearch?: boolean; - reloadOnUrl?: boolean; - outerAngularWrapperRoute?: boolean; - resolve?: object; - template?: string; - k7Breadcrumbs?: (...args: any[]) => ChromeBreadcrumb[]; - requireUICapability?: string; -} - -function isSystemApiRequest(request: IRequestConfig) { - const { headers } = request; - return headers && !!headers['kbn-system-request']; -} - -/** - * Detects whether a given angular route is a dummy route that doesn't - * require any action. There are two ways this can happen: - * If `outerAngularWrapperRoute` is set on the route config object, - * it means the local application service set up this route on the outer angular - * and the internal routes will handle the hooks. - * - * If angular did not detect a route and it is the local angular, we are currently - * navigating away from a URL controlled by a local angular router and the - * application will get unmounted. In this case the outer router will handle - * the hooks. - * @param $route Injected $route dependency - * @param isLocalAngular Flag whether this is the local angular router - */ -function isDummyRoute($route: any, isLocalAngular: boolean) { - return ( - ($route.current && $route.current.$$route && $route.current.$$route.outerAngularWrapperRoute) || - (!$route.current && isLocalAngular) - ); -} - -export const configureAppAngularModule = ( - angularModule: IModule, - newPlatform: { - core: CoreStart; - readonly env: { - mode: Readonly; - packageInfo: Readonly; - }; - }, - isLocalAngular: boolean, - getHistory?: () => History -) => { - const core = 'core' in newPlatform ? newPlatform.core : newPlatform; - const packageInfo = newPlatform.env.packageInfo; - - angularModule - .value('kbnVersion', packageInfo.version) - .value('buildNum', packageInfo.buildNum) - .value('buildSha', packageInfo.buildSha) - .value('esUrl', getEsUrl(core)) - .value('uiCapabilities', core.application.capabilities) - .config(setupCompileProvider(newPlatform.env.mode.dev)) - .config(setupLocationProvider()) - .config($setupXsrfRequestInterceptor(packageInfo.version)) - .run(capture$httpLoadingCount(core)) - .run(digestOnHashChange(getHistory)) - .run($setupBreadcrumbsAutoClear(core, isLocalAngular)) - .run($setupBadgeAutoClear(core, isLocalAngular)) - .run($setupHelpExtensionAutoClear(core, isLocalAngular)) - .run($setupUICapabilityRedirect(core)); -}; - -const getEsUrl = (newPlatform: CoreStart) => { - const a = document.createElement('a'); - a.href = newPlatform.http.basePath.prepend('/elasticsearch'); - const protocolPort = /https/.test(a.protocol) ? 443 : 80; - const port = a.port || protocolPort; - return { - host: a.hostname, - port, - protocol: a.protocol, - pathname: a.pathname, - }; -}; - -const digestOnHashChange = (getHistory?: () => History) => ($rootScope: IRootScopeService) => { - if (!getHistory) return; - const unlisten = getHistory().listen(() => { - // dispatch synthetic hash change event to update hash history objects and angular routing - // this is necessary because hash updates triggered by using popState won't trigger this event naturally. - // this has to happen in the next tick to not change the existing timing of angular digest cycles. - setTimeout(() => { - window.dispatchEvent(new HashChangeEvent('hashchange')); - }, 0); - }); - $rootScope.$on('$destroy', unlisten); -}; - -const setupCompileProvider = (devMode: boolean) => ($compileProvider: ICompileProvider) => { - if (!devMode) { - $compileProvider.debugInfoEnabled(false); - } -}; - -const setupLocationProvider = () => ($locationProvider: ILocationProvider) => { - $locationProvider.html5Mode({ - enabled: false, - requireBase: false, - rewriteLinks: false, - }); - - $locationProvider.hashPrefix(''); -}; - -export const $setupXsrfRequestInterceptor = (version: string) => { - // Configure jQuery prefilter - $.ajaxPrefilter(({ kbnXsrfToken = true }: any, originalOptions, jqXHR) => { - if (kbnXsrfToken) { - jqXHR.setRequestHeader('kbn-version', version); - } - }); - - return ($httpProvider: IHttpProvider) => { - // Configure $httpProvider interceptor - $httpProvider.interceptors.push(() => { - return { - request(opts) { - const { kbnXsrfToken = true } = opts as any; - if (kbnXsrfToken) { - set(opts, ['headers', 'kbn-version'], version); - } - return opts; - }, - }; - }); - }; -}; - -/** - * Injected into angular module by ui/chrome angular integration - * and adds a root-level watcher that will capture the count of - * active $http requests on each digest loop and expose the count to - * the core.loadingCount api - */ -const capture$httpLoadingCount = - (newPlatform: CoreStart) => ($rootScope: IRootScopeService, $http: IHttpService) => { - newPlatform.http.addLoadingCountSource( - new Rx.Observable((observer) => { - const unwatch = $rootScope.$watch(() => { - const reqs = $http.pendingRequests || []; - observer.next(reqs.filter((req) => !isSystemApiRequest(req)).length); - }); - - return unwatch; - }) - ); - }; - -/** - * integrates with angular to automatically redirect to home if required - * capability is not met - */ -const $setupUICapabilityRedirect = - (newPlatform: CoreStart) => ($rootScope: IRootScopeService, $injector: any) => { - const isKibanaAppRoute = window.location.pathname.endsWith('/app/kibana'); - // this feature only works within kibana app for now after everything is - // switched to the application service, this can be changed to handle all - // apps. - if (!isKibanaAppRoute) { - return; - } - $rootScope.$on( - '$routeChangeStart', - (event, { $$route: route }: { $$route?: RouteConfiguration } = {}) => { - if (!route || !route.requireUICapability) { - return; - } - - if (!get(newPlatform.application.capabilities, route.requireUICapability)) { - $injector.get('$location').url('/home'); - event.preventDefault(); - } - } - ); - }; - -/** - * internal angular run function that will be called when angular bootstraps and - * lets us integrate with the angular router so that we can automatically clear - * the breadcrumbs if we switch to a Kibana app that does not use breadcrumbs correctly - */ -const $setupBreadcrumbsAutoClear = - (newPlatform: CoreStart, isLocalAngular: boolean) => - ($rootScope: IRootScopeService, $injector: any) => { - // A flag used to determine if we should automatically - // clear the breadcrumbs between angular route changes. - let breadcrumbSetSinceRouteChange = false; - const $route = $injector.has('$route') ? $injector.get('$route') : {}; - - // reset breadcrumbSetSinceRouteChange any time the breadcrumbs change, even - // if it was done directly through the new platform - newPlatform.chrome.getBreadcrumbs$().subscribe({ - next() { - breadcrumbSetSinceRouteChange = true; - }, - }); - - $rootScope.$on('$routeChangeStart', () => { - breadcrumbSetSinceRouteChange = false; - }); - - $rootScope.$on('$routeChangeSuccess', () => { - if (isDummyRoute($route, isLocalAngular)) { - return; - } - const current = $route.current || {}; - - if (breadcrumbSetSinceRouteChange || (current.$$route && current.$$route.redirectTo)) { - return; - } - - const k7BreadcrumbsProvider = current.k7Breadcrumbs; - if (!k7BreadcrumbsProvider) { - newPlatform.chrome.setBreadcrumbs([]); - return; - } - - try { - newPlatform.chrome.setBreadcrumbs($injector.invoke(k7BreadcrumbsProvider)); - } catch (error) { - if (isAngularHttpError(error)) { - error = formatAngularHttpError(error); - } - newPlatform.fatalErrors.add(error, 'location'); - } - }); - }; - -/** - * internal angular run function that will be called when angular bootstraps and - * lets us integrate with the angular router so that we can automatically clear - * the badge if we switch to a Kibana app that does not use the badge correctly - */ -const $setupBadgeAutoClear = - (newPlatform: CoreStart, isLocalAngular: boolean) => - ($rootScope: IRootScopeService, $injector: any) => { - // A flag used to determine if we should automatically - // clear the badge between angular route changes. - let badgeSetSinceRouteChange = false; - const $route = $injector.has('$route') ? $injector.get('$route') : {}; - - $rootScope.$on('$routeChangeStart', () => { - badgeSetSinceRouteChange = false; - }); - - $rootScope.$on('$routeChangeSuccess', () => { - if (isDummyRoute($route, isLocalAngular)) { - return; - } - const current = $route.current || {}; - - if (badgeSetSinceRouteChange || (current.$$route && current.$$route.redirectTo)) { - return; - } - - const badgeProvider = current.badge; - if (!badgeProvider) { - newPlatform.chrome.setBadge(undefined); - return; - } - - try { - newPlatform.chrome.setBadge($injector.invoke(badgeProvider)); - } catch (error) { - if (isAngularHttpError(error)) { - error = formatAngularHttpError(error); - } - newPlatform.fatalErrors.add(error, 'location'); - } - }); - }; - -/** - * internal angular run function that will be called when angular bootstraps and - * lets us integrate with the angular router so that we can automatically clear - * the helpExtension if we switch to a Kibana app that does not set its own - * helpExtension - */ -const $setupHelpExtensionAutoClear = - (newPlatform: CoreStart, isLocalAngular: boolean) => - ($rootScope: IRootScopeService, $injector: any) => { - /** - * reset helpExtensionSetSinceRouteChange any time the helpExtension changes, even - * if it was done directly through the new platform - */ - let helpExtensionSetSinceRouteChange = false; - newPlatform.chrome.getHelpExtension$().subscribe({ - next() { - helpExtensionSetSinceRouteChange = true; - }, - }); - - const $route = $injector.has('$route') ? $injector.get('$route') : {}; - - $rootScope.$on('$routeChangeStart', () => { - if (isDummyRoute($route, isLocalAngular)) { - return; - } - helpExtensionSetSinceRouteChange = false; - }); - - $rootScope.$on('$routeChangeSuccess', () => { - if (isDummyRoute($route, isLocalAngular)) { - return; - } - const current = $route.current || {}; - - if (helpExtensionSetSinceRouteChange || (current.$$route && current.$$route.redirectTo)) { - return; - } - - newPlatform.chrome.setHelpExtension(current.helpExtension); - }); - }; diff --git a/x-pack/plugins/monitoring/public/angular/top_nav/kbn_top_nav.d.ts b/x-pack/plugins/monitoring/public/angular/top_nav/kbn_top_nav.d.ts deleted file mode 100644 index 0cff77241bb9c..0000000000000 --- a/x-pack/plugins/monitoring/public/angular/top_nav/kbn_top_nav.d.ts +++ /dev/null @@ -1,16 +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 { Injectable, IDirectiveFactory, IScope, IAttributes, IController } from 'angular'; - -export const createTopNavDirective: Injectable< - IDirectiveFactory ->; -export const createTopNavHelper: ( - options: unknown -) => Injectable>; -export function loadKbnTopNavDirectives(navUi: unknown): void; diff --git a/x-pack/plugins/monitoring/public/angular/top_nav/kbn_top_nav.js b/x-pack/plugins/monitoring/public/angular/top_nav/kbn_top_nav.js deleted file mode 100644 index 6edcca6aa714a..0000000000000 --- a/x-pack/plugins/monitoring/public/angular/top_nav/kbn_top_nav.js +++ /dev/null @@ -1,119 +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 angular from 'angular'; -import 'ngreact'; - -export function createTopNavDirective() { - return { - restrict: 'E', - template: '', - compile: (elem) => { - const child = document.createElement('kbn-top-nav-helper'); - - // Copy attributes to the child directive - for (const attr of elem[0].attributes) { - child.setAttribute(attr.name, attr.value); - } - - // Add a special attribute that will change every time that one - // of the config array's disableButton function return value changes. - child.setAttribute('disabled-buttons', 'disabledButtons'); - - // Append helper directive - elem.append(child); - - const linkFn = ($scope, _, $attr) => { - // Watch config changes - $scope.$watch( - () => { - const config = $scope.$eval($attr.config) || []; - return config.map((item) => { - // Copy key into id, as it's a reserved react propery. - // This is done for Angular directive backward compatibility. - // In React only id is recognized. - if (item.key && !item.id) { - item.id = item.key; - } - - // Watch the disableButton functions - if (typeof item.disableButton === 'function') { - return item.disableButton(); - } - return item.disableButton; - }); - }, - (newVal) => { - $scope.disabledButtons = newVal; - }, - true - ); - }; - - return linkFn; - }, - }; -} - -export const createTopNavHelper = - ({ TopNavMenu }) => - (reactDirective) => { - return reactDirective(TopNavMenu, [ - ['config', { watchDepth: 'value' }], - ['setMenuMountPoint', { watchDepth: 'reference' }], - ['disabledButtons', { watchDepth: 'reference' }], - - ['query', { watchDepth: 'reference' }], - ['savedQuery', { watchDepth: 'reference' }], - ['intl', { watchDepth: 'reference' }], - - ['onQuerySubmit', { watchDepth: 'reference' }], - ['onFiltersUpdated', { watchDepth: 'reference' }], - ['onRefreshChange', { watchDepth: 'reference' }], - ['onClearSavedQuery', { watchDepth: 'reference' }], - ['onSaved', { watchDepth: 'reference' }], - ['onSavedQueryUpdated', { watchDepth: 'reference' }], - ['onSavedQueryIdChange', { watchDepth: 'reference' }], - - ['indexPatterns', { watchDepth: 'collection' }], - ['filters', { watchDepth: 'collection' }], - - // All modifiers default to true. - // Set to false to hide subcomponents. - 'showSearchBar', - 'showQueryBar', - 'showQueryInput', - 'showSaveQuery', - 'showDatePicker', - 'showFilterBar', - - 'appName', - 'screenTitle', - 'dateRangeFrom', - 'dateRangeTo', - 'savedQueryId', - 'isRefreshPaused', - 'refreshInterval', - 'disableAutoFocus', - 'showAutoRefreshOnly', - - // temporary flag to use the stateful components - 'useDefaultBehaviors', - ]); - }; - -let isLoaded = false; - -export function loadKbnTopNavDirectives(navUi) { - if (!isLoaded) { - isLoaded = true; - angular - .module('kibana') - .directive('kbnTopNav', createTopNavDirective) - .directive('kbnTopNavHelper', createTopNavHelper(navUi)); - } -} diff --git a/x-pack/plugins/monitoring/public/application/contexts/global_state_context.tsx b/x-pack/plugins/monitoring/public/application/contexts/global_state_context.tsx index e6638b4c4fede..cc8619dbc7ad2 100644 --- a/x-pack/plugins/monitoring/public/application/contexts/global_state_context.tsx +++ b/x-pack/plugins/monitoring/public/application/contexts/global_state_context.tsx @@ -32,31 +32,8 @@ export const GlobalStateProvider: React.FC = ({ toasts, children, }) => { - // TODO: remove fakeAngularRootScope and fakeAngularLocation when angular is removed - const fakeAngularRootScope: Partial = { - $on: - (name: string, listener: (event: ng.IAngularEvent, ...args: any[]) => any): (() => void) => - () => {}, - $applyAsync: () => {}, - }; - - const fakeAngularLocation: Partial = { - search: () => { - return {} as any; - }, - replace: () => { - return {} as any; - }, - }; - const localState: State = {}; - const state = new GlobalState( - query, - toasts, - fakeAngularRootScope, - fakeAngularLocation, - localState as { [key: string]: unknown } - ); + const state = new GlobalState(query, toasts, localState as { [key: string]: unknown }); const initialState: any = state.getState(); for (const key in initialState) { diff --git a/x-pack/plugins/monitoring/public/legacy_shims.ts b/x-pack/plugins/monitoring/public/legacy_shims.ts index 72d50aac1dbb8..48484421839bd 100644 --- a/x-pack/plugins/monitoring/public/legacy_shims.ts +++ b/x-pack/plugins/monitoring/public/legacy_shims.ts @@ -44,7 +44,7 @@ const angularNoop = () => { export interface IShims { toastNotifications: CoreStart['notifications']['toasts']; capabilities: CoreStart['application']['capabilities']; - getAngularInjector: typeof angularNoop | (() => angular.auto.IInjectorService); + getAngularInjector: typeof angularNoop; getBasePath: () => string; getInjected: (name: string, defaultValue?: unknown) => unknown; breadcrumbs: { @@ -73,23 +73,18 @@ export interface IShims { export class Legacy { private static _shims: IShims; - public static init( - { - core, - data, - isCloud, - triggersActionsUi, - usageCollection, - appMountParameters, - }: MonitoringStartPluginDependencies, - ngInjector?: angular.auto.IInjectorService - ) { + public static init({ + core, + data, + isCloud, + triggersActionsUi, + usageCollection, + appMountParameters, + }: MonitoringStartPluginDependencies) { this._shims = { toastNotifications: core.notifications.toasts, capabilities: core.application.capabilities, - getAngularInjector: ngInjector - ? (): angular.auto.IInjectorService => ngInjector - : angularNoop, + getAngularInjector: angularNoop, getBasePath: (): string => core.http.basePath.get(), getInjected: (name: string, defaultValue?: unknown): string | unknown => core.injectedMetadata.getInjectedVar(name, defaultValue), diff --git a/x-pack/plugins/monitoring/public/url_state.ts b/x-pack/plugins/monitoring/public/url_state.ts index 25086411c65a3..8f89df732b800 100644 --- a/x-pack/plugins/monitoring/public/url_state.ts +++ b/x-pack/plugins/monitoring/public/url_state.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { Subscription } from 'rxjs'; import { History, createHashHistory } from 'history'; import { MonitoringStartPluginDependencies } from './types'; @@ -27,10 +26,6 @@ import { withNotifyOnErrors, } from '../../../../src/plugins/kibana_utils/public'; -interface Route { - params: { _g: unknown }; -} - interface RawObject { [key: string]: unknown; } @@ -57,7 +52,6 @@ export interface MonitoringAppStateTransitions { const GLOBAL_STATE_KEY = '_g'; const objectEquals = (objA: any, objB: any) => JSON.stringify(objA) === JSON.stringify(objB); -// TODO: clean all angular references after angular is removed export class GlobalState { private readonly stateSyncRef: ISyncStateRef; private readonly stateContainer: StateContainer< @@ -70,13 +64,10 @@ export class GlobalState { private readonly timefilterRef: MonitoringStartPluginDependencies['data']['query']['timefilter']['timefilter']; private lastAssignedState: MonitoringAppState = {}; - private lastKnownGlobalState?: string; constructor( queryService: MonitoringStartPluginDependencies['data']['query'], toasts: MonitoringStartPluginDependencies['core']['notifications']['toasts'], - rootScope: Partial, - ngLocation: Partial, externalState: RawObject ) { this.timefilterRef = queryService.timefilter.timefilter; @@ -102,9 +93,6 @@ export class GlobalState { this.stateContainerChangeSub = this.stateContainer.state$.subscribe(() => { this.lastAssignedState = this.getState(); - if (!this.stateContainer.get() && this.lastKnownGlobalState) { - ngLocation.search?.(`${GLOBAL_STATE_KEY}=${this.lastKnownGlobalState}`).replace(); - } // TODO: check if this is not needed after https://github.com/elastic/kibana/pull/109132 is merged if (Legacy.isInitializated()) { @@ -112,15 +100,11 @@ export class GlobalState { } this.syncExternalState(externalState); - rootScope.$applyAsync?.(); }); this.syncQueryStateWithUrlManager = syncQueryStateWithUrl(queryService, this.stateStorage); this.stateSyncRef.start(); - this.startHashSync(rootScope, ngLocation); this.lastAssignedState = this.getState(); - - rootScope.$on?.('$destroy', () => this.destroy()); } private syncExternalState(externalState: { [key: string]: unknown }) { @@ -137,24 +121,6 @@ export class GlobalState { } } - private startHashSync( - rootScope: Partial, - ngLocation: Partial - ) { - rootScope.$on?.( - '$routeChangeStart', - (_: { preventDefault: () => void }, newState: Route, oldState: Route) => { - const currentGlobalState = oldState?.params?._g; - const nextGlobalState = newState?.params?._g; - if (!nextGlobalState && currentGlobalState && typeof currentGlobalState === 'string') { - newState.params._g = currentGlobalState; - ngLocation.search?.(`${GLOBAL_STATE_KEY}=${currentGlobalState}`).replace(); - } - this.lastKnownGlobalState = (nextGlobalState || currentGlobalState) as string; - } - ); - } - public setState(state?: { [key: string]: unknown }) { const currentAppState = this.getState(); const newAppState = { ...currentAppState, ...state }; diff --git a/x-pack/plugins/observability/public/components/app/header/header_menu.tsx b/x-pack/plugins/observability/public/components/app/header/header_menu.tsx index 707cb241501fd..0ed01b7d3673e 100644 --- a/x-pack/plugins/observability/public/components/app/header/header_menu.tsx +++ b/x-pack/plugins/observability/public/components/app/header/header_menu.tsx @@ -26,7 +26,7 @@ export function ObservabilityHeaderMenu(): React.ReactElement | null { {addDataLinkText} diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx index af91624769e6b..d852d6fdb9a31 100644 --- a/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx +++ b/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx @@ -16,9 +16,9 @@ export function SyntheticsAddData() { return ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts index e4473b183d729..c12e67bc9b1ae 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts @@ -49,7 +49,9 @@ import { MONITORS_DURATION_LABEL, PAGE_LOAD_TIME_LABEL, LABELS_FIELD, + STEP_NAME_LABEL, } from './labels'; +import { SYNTHETICS_STEP_NAME } from './field_names/synthetics'; export const DEFAULT_TIME = { from: 'now-1h', to: 'now' }; @@ -77,6 +79,7 @@ export const FieldLabels: Record = { 'monitor.id': MONITOR_ID_LABEL, 'monitor.status': MONITOR_STATUS_LABEL, 'monitor.duration.us': MONITORS_DURATION_LABEL, + [SYNTHETICS_STEP_NAME]: STEP_NAME_LABEL, 'agent.hostname': AGENT_HOST_LABEL, 'host.hostname': HOST_NAME_LABEL, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/field_names/synthetics.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/field_names/synthetics.ts index eff73d242de75..0f28648552728 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/field_names/synthetics.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/field_names/synthetics.ts @@ -11,3 +11,5 @@ export const SYNTHETICS_LCP = 'browser.experience.lcp.us'; export const SYNTHETICS_FCP = 'browser.experience.fcp.us'; export const SYNTHETICS_DOCUMENT_ONLOAD = 'browser.experience.load.us'; export const SYNTHETICS_DCL = 'browser.experience.dcl.us'; +export const SYNTHETICS_STEP_NAME = 'synthetics.step.name.keyword'; +export const SYNTHETICS_STEP_DURATION = 'synthetics.step.duration.us'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts index cdaa89fc71389..599f846af2ff9 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts @@ -231,6 +231,20 @@ export const MONITORS_DURATION_LABEL = i18n.translate( } ); +export const STEP_DURATION_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.stepDurationLabel', + { + defaultMessage: 'Step duration', + } +); + +export const STEP_NAME_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.stepNameLabel', + { + defaultMessage: 'Step name', + } +); + export const WEB_APPLICATION_LABEL = i18n.translate( 'xpack.observability.expView.fieldLabels.webApplication', { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index 38c9ecc06491d..a31bef7c9c214 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -194,10 +194,12 @@ export class LensAttributes { label, sourceField, columnType, + columnFilter, operationType, }: { sourceField: string; columnType?: string; + columnFilter?: ColumnFilter; operationType?: string; label?: string; seriesConfig: SeriesConfig; @@ -214,6 +216,7 @@ export class LensAttributes { operationType, label, seriesConfig, + columnFilter, }); } if (operationType?.includes('th')) { @@ -228,11 +231,13 @@ export class LensAttributes { label, seriesConfig, operationType, + columnFilter, }: { sourceField: string; operationType: 'average' | 'median' | 'sum' | 'unique_count'; label?: string; seriesConfig: SeriesConfig; + columnFilter?: ColumnFilter; }): | AvgIndexPatternColumn | MedianIndexPatternColumn @@ -247,6 +252,7 @@ export class LensAttributes { operationType: capitalize(operationType), }, }), + filter: columnFilter, operationType, }; } @@ -391,6 +397,7 @@ export class LensAttributes { return this.getNumberColumn({ sourceField: fieldName, columnType, + columnFilter: columnFilters?.[0], operationType, label: columnLabel || label, seriesConfig: layerConfig.seriesConfig, @@ -447,10 +454,10 @@ export class LensAttributes { return this.getColumnBasedOnType({ sourceField, - operationType: breakdown === PERCENTILE ? PERCENTILE_RANKS[0] : operationType, label, layerConfig, colIndex: 0, + operationType: breakdown === PERCENTILE ? PERCENTILE_RANKS[0] : operationType, }); } @@ -629,7 +636,12 @@ export class LensAttributes { [`y-axis-column-${layerId}`]: { ...mainYAxis, label, - filter: { query: columnFilter, language: 'kuery' }, + filter: { + query: mainYAxis.filter + ? `${columnFilter} and ${mainYAxis.filter.query}` + : columnFilter, + language: 'kuery', + }, ...(timeShift ? { timeShift } : {}), }, ...(breakdown && sourceField !== USE_BREAK_DOWN_COLUMN && breakdown !== PERCENTILE diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts index da90f45d15201..fb44da8e4327f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts @@ -29,6 +29,7 @@ import { SYNTHETICS_FCP, SYNTHETICS_LCP, } from '../constants/field_names/synthetics'; +import { buildExistsFilter } from '../utils'; export function getSyntheticsDistributionConfig({ series, @@ -58,7 +59,10 @@ export function getSyntheticsDistributionConfig({ 'url.port', ], baseFilters: [], - definitionFields: ['monitor.name', 'url.full'], + definitionFields: [ + { field: 'monitor.name', nested: 'synthetics.step.name.keyword', singleSelection: true }, + { field: 'url.full', filters: buildExistsFilter('summary.up', indexPattern) }, + ], metricOptions: [ { label: MONITORS_DURATION_LABEL, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts index 5f8a6a28ca81d..f3e3fe0817845 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts @@ -11,6 +11,7 @@ import { SYNTHETICS_DOCUMENT_ONLOAD, SYNTHETICS_FCP, SYNTHETICS_LCP, + SYNTHETICS_STEP_DURATION, } from '../constants/field_names/synthetics'; export const syntheticsFieldFormats: FieldFormat[] = [ @@ -27,6 +28,19 @@ export const syntheticsFieldFormats: FieldFormat[] = [ }, }, }, + { + field: SYNTHETICS_STEP_DURATION, + format: { + id: 'duration', + params: { + inputFormat: 'microseconds', + outputFormat: 'humanizePrecise', + outputPrecision: 1, + showSuffix: true, + useShortSuffix: true, + }, + }, + }, { field: SYNTHETICS_LCP, format: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts index 6df9cdcd0503a..8951ffcda63d8 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ConfigProps, SeriesConfig } from '../../types'; +import { ColumnFilter, ConfigProps, SeriesConfig } from '../../types'; import { FieldLabels, OPERATION_COLUMN, @@ -21,6 +21,7 @@ import { FCP_LABEL, LCP_LABEL, MONITORS_DURATION_LABEL, + STEP_DURATION_LABEL, UP_LABEL, } from '../constants/labels'; import { @@ -30,10 +31,26 @@ import { SYNTHETICS_DOCUMENT_ONLOAD, SYNTHETICS_FCP, SYNTHETICS_LCP, + SYNTHETICS_STEP_DURATION, + SYNTHETICS_STEP_NAME, } from '../constants/field_names/synthetics'; +import { buildExistsFilter } from '../utils'; const SUMMARY_UP = 'summary.up'; const SUMMARY_DOWN = 'summary.down'; +export const isStepLevelMetric = (metric?: string) => { + if (!metric) { + return false; + } + return [ + SYNTHETICS_LCP, + SYNTHETICS_FCP, + SYNTHETICS_CLS, + SYNTHETICS_DCL, + SYNTHETICS_STEP_DURATION, + SYNTHETICS_DOCUMENT_ONLOAD, + ].includes(metric); +}; export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): SeriesConfig { return { reportType: ReportTypes.KPI, @@ -50,10 +67,19 @@ export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): SeriesCon ], hasOperationType: false, filterFields: ['observer.geo.name', 'monitor.type', 'tags'], - breakdownFields: ['observer.geo.name', 'monitor.type', 'monitor.name', PERCENTILE], + breakdownFields: [ + 'observer.geo.name', + 'monitor.type', + 'monitor.name', + SYNTHETICS_STEP_NAME, + PERCENTILE, + ], baseFilters: [], palette: { type: 'palette', name: 'status' }, - definitionFields: ['monitor.name', 'url.full'], + definitionFields: [ + { field: 'monitor.name', nested: SYNTHETICS_STEP_NAME, singleSelection: true }, + { field: 'url.full', filters: buildExistsFilter('summary.up', indexPattern) }, + ], metricOptions: [ { label: MONITORS_DURATION_LABEL, @@ -73,37 +99,59 @@ export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): SeriesCon label: DOWN_LABEL, columnType: OPERATION_COLUMN, }, + { + label: STEP_DURATION_LABEL, + field: SYNTHETICS_STEP_DURATION, + id: SYNTHETICS_STEP_DURATION, + columnType: OPERATION_COLUMN, + columnFilters: [STEP_END_FILTER], + }, { label: LCP_LABEL, field: SYNTHETICS_LCP, id: SYNTHETICS_LCP, columnType: OPERATION_COLUMN, + columnFilters: [STEP_METRIC_FILTER], }, { label: FCP_LABEL, field: SYNTHETICS_FCP, id: SYNTHETICS_FCP, columnType: OPERATION_COLUMN, + columnFilters: [STEP_METRIC_FILTER], }, { label: DCL_LABEL, field: SYNTHETICS_DCL, id: SYNTHETICS_DCL, columnType: OPERATION_COLUMN, + columnFilters: [STEP_METRIC_FILTER], }, { label: DOCUMENT_ONLOAD_LABEL, field: SYNTHETICS_DOCUMENT_ONLOAD, id: SYNTHETICS_DOCUMENT_ONLOAD, columnType: OPERATION_COLUMN, + columnFilters: [STEP_METRIC_FILTER], }, { label: CLS_LABEL, field: SYNTHETICS_CLS, id: SYNTHETICS_CLS, columnType: OPERATION_COLUMN, + columnFilters: [STEP_METRIC_FILTER], }, ], labels: { ...FieldLabels, [SUMMARY_UP]: UP_LABEL, [SUMMARY_DOWN]: DOWN_LABEL }, }; } + +const STEP_METRIC_FILTER: ColumnFilter = { + language: 'kuery', + query: `synthetics.type: step/metrics`, +}; + +const STEP_END_FILTER: ColumnFilter = { + language: 'kuery', + query: `synthetics.type: step/end`, +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts index adc6d4bb14462..4563509eeb19a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts @@ -77,7 +77,8 @@ export const sampleAttributeCoreWebVital = { dataType: 'number', filter: { language: 'kuery', - query: 'transaction.type: page-load and processor.event: transaction', + query: + 'transaction.type: page-load and processor.event: transaction and transaction.marks.agent.largestContentfulPaint < 2500', }, isBucketed: false, label: 'Good', diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.tsx index a235cbd8852ad..f30a80f87ebb7 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import styled from 'styled-components'; import { EuiSuperSelect, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -17,6 +17,8 @@ import { PERCENTILE, } from '../../configurations/constants'; import { SeriesConfig, SeriesUrl } from '../../types'; +import { SYNTHETICS_STEP_NAME } from '../../configurations/constants/field_names/synthetics'; +import { isStepLevelMetric } from '../../configurations/synthetics/kpi_over_time_config'; interface Props { seriesId: number; @@ -51,6 +53,18 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) { } }; + useEffect(() => { + if ( + !isStepLevelMetric(series.selectedMetricField) && + selectedBreakdown === SYNTHETICS_STEP_NAME + ) { + setSeries(seriesId, { + ...series, + breakdown: undefined, + }); + } + }); + if (!seriesConfig) { return null; } @@ -71,11 +85,26 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) { } const options = items - .map(({ id, label }) => ({ - inputDisplay: label, - value: id, - dropdownDisplay: label, - })) + .map(({ id, label }) => { + if (id === SYNTHETICS_STEP_NAME && !isStepLevelMetric(series.selectedMetricField)) { + return { + inputDisplay: label, + value: id, + dropdownDisplay: ( + + <>{label} + + ), + disabled: true, + }; + } else { + return { + inputDisplay: label, + value: id, + dropdownDisplay: label, + }; + } + }) .filter(({ value }) => !(value === PERCENTILE && isRecordsMetric)); let valueOfSelected = @@ -121,6 +150,14 @@ export const BREAKDOWN_WARNING = i18n.translate('xpack.observability.exp.breakDo defaultMessage: 'Breakdowns can be applied to only one series at a time.', }); +export const BREAKDOWN_UNAVAILABLE = i18n.translate( + 'xpack.observability.exp.breakDownFilter.unavailable', + { + defaultMessage: + 'Step name breakdown is not available for monitor duration metric. Use step duration metric to breakdown by step name.', + } +); + const Wrapper = styled.span` .euiToolTipAnchor { width: 100%; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/incomplete_badge.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/incomplete_badge.tsx index 4e1c385921908..8e64f4bcea680 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/incomplete_badge.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/incomplete_badge.tsx @@ -31,7 +31,14 @@ export function IncompleteBadge({ seriesConfig, series }: Props) { const incompleteDefinition = isEmpty(reportDefinitions) ? i18n.translate('xpack.observability.overview.exploratoryView.missingReportDefinition', { defaultMessage: 'Missing {reportDefinition}', - values: { reportDefinition: labels?.[definitionFields[0]] }, + values: { + reportDefinition: + labels?.[ + typeof definitionFields[0] === 'string' + ? definitionFields[0] + : definitionFields[0].field + ], + }, }) : ''; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx index fbd7c34303d94..a665ec1999133 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx @@ -6,10 +6,13 @@ */ import React from 'react'; +import { isEmpty } from 'lodash'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { SeriesConfig, SeriesUrl } from '../../types'; import { ReportDefinitionField } from './report_definition_field'; +import { isStepLevelMetric } from '../../configurations/synthetics/kpi_over_time_config'; +import { SYNTHETICS_STEP_NAME } from '../../configurations/constants/field_names/synthetics'; export function ReportDefinitionCol({ seriesId, @@ -41,19 +44,64 @@ export function ReportDefinitionCol({ } }; + const hasFieldDataSelected = (field: string) => { + return !isEmpty(series.reportDefinitions?.[field]); + }; + return ( - {definitionFields.map((field) => ( - - - - ))} + {definitionFields.map((field) => { + const fieldStr = typeof field === 'string' ? field : field.field; + const singleSelection = typeof field !== 'string' && field.singleSelection; + const nestedField = typeof field !== 'string' && field.nested; + const filters = typeof field !== 'string' ? field.filters : undefined; + + const isNonStepMetric = !isStepLevelMetric(series.selectedMetricField); + + const hideNestedStep = nestedField === SYNTHETICS_STEP_NAME && isNonStepMetric; + + if (hideNestedStep && nestedField && selectedReportDefinitions[nestedField]?.length > 0) { + setSeries(seriesId, { + ...series, + reportDefinitions: { ...selectedReportDefinitions, [nestedField]: [] }, + }); + } + + let nestedFieldElement; + + if (nestedField && hasFieldDataSelected(fieldStr) && !hideNestedStep) { + nestedFieldElement = ( + + + + ); + } + + return ( + <> + + + + {nestedFieldElement} + + ); + })} ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx index 01f36e85c03ae..f3e0eb767d336 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx @@ -7,7 +7,7 @@ import React, { useMemo } from 'react'; import { isEmpty } from 'lodash'; -import { ExistsFilter } from '@kbn/es-query'; +import { ExistsFilter, PhraseFilter } from '@kbn/es-query'; import FieldValueSuggestions from '../../../field_value_suggestions'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearch'; @@ -19,32 +19,49 @@ import { ALL_VALUES_SELECTED } from '../../../field_value_suggestions/field_valu interface Props { seriesId: number; series: SeriesUrl; - field: string; + singleSelection?: boolean; + keepHistory?: boolean; + field: string | { field: string; nested: string }; seriesConfig: SeriesConfig; onChange: (field: string, value?: string[]) => void; + filters?: Array; } -export function ReportDefinitionField({ series, field, seriesConfig, onChange }: Props) { +export function ReportDefinitionField({ + singleSelection, + keepHistory, + series, + field: fieldProp, + seriesConfig, + onChange, + filters, +}: Props) { const { indexPattern } = useAppIndexPatternContext(series.dataType); + const field = typeof fieldProp === 'string' ? fieldProp : fieldProp.field; + const { reportDefinitions: selectedReportDefinitions = {} } = series; const { labels, baseFilters, definitionFields } = seriesConfig; const queryFilters = useMemo(() => { const filtersN: ESFilter[] = []; - (baseFilters ?? []).forEach((qFilter: PersistableFilter | ExistsFilter) => { - if (qFilter.query) { - filtersN.push(qFilter.query); - } - const existFilter = qFilter as ExistsFilter; - if (existFilter.query.exists) { - filtersN.push({ exists: existFilter.query.exists }); - } - }); + (baseFilters ?? []) + .concat(filters ?? []) + .forEach((qFilter: PersistableFilter | ExistsFilter) => { + if (qFilter.query) { + filtersN.push(qFilter.query); + } + const existFilter = qFilter as ExistsFilter; + if (existFilter.query.exists) { + filtersN.push({ exists: existFilter.query.exists }); + } + }); if (!isEmpty(selectedReportDefinitions)) { - definitionFields.forEach((fieldT) => { + definitionFields.forEach((fieldObj) => { + const fieldT = typeof fieldObj === 'string' ? fieldObj : fieldObj.field; + if (indexPattern && selectedReportDefinitions?.[fieldT] && fieldT !== field) { const values = selectedReportDefinitions?.[fieldT]; if (!values.includes(ALL_VALUES_SELECTED)) { @@ -65,7 +82,7 @@ export function ReportDefinitionField({ series, field, seriesConfig, onChange }: return ( ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx index 180be1ac0414f..12e0ceca20649 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx @@ -48,13 +48,13 @@ export function ExpandedSeriesRow(seriesProps: Props) { return (
- - + + - + diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts index 001664cf12783..acd49fc25588e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -56,7 +56,15 @@ export interface SeriesConfig { filterFields: Array; seriesTypes: SeriesType[]; baseFilters?: Array; - definitionFields: string[]; + definitionFields: Array< + | string + | { + field: string; + nested?: string; + singleSelection?: boolean; + filters?: Array; + } + >; metricOptions?: MetricOption[]; labels: Record; hasOperationType: boolean; diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx index 0735df53888aa..e04d5463d5494 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx @@ -42,6 +42,7 @@ export function FieldValueCombobox({ usePrependLabel = true, compressed = true, required = true, + singleSelection = false, allowAllValuesSelection, onChange: onSelectionChange, }: FieldValueSelectionProps) { @@ -68,6 +69,7 @@ export function FieldValueCombobox({ const comboBox = ( ); diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts index 6f6d520a83154..95b24aa69b1e7 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts @@ -29,6 +29,7 @@ interface CommonProps { allowAllValuesSelection?: boolean; cardinalityField?: string; required?: boolean; + keepHistory?: boolean; } export type FieldValueSuggestionsProps = CommonProps & { diff --git a/x-pack/plugins/observability/public/pages/overview/empty_section.ts b/x-pack/plugins/observability/public/pages/overview/empty_section.ts index 2747b2ecdebc9..f249a820a60a4 100644 --- a/x-pack/plugins/observability/public/pages/overview/empty_section.ts +++ b/x-pack/plugins/observability/public/pages/overview/empty_section.ts @@ -69,7 +69,7 @@ export const getEmptySections = ({ core }: { core: CoreStart }): ISection[] => { linkTitle: i18n.translate('xpack.observability.emptySection.apps.uptime.link', { defaultMessage: 'Install Heartbeat', }), - href: core.http.basePath.prepend('/app/home#/tutorial/uptimeMonitors'), + href: core.http.basePath.prepend('/app/integrations/detail/synthetics/overview'), }, { id: 'ux', diff --git a/x-pack/plugins/observability/public/utils/no_data_config.ts b/x-pack/plugins/observability/public/utils/no_data_config.ts index 1e16fb145bdce..2c87b1434a0b4 100644 --- a/x-pack/plugins/observability/public/utils/no_data_config.ts +++ b/x-pack/plugins/observability/public/utils/no_data_config.ts @@ -24,12 +24,15 @@ export function getNoDataConfig({ defaultMessage: 'Observability', }), actions: { - beats: { + elasticAgent: { + title: i18n.translate('xpack.observability.noDataConfig.beatsCard.title', { + defaultMessage: 'Add integrations', + }), description: i18n.translate('xpack.observability.noDataConfig.beatsCard.description', { defaultMessage: 'Use Beats and APM agents to send observability data to Elasticsearch. We make it easy with support for many popular systems, apps, and languages.', }), - href: basePath.prepend(`/app/home#/tutorial_directory/logging`), + href: basePath.prepend(`/app/integrations`), }, }, docsLink, diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 5c41e92661e58..5a7e19e2cdd05 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -16,7 +16,7 @@ export const APP_NAME = 'Security'; export const APP_ICON = 'securityAnalyticsApp'; export const APP_ICON_SOLUTION = 'logoSecurity'; export const APP_PATH = `/app/security`; -export const ADD_DATA_PATH = `/app/home#/tutorial_directory/security`; +export const ADD_DATA_PATH = `/app/integrations/browse/security`; export const DEFAULT_BYTES_FORMAT = 'format:bytes:defaultPattern'; export const DEFAULT_DATE_FORMAT = 'dateFormat'; export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz'; diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index 6e9123da2dd9b..178a2b68a4aab 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -10,7 +10,7 @@ export const ENDPOINT_ACTIONS_DS = '.logs-endpoint.actions'; export const ENDPOINT_ACTIONS_INDEX = `${ENDPOINT_ACTIONS_DS}-default`; export const ENDPOINT_ACTION_RESPONSES_DS = '.logs-endpoint.action.responses'; -export const ENDPOINT_ACTION_RESPONSES_INDEX = `${ENDPOINT_ACTIONS_DS}-default`; +export const ENDPOINT_ACTION_RESPONSES_INDEX = `${ENDPOINT_ACTION_RESPONSES_DS}-default`; export const eventsIndexPattern = 'logs-endpoint.events.*'; export const alertsIndexPattern = 'logs-endpoint.alerts-*'; @@ -60,3 +60,5 @@ export const UNISOLATE_HOST_ROUTE = `${BASE_ENDPOINT_ROUTE}/unisolate`; /** Endpoint Actions Log Routes */ export const ENDPOINT_ACTION_LOG_ROUTE = `/api/endpoint/action_log/{agent_id}`; export const ACTION_STATUS_ROUTE = `/api/endpoint/action_status`; + +export const failedFleetActionErrorCode = '424'; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index bc46ca2f5b451..fb29297eb5929 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -10,6 +10,13 @@ import { ActionStatusRequestSchema, HostIsolationRequestSchema } from '../schema export type ISOLATION_ACTIONS = 'isolate' | 'unisolate'; +export const ActivityLogItemTypes = { + ACTION: 'action' as const, + RESPONSE: 'response' as const, + FLEET_ACTION: 'fleetAction' as const, + FLEET_RESPONSE: 'fleetResponse' as const, +}; + interface EcsError { code?: string; id?: string; @@ -87,8 +94,24 @@ export interface EndpointActionResponse { action_data: EndpointActionData; } +export interface EndpointActivityLogAction { + type: typeof ActivityLogItemTypes.ACTION; + item: { + id: string; + data: LogsEndpointAction; + }; +} + +export interface EndpointActivityLogActionResponse { + type: typeof ActivityLogItemTypes.RESPONSE; + item: { + id: string; + data: LogsEndpointActionResponse; + }; +} + export interface ActivityLogAction { - type: 'action'; + type: typeof ActivityLogItemTypes.FLEET_ACTION; item: { // document _id id: string; @@ -97,7 +120,7 @@ export interface ActivityLogAction { }; } export interface ActivityLogActionResponse { - type: 'response'; + type: typeof ActivityLogItemTypes.FLEET_RESPONSE; item: { // document id id: string; @@ -105,7 +128,11 @@ export interface ActivityLogActionResponse { data: EndpointActionResponse; }; } -export type ActivityLogEntry = ActivityLogAction | ActivityLogActionResponse; +export type ActivityLogEntry = + | ActivityLogAction + | ActivityLogActionResponse + | EndpointActivityLogAction + | EndpointActivityLogActionResponse; export interface ActivityLog { page: number; pageSize: number; diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts index 287d86c6fba9e..69b623de0b43c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts @@ -20,7 +20,8 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { CASES_URL } from '../../urls/navigation'; -describe('Cases connectors', () => { +// Skipping flakey test: https://github.com/elastic/kibana/issues/115438 +describe.skip('Cases connectors', () => { const configureResult = { connector: { id: 'e271c3b8-f702-4fbc-98e0-db942b573bbd', diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/use_endpoint_privileges.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/use_endpoint_privileges.ts deleted file mode 100644 index 80ca534534187..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/use_endpoint_privileges.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EndpointPrivileges } from '../use_endpoint_privileges'; - -export const useEndpointPrivileges = jest.fn(() => { - const endpointPrivilegesMock: EndpointPrivileges = { - loading: false, - canAccessFleet: true, - canAccessEndpointManagement: true, - isPlatinumPlus: true, - }; - return endpointPrivilegesMock; -}); diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/__mocks__/use_endpoint_privileges.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/__mocks__/use_endpoint_privileges.ts new file mode 100644 index 0000000000000..ae9aacaf3d55b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/__mocks__/use_endpoint_privileges.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getEndpointPrivilegesInitialStateMock } from '../mocks'; + +export { getEndpointPrivilegesInitialState } from '../utils'; + +export const useEndpointPrivileges = jest.fn(getEndpointPrivilegesInitialStateMock); diff --git a/x-pack/plugins/monitoring/public/angular/top_nav/index.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/index.ts similarity index 63% rename from x-pack/plugins/monitoring/public/angular/top_nav/index.ts rename to x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/index.ts index b3501e4cbad1f..adea89ce1a051 100644 --- a/x-pack/plugins/monitoring/public/angular/top_nav/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/index.ts @@ -5,6 +5,5 @@ * 2.0. */ -export * from './angular_config'; -// @ts-ignore -export { createTopNavDirective, createTopNavHelper, loadKbnTopNavDirectives } from './kbn_top_nav'; +export * from './use_endpoint_privileges'; +export { getEndpointPrivilegesInitialState } from './utils'; diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/mocks.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/mocks.ts new file mode 100644 index 0000000000000..2851c92816cea --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/mocks.ts @@ -0,0 +1,29 @@ +/* + * 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 { EndpointPrivileges } from './use_endpoint_privileges'; +import { getEndpointPrivilegesInitialState } from './utils'; + +export const getEndpointPrivilegesInitialStateMock = ( + overrides: Partial = {} +): EndpointPrivileges => { + // Get the initial state and set all permissions to `true` (enabled) for testing + const endpointPrivilegesMock: EndpointPrivileges = { + ...( + Object.entries(getEndpointPrivilegesInitialState()) as Array< + [keyof EndpointPrivileges, boolean] + > + ).reduce((mockPrivileges, [key, value]) => { + mockPrivileges[key] = !value; + + return mockPrivileges; + }, {} as EndpointPrivileges), + ...overrides, + }; + + return endpointPrivilegesMock; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.test.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts similarity index 63% rename from x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.test.ts rename to x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts index 82443e913499b..d4ba29a4ef950 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts @@ -6,16 +6,17 @@ */ import { act, renderHook, RenderHookResult, RenderResult } from '@testing-library/react-hooks'; -import { useHttp, useCurrentUser } from '../../lib/kibana'; +import { useHttp, useCurrentUser } from '../../../lib/kibana'; import { EndpointPrivileges, useEndpointPrivileges } from './use_endpoint_privileges'; -import { securityMock } from '../../../../../security/public/mocks'; -import { appRoutesService } from '../../../../../fleet/common'; -import { AuthenticatedUser } from '../../../../../security/common'; -import { licenseService } from '../../hooks/use_license'; -import { fleetGetCheckPermissionsHttpMock } from '../../../management/pages/mocks'; - -jest.mock('../../lib/kibana'); -jest.mock('../../hooks/use_license', () => { +import { securityMock } from '../../../../../../security/public/mocks'; +import { appRoutesService } from '../../../../../../fleet/common'; +import { AuthenticatedUser } from '../../../../../../security/common'; +import { licenseService } from '../../../hooks/use_license'; +import { fleetGetCheckPermissionsHttpMock } from '../../../../management/pages/mocks'; +import { getEndpointPrivilegesInitialStateMock } from './mocks'; + +jest.mock('../../../lib/kibana'); +jest.mock('../../../hooks/use_license', () => { const licenseServiceInstance = { isPlatinumPlus: jest.fn(), }; @@ -27,6 +28,8 @@ jest.mock('../../hooks/use_license', () => { }; }); +const licenseServiceMock = licenseService as jest.Mocked; + describe('When using useEndpointPrivileges hook', () => { let authenticatedUser: AuthenticatedUser; let fleetApiMock: ReturnType; @@ -45,7 +48,7 @@ describe('When using useEndpointPrivileges hook', () => { fleetApiMock = fleetGetCheckPermissionsHttpMock( useHttp() as Parameters[0] ); - (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(true); + licenseServiceMock.isPlatinumPlus.mockReturnValue(true); render = () => { const hookRenderResponse = renderHook(() => useEndpointPrivileges()); @@ -69,34 +72,31 @@ describe('When using useEndpointPrivileges hook', () => { (useCurrentUser as jest.Mock).mockReturnValue(null); const { rerender } = render(); - expect(result.current).toEqual({ - canAccessEndpointManagement: false, - canAccessFleet: false, - loading: true, - isPlatinumPlus: true, - }); + expect(result.current).toEqual( + getEndpointPrivilegesInitialStateMock({ + canAccessEndpointManagement: false, + canAccessFleet: false, + loading: true, + }) + ); // Make user service available (useCurrentUser as jest.Mock).mockReturnValue(authenticatedUser); rerender(); - expect(result.current).toEqual({ - canAccessEndpointManagement: false, - canAccessFleet: false, - loading: true, - isPlatinumPlus: true, - }); + expect(result.current).toEqual( + getEndpointPrivilegesInitialStateMock({ + canAccessEndpointManagement: false, + canAccessFleet: false, + loading: true, + }) + ); // Release the API response await act(async () => { fleetApiMock.waitForApi(); releaseApiResponse!(); }); - expect(result.current).toEqual({ - canAccessEndpointManagement: true, - canAccessFleet: true, - loading: false, - isPlatinumPlus: true, - }); + expect(result.current).toEqual(getEndpointPrivilegesInitialStateMock()); }); it('should call Fleet permissions api to determine user privilege to fleet', async () => { @@ -113,12 +113,11 @@ describe('When using useEndpointPrivileges hook', () => { render(); await waitForNextUpdate(); await fleetApiMock.waitForApi(); - expect(result.current).toEqual({ - canAccessEndpointManagement: false, - canAccessFleet: true, // this is only true here because I did not adjust the API mock - loading: false, - isPlatinumPlus: true, - }); + expect(result.current).toEqual( + getEndpointPrivilegesInitialStateMock({ + canAccessEndpointManagement: false, + }) + ); }); it('should set privileges to false if fleet api check returns failure', async () => { @@ -130,11 +129,21 @@ describe('When using useEndpointPrivileges hook', () => { render(); await waitForNextUpdate(); await fleetApiMock.waitForApi(); - expect(result.current).toEqual({ - canAccessEndpointManagement: false, - canAccessFleet: false, - loading: false, - isPlatinumPlus: true, - }); + expect(result.current).toEqual( + getEndpointPrivilegesInitialStateMock({ + canAccessEndpointManagement: false, + canAccessFleet: false, + }) + ); }); + + it.each([['canIsolateHost'], ['canCreateArtifactsByPolicy']])( + 'should set %s to false if license is not PlatinumPlus', + async (privilege) => { + licenseServiceMock.isPlatinumPlus.mockReturnValue(false); + render(); + await waitForNextUpdate(); + expect(result.current).toEqual(expect.objectContaining({ [privilege]: false })); + } + ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts similarity index 71% rename from x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.ts rename to x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts index 315935104d107..36f02d22487fc 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts @@ -6,9 +6,9 @@ */ import { useEffect, useMemo, useRef, useState } from 'react'; -import { useCurrentUser, useHttp } from '../../lib/kibana'; -import { appRoutesService, CheckPermissionsResponse } from '../../../../../fleet/common'; -import { useLicense } from '../../hooks/use_license'; +import { useCurrentUser, useHttp } from '../../../lib/kibana'; +import { appRoutesService, CheckPermissionsResponse } from '../../../../../../fleet/common'; +import { useLicense } from '../../../hooks/use_license'; export interface EndpointPrivileges { loading: boolean; @@ -16,6 +16,11 @@ export interface EndpointPrivileges { canAccessFleet: boolean; /** If user has permissions to access Endpoint management (includes check to ensure they also have access to fleet) */ canAccessEndpointManagement: boolean; + /** if user has permissions to create Artifacts by Policy */ + canCreateArtifactsByPolicy: boolean; + /** If user has permissions to use the Host isolation feature */ + canIsolateHost: boolean; + /** @deprecated do not use. instead, use one of the other privileges defined */ isPlatinumPlus: boolean; } @@ -29,7 +34,7 @@ export const useEndpointPrivileges = (): EndpointPrivileges => { const http = useHttp(); const user = useCurrentUser(); const isMounted = useRef(true); - const license = useLicense(); + const isPlatinumPlusLicense = useLicense().isPlatinumPlus(); const [canAccessFleet, setCanAccessFleet] = useState(false); const [fleetCheckDone, setFleetCheckDone] = useState(false); @@ -61,13 +66,19 @@ export const useEndpointPrivileges = (): EndpointPrivileges => { }, [user?.roles]); const privileges = useMemo(() => { - return { + const privilegeList: EndpointPrivileges = { loading: !fleetCheckDone || !user, canAccessFleet, canAccessEndpointManagement: canAccessFleet && isSuperUser, - isPlatinumPlus: license.isPlatinumPlus(), + canCreateArtifactsByPolicy: isPlatinumPlusLicense, + canIsolateHost: isPlatinumPlusLicense, + // FIXME: Remove usages of the property below + /** @deprecated */ + isPlatinumPlus: isPlatinumPlusLicense, }; - }, [canAccessFleet, fleetCheckDone, isSuperUser, user, license]); + + return privilegeList; + }, [canAccessFleet, fleetCheckDone, isSuperUser, user, isPlatinumPlusLicense]); // Capture if component is unmounted useEffect( diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/utils.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/utils.ts new file mode 100644 index 0000000000000..df91314479f18 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/utils.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 { EndpointPrivileges } from './use_endpoint_privileges'; + +export const getEndpointPrivilegesInitialState = (): EndpointPrivileges => { + return { + loading: true, + canAccessFleet: false, + canAccessEndpointManagement: false, + canIsolateHost: false, + canCreateArtifactsByPolicy: false, + isPlatinumPlus: false, + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx b/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx index 437d27278102b..bc0640296b33d 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx @@ -11,9 +11,10 @@ import { DeepReadonly } from 'utility-types'; import { Capabilities } from '../../../../../../../src/core/public'; import { useFetchDetectionEnginePrivileges } from '../../../detections/components/user_privileges/use_fetch_detection_engine_privileges'; import { useFetchListPrivileges } from '../../../detections/components/user_privileges/use_fetch_list_privileges'; -import { EndpointPrivileges, useEndpointPrivileges } from './use_endpoint_privileges'; +import { EndpointPrivileges, useEndpointPrivileges } from './endpoint'; import { SERVER_APP_ID } from '../../../../common/constants'; +import { getEndpointPrivilegesInitialState } from './endpoint/utils'; export interface UserPrivilegesState { listPrivileges: ReturnType; detectionEnginePrivileges: ReturnType; @@ -24,12 +25,7 @@ export interface UserPrivilegesState { export const initialUserPrivilegesState = (): UserPrivilegesState => ({ listPrivileges: { loading: false, error: undefined, result: undefined }, detectionEnginePrivileges: { loading: false, error: undefined, result: undefined }, - endpointPrivileges: { - loading: true, - canAccessEndpointManagement: false, - canAccessFleet: false, - isPlatinumPlus: false, - }, + endpointPrivileges: getEndpointPrivilegesInitialState(), kibanaSecuritySolutionsPrivileges: { crud: false, read: false }, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx index 3d95dca81165e..1a8588017e4d6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx @@ -17,7 +17,7 @@ import { UserPrivilegesProvider } from '../../../common/components/user_privileg jest.mock('../../../common/lib/kibana'); jest.mock('../../containers/detection_engine/alerts/api'); -jest.mock('../../../common/components/user_privileges/use_endpoint_privileges'); +jest.mock('../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); describe('useUserInfo', () => { beforeAll(() => { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx index 40894c1d01929..1dc1423606097 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx @@ -12,6 +12,7 @@ import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock'; import { useUserPrivileges } from '../../../../common/components/user_privileges'; import { Privilege } from './types'; import { UseAlertsPrivelegesReturn, useAlertsPrivileges } from './use_alerts_privileges'; +import { getEndpointPrivilegesInitialStateMock } from '../../../../common/components/user_privileges/endpoint/mocks'; jest.mock('./api'); jest.mock('../../../../common/hooks/use_app_toasts'); @@ -86,12 +87,11 @@ const userPrivilegesInitial: ReturnType = { result: undefined, error: undefined, }, - endpointPrivileges: { + endpointPrivileges: getEndpointPrivilegesInitialStateMock({ loading: true, canAccessEndpointManagement: false, canAccessFleet: false, - isPlatinumPlus: true, - }, + }), kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx index ade83fed4fd6b..ad4ad5062c9d5 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx @@ -13,7 +13,7 @@ import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; jest.mock('./api'); jest.mock('../../../../common/hooks/use_app_toasts'); -jest.mock('../../../../common/components/user_privileges/use_endpoint_privileges'); +jest.mock('../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); describe('useSignalIndex', () => { let appToastsMock: jest.Mocked>; diff --git a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx index 084978d35d03a..3b987a7211411 100644 --- a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx @@ -11,11 +11,12 @@ import { AppContextTestRender, createAppRootMockRenderer } from '../../../common import { EndpointPrivileges, useEndpointPrivileges, -} from '../../../common/components/user_privileges/use_endpoint_privileges'; +} from '../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; import { SearchExceptions, SearchExceptionsProps } from '.'; -jest.mock('../../../common/components/user_privileges/use_endpoint_privileges'); +import { getEndpointPrivilegesInitialStateMock } from '../../../common/components/user_privileges/endpoint/mocks'; +jest.mock('../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); let onSearchMock: jest.Mock; const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock; @@ -29,13 +30,11 @@ describe('Search exceptions', () => { const loadedUserEndpointPrivilegesState = ( endpointOverrides: Partial = {} - ): EndpointPrivileges => ({ - loading: false, - canAccessFleet: true, - canAccessEndpointManagement: true, - isPlatinumPlus: false, - ...endpointOverrides, - }); + ): EndpointPrivileges => + getEndpointPrivilegesInitialStateMock({ + isPlatinumPlus: false, + ...endpointOverrides, + }); beforeEach(() => { onSearchMock = jest.fn(); diff --git a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx index 1f3eab5db2947..569916ac20315 100644 --- a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx +++ b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiButton } from '@elastic/e import { i18n } from '@kbn/i18n'; import { PolicySelectionItem, PoliciesSelector } from '../policies_selector'; import { ImmutableArray, PolicyData } from '../../../../common/endpoint/types'; -import { useEndpointPrivileges } from '../../../common/components/user_privileges/use_endpoint_privileges'; +import { useEndpointPrivileges } from '../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; export interface SearchExceptionsProps { defaultValue?: string; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx index 05887d82cacad..a57fa8d8e4ce5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx @@ -32,7 +32,6 @@ interface Range { const DatePickerWrapper = styled.div` width: ${(props) => props.theme.eui.fractions.single.percentage}; - max-width: 350px; `; const StickyFlexItem = styled(EuiFlexItem)` background: ${(props) => `${props.theme.eui.euiHeaderBackgroundColor}`}; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx index bbe0a6f3afcd1..79af2ecb354fd 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx @@ -9,24 +9,34 @@ import React, { memo, useMemo } from 'react'; import styled from 'styled-components'; import { EuiComment, EuiText, EuiAvatarProps, EuiCommentProps, IconType } from '@elastic/eui'; -import { Immutable, ActivityLogEntry } from '../../../../../../../common/endpoint/types'; +import { + Immutable, + ActivityLogEntry, + ActivityLogItemTypes, +} from '../../../../../../../common/endpoint/types'; import { FormattedRelativePreferenceDate } from '../../../../../../common/components/formatted_date'; import { LogEntryTimelineIcon } from './log_entry_timeline_icon'; +import { useEuiTheme } from '../../../../../../common/lib/theme/use_eui_theme'; import * as i18 from '../../translations'; const useLogEntryUIProps = ( - logEntry: Immutable + logEntry: Immutable, + theme: ReturnType ): { actionEventTitle: string; + avatarColor: EuiAvatarProps['color']; + avatarIconColor: EuiAvatarProps['iconColor']; avatarSize: EuiAvatarProps['size']; commentText: string; commentType: EuiCommentProps['type']; displayComment: boolean; displayResponseEvent: boolean; + failedActionEventTitle: string; iconType: IconType; isResponseEvent: boolean; isSuccessful: boolean; + isCompleted: boolean; responseEventTitle: string; username: string | React.ReactNode; } => { @@ -34,15 +44,19 @@ const useLogEntryUIProps = ( let iconType: IconType = 'dot'; let commentType: EuiCommentProps['type'] = 'update'; let commentText: string = ''; + let avatarColor: EuiAvatarProps['color'] = theme.euiColorLightestShade; + let avatarIconColor: EuiAvatarProps['iconColor']; let avatarSize: EuiAvatarProps['size'] = 's'; + let failedActionEventTitle: string = ''; let isIsolateAction: boolean = false; let isResponseEvent: boolean = false; let isSuccessful: boolean = false; + let isCompleted: boolean = false; let displayComment: boolean = false; let displayResponseEvent: boolean = true; let username: EuiCommentProps['username'] = ''; - if (logEntry.type === 'action') { + if (logEntry.type === ActivityLogItemTypes.FLEET_ACTION) { avatarSize = 'm'; commentType = 'regular'; commentText = logEntry.item.data.data.comment?.trim() ?? ''; @@ -59,13 +73,51 @@ const useLogEntryUIProps = ( displayComment = true; } } - } else if (logEntry.type === 'response') { + } + if (logEntry.type === ActivityLogItemTypes.ACTION) { + avatarSize = 'm'; + commentType = 'regular'; + commentText = logEntry.item.data.EndpointActions.data.comment?.trim() ?? ''; + displayResponseEvent = false; + iconType = 'lockOpen'; + username = logEntry.item.data.user.id; + avatarIconColor = theme.euiColorVis9_behindText; + failedActionEventTitle = i18.ACTIVITY_LOG.LogEntry.action.failedEndpointReleaseAction; + if (logEntry.item.data.EndpointActions.data) { + const data = logEntry.item.data.EndpointActions.data; + if (data.command === 'isolate') { + iconType = 'lock'; + failedActionEventTitle = i18.ACTIVITY_LOG.LogEntry.action.failedEndpointIsolateAction; + } + if (commentText) { + displayComment = true; + } + } + } else if (logEntry.type === ActivityLogItemTypes.FLEET_RESPONSE) { isResponseEvent = true; if (logEntry.item.data.action_data.command === 'isolate') { isIsolateAction = true; } if (!!logEntry.item.data.completed_at && !logEntry.item.data.error) { isSuccessful = true; + } else { + avatarColor = theme.euiColorVis9_behindText; + } + } else if (logEntry.type === ActivityLogItemTypes.RESPONSE) { + iconType = 'check'; + isResponseEvent = true; + if (logEntry.item.data.EndpointActions.data.command === 'isolate') { + isIsolateAction = true; + } + if (logEntry.item.data.EndpointActions.completed_at) { + isCompleted = true; + if (!logEntry.item.data.error) { + isSuccessful = true; + avatarColor = theme.euiColorVis0_behindText; + } else { + isSuccessful = false; + avatarColor = theme.euiColorVis9_behindText; + } } } @@ -75,13 +127,23 @@ const useLogEntryUIProps = ( const getResponseEventTitle = () => { if (isIsolateAction) { - if (isSuccessful) { + if (isCompleted) { + if (isSuccessful) { + return i18.ACTIVITY_LOG.LogEntry.response.unisolationCompletedAndSuccessful; + } + return i18.ACTIVITY_LOG.LogEntry.response.unisolationCompletedAndUnsuccessful; + } else if (isSuccessful) { return i18.ACTIVITY_LOG.LogEntry.response.isolationSuccessful; } else { return i18.ACTIVITY_LOG.LogEntry.response.isolationFailed; } } else { - if (isSuccessful) { + if (isCompleted) { + if (isSuccessful) { + return i18.ACTIVITY_LOG.LogEntry.response.unisolationCompletedAndSuccessful; + } + return i18.ACTIVITY_LOG.LogEntry.response.unisolationCompletedAndUnsuccessful; + } else if (isSuccessful) { return i18.ACTIVITY_LOG.LogEntry.response.unisolationSuccessful; } else { return i18.ACTIVITY_LOG.LogEntry.response.unisolationFailed; @@ -91,18 +153,22 @@ const useLogEntryUIProps = ( return { actionEventTitle, + avatarColor, + avatarIconColor, avatarSize, commentText, commentType, displayComment, displayResponseEvent, + failedActionEventTitle, iconType, isResponseEvent, isSuccessful, + isCompleted, responseEventTitle: getResponseEventTitle(), username, }; - }, [logEntry]); + }, [logEntry, theme]); }; const StyledEuiComment = styled(EuiComment)` @@ -126,28 +192,41 @@ const StyledEuiComment = styled(EuiComment)` `; export const LogEntry = memo(({ logEntry }: { logEntry: Immutable }) => { + const theme = useEuiTheme(); const { actionEventTitle, + avatarColor, + avatarIconColor, avatarSize, commentText, commentType, displayComment, displayResponseEvent, + failedActionEventTitle, iconType, isResponseEvent, - isSuccessful, responseEventTitle, username, - } = useLogEntryUIProps(logEntry); + } = useLogEntryUIProps(logEntry, theme); return ( } - event={{displayResponseEvent ? responseEventTitle : actionEventTitle}} + event={ + + {displayResponseEvent + ? responseEventTitle + : failedActionEventTitle + ? failedActionEventTitle + : actionEventTitle} + + } timelineIcon={ - + } data-test-subj="timelineEntry" > diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry_timeline_icon.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry_timeline_icon.tsx index 3ff311cd8a139..25e7c7d2c4a49 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry_timeline_icon.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry_timeline_icon.tsx @@ -7,32 +7,27 @@ import React, { memo } from 'react'; import { EuiAvatar, EuiAvatarProps } from '@elastic/eui'; -import { useEuiTheme } from '../../../../../../common/lib/theme/use_eui_theme'; export const LogEntryTimelineIcon = memo( ({ + avatarColor, + avatarIconColor, avatarSize, - isResponseEvent, - isSuccessful, iconType, + isResponseEvent, }: { + avatarColor: EuiAvatarProps['color']; + avatarIconColor?: EuiAvatarProps['iconColor']; avatarSize: EuiAvatarProps['size']; - isResponseEvent: boolean; - isSuccessful: boolean; iconType: EuiAvatarProps['iconType']; + isResponseEvent: boolean; }) => { - const euiTheme = useEuiTheme(); - return ( ); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx index 123a51e5a52bd..717368a1ff3a0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx @@ -8,7 +8,11 @@ import React, { ComponentType } from 'react'; import moment from 'moment'; -import { ActivityLog, Immutable } from '../../../../../../common/endpoint/types'; +import { + ActivityLog, + Immutable, + ActivityLogItemTypes, +} from '../../../../../../common/endpoint/types'; import { EndpointDetailsFlyoutTabs } from './components/endpoint_details_tabs'; import { EndpointActivityLog } from './endpoint_activity_log'; import { EndpointDetailsFlyout } from '.'; @@ -26,7 +30,7 @@ export const dummyEndpointActivityLog = ( endDate: moment().toString(), data: [ { - type: 'action', + type: ActivityLogItemTypes.FLEET_ACTION, item: { id: '', data: { @@ -44,7 +48,7 @@ export const dummyEndpointActivityLog = ( }, }, { - type: 'action', + type: ActivityLogItemTypes.FLEET_ACTION, item: { id: '', data: { @@ -63,7 +67,7 @@ export const dummyEndpointActivityLog = ( }, }, { - type: 'action', + type: ActivityLogItemTypes.FLEET_ACTION, item: { id: '', data: { @@ -82,7 +86,7 @@ export const dummyEndpointActivityLog = ( }, }, { - type: 'action', + type: ActivityLogItemTypes.FLEET_ACTION, item: { id: '', data: { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index b2c438659b771..727c2e8a35024 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -42,6 +42,7 @@ import { import { getCurrentIsolationRequestState } from '../store/selectors'; import { licenseService } from '../../../../common/hooks/use_license'; import { FleetActionGenerator } from '../../../../../common/endpoint/data_generators/fleet_action_generator'; +import { EndpointActionGenerator } from '../../../../../common/endpoint/data_generators/endpoint_action_generator'; import { APP_PATH, MANAGEMENT_PATH, @@ -807,7 +808,7 @@ describe('when on the endpoint list page', () => { let renderResult: ReturnType; const agentId = 'some_agent_id'; - let getMockData: () => ActivityLog; + let getMockData: (option?: { hasLogsEndpointActionResponses?: boolean }) => ActivityLog; beforeEach(async () => { window.IntersectionObserver = jest.fn(() => ({ root: null, @@ -828,10 +829,15 @@ describe('when on the endpoint list page', () => { }); const fleetActionGenerator = new FleetActionGenerator('seed'); - const responseData = fleetActionGenerator.generateResponse({ + const endpointActionGenerator = new EndpointActionGenerator('seed'); + const endpointResponseData = endpointActionGenerator.generateResponse({ + agent: { id: agentId }, + }); + const fleetResponseData = fleetActionGenerator.generateResponse({ agent_id: agentId, }); - const actionData = fleetActionGenerator.generate({ + + const fleetActionData = fleetActionGenerator.generate({ agents: [agentId], data: { comment: 'some comment', @@ -844,35 +850,49 @@ describe('when on the endpoint list page', () => { }, }); - getMockData = () => ({ - page: 1, - pageSize: 50, - startDate: 'now-1d', - endDate: 'now', - data: [ - { - type: 'response', - item: { - id: 'some_id_0', - data: responseData, + getMockData = (hasLogsEndpointActionResponses?: { + hasLogsEndpointActionResponses?: boolean; + }) => { + const response: ActivityLog = { + page: 1, + pageSize: 50, + startDate: 'now-1d', + endDate: 'now', + data: [ + { + type: 'fleetResponse', + item: { + id: 'some_id_1', + data: fleetResponseData, + }, }, - }, - { - type: 'action', - item: { - id: 'some_id_1', - data: actionData, + { + type: 'fleetAction', + item: { + id: 'some_id_2', + data: fleetActionData, + }, }, - }, - { - type: 'action', + { + type: 'fleetAction', + item: { + id: 'some_id_3', + data: isolatedActionData, + }, + }, + ], + }; + if (hasLogsEndpointActionResponses) { + response.data.unshift({ + type: 'response', item: { - id: 'some_id_3', - data: isolatedActionData, + id: 'some_id_0', + data: endpointResponseData, }, - }, - ], - }); + }); + } + return response; + }; renderResult = render(); await reactTestingLibrary.act(async () => { @@ -912,6 +932,25 @@ describe('when on the endpoint list page', () => { expect(`${logEntries[1]} .euiCommentTimeline__icon--regular`).not.toBe(null); }); + it('should display log accurately with endpoint responses', async () => { + const activityLogTab = await renderResult.findByTestId('activity_log'); + reactTestingLibrary.act(() => { + reactTestingLibrary.fireEvent.click(activityLogTab); + }); + await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged'); + reactTestingLibrary.act(() => { + dispatchEndpointDetailsActivityLogChanged( + 'success', + getMockData({ hasLogsEndpointActionResponses: true }) + ); + }); + const logEntries = await renderResult.queryAllByTestId('timelineEntry'); + expect(logEntries.length).toEqual(4); + expect(`${logEntries[0]} .euiCommentTimeline__icon--update`).not.toBe(null); + expect(`${logEntries[1]} .euiCommentTimeline__icon--update`).not.toBe(null); + expect(`${logEntries[2]} .euiCommentTimeline__icon--regular`).not.toBe(null); + }); + it('should display empty state when API call has failed', async () => { const activityLogTab = await renderResult.findByTestId('activity_log'); reactTestingLibrary.act(() => { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts index c8a29eed3fda7..9cd55a70005ec 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts @@ -56,8 +56,44 @@ export const ACTIVITY_LOG = { defaultMessage: 'submitted request: Release host', } ), + failedEndpointReleaseAction: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.failedEndpointReleaseAction', + { + defaultMessage: 'failed to submit request: Release host', + } + ), + failedEndpointIsolateAction: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.failedEndpointIsolateAction', + { + defaultMessage: 'failed to submit request: Isolate host', + } + ), }, response: { + isolationCompletedAndSuccessful: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.isolationCompletedAndSuccessful', + { + defaultMessage: 'Host isolation request completed by Endpoint', + } + ), + isolationCompletedAndUnsuccessful: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.isolationCompletedAndUnsuccessful', + { + defaultMessage: 'Host isolation request completed by Endpoint with errors', + } + ), + unisolationCompletedAndSuccessful: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.unisolationCompletedAndSuccessful', + { + defaultMessage: 'Release request completed by Endpoint', + } + ), + unisolationCompletedAndUnsuccessful: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.unisolationCompletedAndUnsuccessful', + { + defaultMessage: 'Release request completed by Endpoint with errors', + } + ), isolationSuccessful: i18n.translate( 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.isolationSuccessful', { diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx index 0729f95bb44a9..02efce1ab59e8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx @@ -14,7 +14,7 @@ import { isFailedResourceState, isLoadedResourceState } from '../../../state'; // Needed to mock the data services used by the ExceptionItem component jest.mock('../../../../common/lib/kibana'); -jest.mock('../../../../common/components/user_privileges/use_endpoint_privileges'); +jest.mock('../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); describe('When on the Event Filters List Page', () => { let render: () => ReturnType; diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx index 5113457e5bccc..625da11a3644e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx @@ -16,8 +16,7 @@ import { getHostIsolationExceptionItems } from '../service'; import { HostIsolationExceptionsList } from './host_isolation_exceptions_list'; import { useLicense } from '../../../../common/hooks/use_license'; -jest.mock('../../../../common/components/user_privileges/use_endpoint_privileges'); - +jest.mock('../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); jest.mock('../service'); jest.mock('../../../../common/hooks/use_license'); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx index ee52e1210a481..c12bec03ada04 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx @@ -10,7 +10,7 @@ import { EuiEmptyPrompt, EuiButton, EuiPageTemplate, EuiLink } from '@elastic/eu import { FormattedMessage } from '@kbn/i18n/react'; import { usePolicyDetailsNavigateCallback } from '../../policy_hooks'; import { useGetLinkTo } from './use_policy_trusted_apps_empty_hooks'; -import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/use_endpoint_privileges'; +import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; interface CommonProps { policyId: string; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.test.tsx index c1d00f7a3f99b..8e412d2020b72 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.test.tsx @@ -21,7 +21,7 @@ import { createLoadedResourceState, isLoadedResourceState } from '../../../../.. import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing'; jest.mock('../../../../trusted_apps/service'); -jest.mock('../../../../../../common/components/user_privileges/use_endpoint_privileges'); +jest.mock('../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); let mockedContext: AppContextTestRender; let waitForAction: MiddlewareActionSpyHelper['waitForAction']; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx index 43e19c00bcc8e..dbb18a1b0f2ef 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx @@ -19,13 +19,11 @@ import { createLoadedResourceState, isLoadedResourceState } from '../../../../.. import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing'; import { EndpointDocGenerator } from '../../../../../../../common/endpoint/generate_data'; import { policyListApiPathHandlers } from '../../../store/test_mock_utils'; -import { - EndpointPrivileges, - useEndpointPrivileges, -} from '../../../../../../common/components/user_privileges/use_endpoint_privileges'; +import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; +import { getEndpointPrivilegesInitialStateMock } from '../../../../../../common/components/user_privileges/endpoint/mocks'; jest.mock('../../../../trusted_apps/service'); -jest.mock('../../../../../../common/components/user_privileges/use_endpoint_privileges'); +jest.mock('../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock; let mockedContext: AppContextTestRender; @@ -37,16 +35,6 @@ let http: typeof coreStart.http; const generator = new EndpointDocGenerator(); describe('Policy trusted apps layout', () => { - const loadedUserEndpointPrivilegesState = ( - endpointOverrides: Partial = {} - ): EndpointPrivileges => ({ - loading: false, - canAccessFleet: true, - canAccessEndpointManagement: true, - isPlatinumPlus: true, - ...endpointOverrides, - }); - beforeEach(() => { mockedContext = createAppRootMockRenderer(); http = mockedContext.coreStart.http; @@ -137,7 +125,7 @@ describe('Policy trusted apps layout', () => { it('should hide assign button on empty state with unassigned policies when downgraded to a gold or below license', async () => { mockUseEndpointPrivileges.mockReturnValue( - loadedUserEndpointPrivilegesState({ + getEndpointPrivilegesInitialStateMock({ isPlatinumPlus: false, }) ); @@ -155,7 +143,7 @@ describe('Policy trusted apps layout', () => { it('should hide the `Assign trusted applications` button when there is data and the license is downgraded to gold or below', async () => { mockUseEndpointPrivileges.mockReturnValue( - loadedUserEndpointPrivilegesState({ + getEndpointPrivilegesInitialStateMock({ isPlatinumPlus: false, }) ); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx index a3f1ed215286a..49f76ad2e02c6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx @@ -30,7 +30,7 @@ import { import { usePolicyDetailsNavigateCallback, usePolicyDetailsSelector } from '../../policy_hooks'; import { PolicyTrustedAppsFlyout } from '../flyout'; import { PolicyTrustedAppsList } from '../list/policy_trusted_apps_list'; -import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/use_endpoint_privileges'; +import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; import { useAppUrl } from '../../../../../../common/lib/kibana'; import { APP_ID } from '../../../../../../../common/constants'; import { getTrustedAppsListPath } from '../../../../../common/routing'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx index a8d3cc1505463..e18d3c01791c0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx @@ -24,9 +24,10 @@ import { APP_ID } from '../../../../../../../common/constants'; import { EndpointPrivileges, useEndpointPrivileges, -} from '../../../../../../common/components/user_privileges/use_endpoint_privileges'; +} from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; +import { getEndpointPrivilegesInitialStateMock } from '../../../../../../common/components/user_privileges/endpoint/mocks'; -jest.mock('../../../../../../common/components/user_privileges/use_endpoint_privileges'); +jest.mock('../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock; describe('when rendering the PolicyTrustedAppsList', () => { @@ -43,10 +44,7 @@ describe('when rendering the PolicyTrustedAppsList', () => { const loadedUserEndpointPrivilegesState = ( endpointOverrides: Partial = {} ): EndpointPrivileges => ({ - loading: false, - canAccessFleet: true, - canAccessEndpointManagement: true, - isPlatinumPlus: true, + ...getEndpointPrivilegesInitialStateMock(), ...endpointOverrides, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx index f6afd9d502486..def0f490b7fee 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx @@ -38,7 +38,7 @@ import { ContextMenuItemNavByRouterProps } from '../../../../../components/conte import { ArtifactEntryCollapsibleCardProps } from '../../../../../components/artifact_entry_card'; import { useTestIdGenerator } from '../../../../../components/hooks/use_test_id_generator'; import { RemoveTrustedAppFromPolicyModal } from './remove_trusted_app_from_policy_modal'; -import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/use_endpoint_privileges'; +import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; const DATA_TEST_SUBJ = 'policyTrustedAppsGrid'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index f39fd47c78771..b4366a8922927 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -52,7 +52,7 @@ jest.mock('../../../../common/hooks/use_license', () => { }; }); -jest.mock('../../../../common/components/user_privileges/use_endpoint_privileges'); +jest.mock('../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); describe('When on the Trusted Apps Page', () => { const expectedAboutInfo = diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx index 5fa2725f9ee6f..61e9e66f1bb87 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx @@ -45,9 +45,10 @@ describe('OverviewEmpty', () => { expect(wrapper.find('[data-test-subj="empty-page"]').prop('noDataConfig')).toEqual({ actions: { elasticAgent: { + category: 'security', description: - 'Use Elastic Agent to collect security events and protect your endpoints from threats. Manage your agents in Fleet and add integrations with a single click.', - href: '/app/integrations/browse/security', + 'Use Elastic Agent to collect security events and protect your endpoints from threats.', + title: 'Add a Security integration', }, }, docsLink: 'https://www.elastic.co/guide/en/security/mocked-test-branch/index.html', @@ -68,8 +69,11 @@ describe('OverviewEmpty', () => { it('render with correct actions ', () => { expect(wrapper.find('[data-test-subj="empty-page"]').prop('noDataConfig')).toEqual({ actions: { - beats: { - href: '/app/home#/tutorial_directory/security', + elasticAgent: { + category: 'security', + description: + 'Use Elastic Agent to collect security events and protect your endpoints from threats.', + title: 'Add a Security integration', }, }, docsLink: 'https://www.elastic.co/guide/en/security/mocked-test-branch/index.html', diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx index bc76333943191..9b20c079002e6 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx @@ -5,13 +5,10 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../../../common/lib/kibana'; -import { ADD_DATA_PATH } from '../../../../common/constants'; -import { pagePathGetters } from '../../../../../fleet/public'; import { SOLUTION_NAME } from '../../../../public/common/translations'; -import { useUserPrivileges } from '../../../common/components/user_privileges'; import { KibanaPageTemplate, @@ -19,42 +16,27 @@ import { } from '../../../../../../../src/plugins/kibana_react/public'; const OverviewEmptyComponent: React.FC = () => { - const { http, docLinks } = useKibana().services; - const basePath = http.basePath.get(); - const canAccessFleet = useUserPrivileges().endpointPrivileges.canAccessFleet; - const integrationsPathComponents = pagePathGetters.integrations_all({ category: 'security' }); - - const agentAction: NoDataPageActionsProps = useMemo( - () => ({ - elasticAgent: { - href: `${basePath}${integrationsPathComponents[0]}${integrationsPathComponents[1]}`, - description: i18n.translate( - 'xpack.securitySolution.pages.emptyPage.beatsCard.description', - { - defaultMessage: - 'Use Elastic Agent to collect security events and protect your endpoints from threats. Manage your agents in Fleet and add integrations with a single click.', - } - ), - }, - }), - [basePath, integrationsPathComponents] - ); - - const beatsAction: NoDataPageActionsProps = useMemo( - () => ({ - beats: { - href: `${basePath}${ADD_DATA_PATH}`, - }, - }), - [basePath] - ); + const { docLinks } = useKibana().services; + + const agentAction: NoDataPageActionsProps = { + elasticAgent: { + category: 'security', + title: i18n.translate('xpack.securitySolution.pages.emptyPage.beatsCard.title', { + defaultMessage: 'Add a Security integration', + }), + description: i18n.translate('xpack.securitySolution.pages.emptyPage.beatsCard.description', { + defaultMessage: + 'Use Elastic Agent to collect security events and protect your endpoints from threats.', + }), + }, + }; return ( diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx index cab02450f8886..ab5ae4f613e38 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx @@ -30,7 +30,7 @@ import { mockCtiLinksResponse, } from '../components/overview_cti_links/mock'; import { useCtiDashboardLinks } from '../containers/overview_cti_links'; -import { EndpointPrivileges } from '../../common/components/user_privileges/use_endpoint_privileges'; +import { EndpointPrivileges } from '../../common/components/user_privileges/endpoint/use_endpoint_privileges'; import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; import { useHostsRiskScore } from '../containers/overview_risky_host_links/use_hosts_risk_score'; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts index 4bd63c83169e5..5ce7962000788 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts @@ -30,9 +30,15 @@ import { } from '../../mocks'; import { registerActionAuditLogRoutes } from './audit_log'; import uuid from 'uuid'; -import { aMockAction, aMockResponse, MockAction, mockSearchResult, MockResponse } from './mocks'; +import { mockAuditLogSearchResult, Results } from './mocks'; import { SecuritySolutionRequestHandlerContext } from '../../../types'; -import { ActivityLog } from '../../../../common/endpoint/types'; +import { + ActivityLog, + EndpointAction, + EndpointActionResponse, +} from '../../../../common/endpoint/types'; +import { FleetActionGenerator } from '../../../../common/endpoint/data_generators/fleet_action_generator'; +import { EndpointActionGenerator } from '../../../../common/endpoint/data_generators/endpoint_action_generator'; describe('Action Log API', () => { describe('schema', () => { @@ -93,17 +99,30 @@ describe('Action Log API', () => { }); describe('response', () => { - const mockID = 'XYZABC-000'; - const actionID = 'some-known-actionid'; + const mockAgentID = 'XYZABC-000'; let endpointAppContextService: EndpointAppContextService; + const fleetActionGenerator = new FleetActionGenerator('seed'); + const endpointActionGenerator = new EndpointActionGenerator('seed'); // convenience for calling the route and handler for audit log let getActivityLog: ( params: EndpointActionLogRequestParams, query?: EndpointActionLogRequestQuery ) => Promise>; - // convenience for injecting mock responses for actions index and responses - let havingActionsAndResponses: (actions: MockAction[], responses: MockResponse[]) => void; + + // convenience for injecting mock action requests and responses + // for .logs-endpoint and .fleet indices + let mockActions: ({ + numActions, + hasFleetActions, + hasFleetResponses, + hasResponses, + }: { + numActions: number; + hasFleetActions?: boolean; + hasFleetResponses?: boolean; + hasResponses?: boolean; + }) => void; let havingErrors: () => void; @@ -149,12 +168,113 @@ describe('Action Log API', () => { return mockResponse; }; - havingActionsAndResponses = (actions: MockAction[], responses: MockResponse[]) => { - esClientMock.asCurrentUser.search = jest.fn().mockImplementation((req) => { - const items: any[] = - req.index === '.fleet-actions' ? actions.splice(0, 50) : responses.splice(0, 1000); + // some arbitrary ids for needed actions + const getMockActionIds = (numAction: number): string[] => { + return [...Array(numAction).keys()].map(() => Math.random().toString(36).split('.')[1]); + }; + + // create as many actions as needed + const getEndpointActionsData = (actionIds: string[]) => { + const data = actionIds.map((actionId) => + endpointActionGenerator.generate({ + agent: { id: mockAgentID }, + EndpointActions: { + action_id: actionId, + }, + }) + ); + return data; + }; + // create as many responses as needed + const getEndpointResponseData = (actionIds: string[]) => { + const data = actionIds.map((actionId) => + endpointActionGenerator.generateResponse({ + agent: { id: mockAgentID }, + EndpointActions: { + action_id: actionId, + }, + }) + ); + return data; + }; + // create as many fleet actions as needed + const getFleetResponseData = (actionIds: string[]) => { + const data = actionIds.map((actionId) => + fleetActionGenerator.generateResponse({ + agent_id: mockAgentID, + action_id: actionId, + }) + ); + return data; + }; + // create as many fleet responses as needed + const getFleetActionData = (actionIds: string[]) => { + const data = actionIds.map((actionId) => + fleetActionGenerator.generate({ + agents: [mockAgentID], + action_id: actionId, + data: { + comment: 'some comment', + }, + }) + ); + return data; + }; + + // mock actions and responses results in a single response + mockActions = ({ + numActions, + hasFleetActions = false, + hasFleetResponses = false, + hasResponses = false, + }: { + numActions: number; + hasFleetActions?: boolean; + hasFleetResponses?: boolean; + hasResponses?: boolean; + }) => { + esClientMock.asCurrentUser.search = jest.fn().mockImplementationOnce(() => { + let actions: Results[] = []; + let fleetActions: Results[] = []; + let responses: Results[] = []; + let fleetResponses: Results[] = []; + + const actionIds = getMockActionIds(numActions); + + actions = getEndpointActionsData(actionIds).map((e) => ({ + _index: '.ds-.logs-endpoint.actions-default-2021.19.10-000001', + _source: e, + })); + + if (hasFleetActions) { + fleetActions = getFleetActionData(actionIds).map((e) => ({ + _index: '.fleet-actions-7', + _source: e, + })); + } - return Promise.resolve(mockSearchResult(items.map((x) => x.build()))); + if (hasFleetResponses) { + fleetResponses = getFleetResponseData(actionIds).map((e) => ({ + _index: '.ds-.fleet-actions-results-2021.19.10-000001', + _source: e, + })); + } + + if (hasResponses) { + responses = getEndpointResponseData(actionIds).map((e) => ({ + _index: '.ds-.logs-endpoint.action.responses-default-2021.19.10-000001', + _source: e, + })); + } + + const results = mockAuditLogSearchResult([ + ...actions, + ...fleetActions, + ...responses, + ...fleetResponses, + ]); + + return Promise.resolve(results); }); }; @@ -172,45 +292,80 @@ describe('Action Log API', () => { }); it('should return an empty array when nothing in audit log', async () => { - havingActionsAndResponses([], []); - const response = await getActivityLog({ agent_id: mockID }); + mockActions({ numActions: 0 }); + + const response = await getActivityLog({ agent_id: mockAgentID }); expect(response.ok).toBeCalled(); expect((response.ok.mock.calls[0][0]?.body as ActivityLog).data).toHaveLength(0); }); - it('should have actions and action responses', async () => { - havingActionsAndResponses( - [ - aMockAction().withAgent(mockID).withAction('isolate').withID(actionID), - aMockAction().withAgent(mockID).withAction('unisolate'), - ], - [aMockResponse(actionID, mockID).forAction(actionID).forAgent(mockID)] - ); - const response = await getActivityLog({ agent_id: mockID }); + it('should return fleet actions, fleet responses and endpoint responses', async () => { + mockActions({ + numActions: 2, + hasFleetActions: true, + hasFleetResponses: true, + hasResponses: true, + }); + + const response = await getActivityLog({ agent_id: mockAgentID }); + const responseBody = response.ok.mock.calls[0][0]?.body as ActivityLog; + expect(response.ok).toBeCalled(); + expect(responseBody.data).toHaveLength(6); + + expect( + responseBody.data.filter((e) => (e.item.data as EndpointActionResponse).completed_at) + ).toHaveLength(2); + expect( + responseBody.data.filter((e) => (e.item.data as EndpointAction).expiration) + ).toHaveLength(2); + }); + + it('should return only fleet actions and no responses', async () => { + mockActions({ numActions: 2, hasFleetActions: true }); + + const response = await getActivityLog({ agent_id: mockAgentID }); const responseBody = response.ok.mock.calls[0][0]?.body as ActivityLog; + expect(response.ok).toBeCalled(); + expect(responseBody.data).toHaveLength(2); + + expect( + responseBody.data.filter((e) => (e.item.data as EndpointAction).expiration) + ).toHaveLength(2); + }); + + it('should only have fleet data', async () => { + mockActions({ numActions: 2, hasFleetActions: true, hasFleetResponses: true }); + const response = await getActivityLog({ agent_id: mockAgentID }); + const responseBody = response.ok.mock.calls[0][0]?.body as ActivityLog; expect(response.ok).toBeCalled(); - expect(responseBody.data).toHaveLength(3); - expect(responseBody.data.filter((e) => e.type === 'response')).toHaveLength(1); - expect(responseBody.data.filter((e) => e.type === 'action')).toHaveLength(2); + expect(responseBody.data).toHaveLength(4); + + expect( + responseBody.data.filter((e) => (e.item.data as EndpointAction).expiration) + ).toHaveLength(2); + expect( + responseBody.data.filter((e) => (e.item.data as EndpointActionResponse).completed_at) + ).toHaveLength(2); }); it('should throw errors when no results for some agentID', async () => { havingErrors(); try { - await getActivityLog({ agent_id: mockID }); + await getActivityLog({ agent_id: mockAgentID }); } catch (error) { - expect(error.message).toEqual(`Error fetching actions log for agent_id ${mockID}`); + expect(error.message).toEqual(`Error fetching actions log for agent_id ${mockAgentID}`); } }); it('should return date ranges if present in the query', async () => { - havingActionsAndResponses([], []); + mockActions({ numActions: 0 }); + const startDate = new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(); const endDate = new Date().toISOString(); const response = await getActivityLog( - { agent_id: mockID }, + { agent_id: mockAgentID }, { page: 1, page_size: 50, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts index e12299bedbb34..02f0cb4867646 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts @@ -17,6 +17,7 @@ import { ENDPOINT_ACTION_RESPONSES_DS, ISOLATE_HOST_ROUTE, UNISOLATE_HOST_ROUTE, + failedFleetActionErrorCode, } from '../../../../common/endpoint/constants'; import { AGENT_ACTIONS_INDEX } from '../../../../../fleet/common'; import { @@ -33,6 +34,7 @@ import { getMetadataForEndpoints } from '../../services'; import { EndpointAppContext } from '../../types'; import { APP_ID } from '../../../../common/constants'; import { userCanIsolate } from '../../../../common/endpoint/actions'; +import { doLogsEndpointActionDsExists } from '../../utils'; /** * Registers the Host-(un-)isolation routes @@ -78,7 +80,7 @@ const createFailedActionResponseEntry = async ({ body: { ...doc, error: { - code: '424', + code: failedFleetActionErrorCode, message: 'Failed to deliver action request to fleet', }, }, @@ -88,31 +90,6 @@ const createFailedActionResponseEntry = async ({ } }; -const doLogsEndpointActionDsExists = async ({ - context, - logger, - dataStreamName, -}: { - context: SecuritySolutionRequestHandlerContext; - logger: Logger; - dataStreamName: string; -}): Promise => { - try { - const esClient = context.core.elasticsearch.client.asInternalUser; - const doesIndexTemplateExist = await esClient.indices.existsIndexTemplate({ - name: dataStreamName, - }); - return doesIndexTemplateExist.statusCode === 404 ? false : true; - } catch (error) { - const errorType = error?.type ?? ''; - if (errorType !== 'resource_not_found_exception') { - logger.error(error); - throw error; - } - return false; - } -}; - export const isolationRequestHandler = function ( endpointContext: EndpointAppContext, isolate: boolean diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/mocks.ts index 34f7d140a78de..b50d80a9bae71 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/mocks.ts @@ -13,11 +13,43 @@ import { ApiResponse } from '@elastic/elasticsearch'; import moment from 'moment'; import uuid from 'uuid'; import { + LogsEndpointAction, + LogsEndpointActionResponse, EndpointAction, EndpointActionResponse, ISOLATION_ACTIONS, } from '../../../../common/endpoint/types'; +export interface Results { + _index: string; + _source: + | LogsEndpointAction + | LogsEndpointActionResponse + | EndpointAction + | EndpointActionResponse; +} +export const mockAuditLogSearchResult = (results?: Results[]) => { + const response = { + body: { + hits: { + total: { value: results?.length ?? 0, relation: 'eq' }, + hits: + results?.map((a: Results) => ({ + _index: a._index, + _id: Math.random().toString(36).split('.')[1], + _score: 0.0, + _source: a._source, + })) ?? [], + }, + }, + statusCode: 200, + headers: {}, + warnings: [], + meta: {} as any, + }; + return response; +}; + export const mockSearchResult = (results: any = []): ApiResponse => { return { body: { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts index 027107bcf1a59..e98cdc4f11404 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts @@ -48,6 +48,7 @@ import { } from './support/query_strategies'; import { NotFoundError } from '../../errors'; import { EndpointHostUnEnrolledError } from '../../services/metadata'; +import { getAgentStatus } from '../../../../../fleet/common/services/agent_status'; export interface MetadataRequestContext { esClient?: IScopedClusterClient; @@ -522,10 +523,11 @@ async function queryUnitedIndex( const agentPolicy = agentPoliciesMap[agent.policy_id!]; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const endpointPolicy = endpointPoliciesMap[agent.policy_id!]; + const fleetAgentStatus = getAgentStatus(agent as Agent); + return { metadata, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - host_status: fleetAgentStatusToEndpointHostStatus(agent.last_checkin_status!), + host_status: fleetAgentStatusToEndpointHostStatus(fleetAgentStatus), policy_info: { agent: { applied: { diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions.ts index 711d78ba51b59..d59ecb674196c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions.ts @@ -6,15 +6,28 @@ */ import { ElasticsearchClient, Logger } from 'kibana/server'; +import { SearchHit, SearchResponse } from '@elastic/elasticsearch/api/types'; +import { ApiResponse } from '@elastic/elasticsearch'; import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '../../../../fleet/common'; import { SecuritySolutionRequestHandlerContext } from '../../types'; import { ActivityLog, + ActivityLogEntry, EndpointAction, + LogsEndpointAction, EndpointActionResponse, EndpointPendingActions, + LogsEndpointActionResponse, } from '../../../common/endpoint/types'; -import { catchAndWrapError } from '../utils'; +import { + catchAndWrapError, + categorizeActionResults, + categorizeResponseResults, + getActionRequestsResult, + getActionResponsesResult, + getTimeSortedData, + getUniqueLogData, +} from '../utils'; import { EndpointMetadataService } from './metadata'; const PENDING_ACTION_RESPONSE_MAX_LAPSED_TIME = 300000; // 300k ms === 5 minutes @@ -38,9 +51,9 @@ export const getAuditLogResponse = async ({ }): Promise => { const size = Math.floor(pageSize / 2); const from = page <= 1 ? 0 : page * size - size + 1; - const esClient = context.core.elasticsearch.client.asCurrentUser; + const data = await getActivityLog({ - esClient, + context, from, size, startDate, @@ -59,7 +72,7 @@ export const getAuditLogResponse = async ({ }; const getActivityLog = async ({ - esClient, + context, size, from, startDate, @@ -67,83 +80,39 @@ const getActivityLog = async ({ elasticAgentId, logger, }: { - esClient: ElasticsearchClient; + context: SecuritySolutionRequestHandlerContext; elasticAgentId: string; size: number; from: number; startDate: string; endDate: string; logger: Logger; -}) => { - const options = { - headers: { - 'X-elastic-product-origin': 'fleet', - }, - ignore: [404], - }; - - let actionsResult; - let responsesResult; - const dateFilters = [ - { range: { '@timestamp': { gte: startDate } } }, - { range: { '@timestamp': { lte: endDate } } }, - ]; +}): Promise => { + let actionsResult: ApiResponse, unknown>; + let responsesResult: ApiResponse, unknown>; try { // fetch actions with matching agent_id - const baseActionFilters = [ - { term: { agents: elasticAgentId } }, - { term: { input_type: 'endpoint' } }, - { term: { type: 'INPUT_ACTION' } }, - ]; - const actionsFilters = [...baseActionFilters, ...dateFilters]; - actionsResult = await esClient.search( - { - index: AGENT_ACTIONS_INDEX, - size, - from, - body: { - query: { - bool: { - // @ts-ignore - filter: actionsFilters, - }, - }, - sort: [ - { - '@timestamp': { - order: 'desc', - }, - }, - ], - }, - }, - options - ); - const actionIds = actionsResult?.body?.hits?.hits?.map( - (e) => (e._source as EndpointAction).action_id - ); + const { actionIds, actionRequests } = await getActionRequestsResult({ + context, + logger, + elasticAgentId, + startDate, + endDate, + size, + from, + }); + actionsResult = actionRequests; - // fetch responses with matching `action_id`s - const baseResponsesFilter = [ - { term: { agent_id: elasticAgentId } }, - { terms: { action_id: actionIds } }, - ]; - const responsesFilters = [...baseResponsesFilter, ...dateFilters]; - responsesResult = await esClient.search( - { - index: AGENT_ACTIONS_RESULTS_INDEX, - size: 1000, - body: { - query: { - bool: { - filter: responsesFilters, - }, - }, - }, - }, - options - ); + // fetch responses with matching unique set of `action_id`s + responsesResult = await getActionResponsesResult({ + actionIds: [...new Set(actionIds)], // de-dupe `action_id`s + context, + logger, + elasticAgentId, + startDate, + endDate, + }); } catch (error) { logger.error(error); throw error; @@ -153,21 +122,26 @@ const getActivityLog = async ({ throw new Error(`Error fetching actions log for agent_id ${elasticAgentId}`); } - const responses = responsesResult?.body?.hits?.hits?.length - ? responsesResult?.body?.hits?.hits?.map((e) => ({ - type: 'response', - item: { id: e._id, data: e._source }, - })) - : []; - const actions = actionsResult?.body?.hits?.hits?.length - ? actionsResult?.body?.hits?.hits?.map((e) => ({ - type: 'action', - item: { id: e._id, data: e._source }, - })) - : []; - const sortedData = ([...responses, ...actions] as ActivityLog['data']).sort((a, b) => - new Date(b.item.data['@timestamp']) > new Date(a.item.data['@timestamp']) ? 1 : -1 - ); + // label record as `action`, `fleetAction` + const responses = categorizeResponseResults({ + results: responsesResult?.body?.hits?.hits as Array< + SearchHit + >, + }); + + // label record as `response`, `fleetResponse` + const actions = categorizeActionResults({ + results: actionsResult?.body?.hits?.hits as Array< + SearchHit + >, + }); + + // filter out the duplicate endpoint actions that also have fleetActions + // include endpoint actions that have no fleet actions + const uniqueLogData = getUniqueLogData([...responses, ...actions]); + + // sort by @timestamp in desc order, newest first + const sortedData = getTimeSortedData(uniqueLogData); return sortedData; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/audit_log_helpers.ts b/x-pack/plugins/security_solution/server/endpoint/utils/audit_log_helpers.ts new file mode 100644 index 0000000000000..f75b265bf24d7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/utils/audit_log_helpers.ts @@ -0,0 +1,266 @@ +/* + * 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 { Logger } from 'kibana/server'; +import { SearchRequest } from 'src/plugins/data/public'; +import { SearchHit, SearchResponse } from '@elastic/elasticsearch/api/types'; +import { ApiResponse } from '@elastic/elasticsearch'; +import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '../../../../fleet/common'; +import { + ENDPOINT_ACTIONS_INDEX, + ENDPOINT_ACTION_RESPONSES_INDEX, + failedFleetActionErrorCode, +} from '../../../common/endpoint/constants'; +import { SecuritySolutionRequestHandlerContext } from '../../types'; +import { + ActivityLog, + ActivityLogAction, + EndpointActivityLogAction, + ActivityLogActionResponse, + EndpointActivityLogActionResponse, + ActivityLogItemTypes, + EndpointAction, + LogsEndpointAction, + EndpointActionResponse, + LogsEndpointActionResponse, + ActivityLogEntry, +} from '../../../common/endpoint/types'; +import { doesLogsEndpointActionsIndexExist } from '../utils'; + +const actionsIndices = [AGENT_ACTIONS_INDEX, ENDPOINT_ACTIONS_INDEX]; +const responseIndices = [AGENT_ACTIONS_RESULTS_INDEX, ENDPOINT_ACTION_RESPONSES_INDEX]; +export const logsEndpointActionsRegex = new RegExp(`(^\.ds-\.logs-endpoint\.actions-default-).+`); +export const logsEndpointResponsesRegex = new RegExp( + `(^\.ds-\.logs-endpoint\.action\.responses-default-).+` +); +const queryOptions = { + headers: { + 'X-elastic-product-origin': 'fleet', + }, + ignore: [404], +}; + +const getDateFilters = ({ startDate, endDate }: { startDate: string; endDate: string }) => { + return [ + { range: { '@timestamp': { gte: startDate } } }, + { range: { '@timestamp': { lte: endDate } } }, + ]; +}; + +export const getUniqueLogData = (activityLogEntries: ActivityLogEntry[]): ActivityLogEntry[] => { + // find the error responses for actions that didn't make it to fleet index + const onlyResponsesForFleetErrors = activityLogEntries + .filter( + (e) => + e.type === ActivityLogItemTypes.RESPONSE && + e.item.data.error?.code === failedFleetActionErrorCode + ) + .map( + (e: ActivityLogEntry) => (e.item.data as LogsEndpointActionResponse).EndpointActions.action_id + ); + + // all actions and responses minus endpoint actions. + const nonEndpointActionsDocs = activityLogEntries.filter( + (e) => e.type !== ActivityLogItemTypes.ACTION + ); + + // only endpoint actions that match the error responses + const onlyEndpointActionsDocWithoutFleetActions = activityLogEntries + .filter((e) => e.type === ActivityLogItemTypes.ACTION) + .filter((e: ActivityLogEntry) => + onlyResponsesForFleetErrors.includes( + (e.item.data as LogsEndpointAction).EndpointActions.action_id + ) + ); + + // join the error actions and the rest + return [...nonEndpointActionsDocs, ...onlyEndpointActionsDocWithoutFleetActions]; +}; + +export const categorizeResponseResults = ({ + results, +}: { + results: Array>; +}): Array => { + return results?.length + ? results?.map((e) => { + const isResponseDoc: boolean = matchesIndexPattern({ + regexPattern: logsEndpointResponsesRegex, + index: e._index, + }); + return isResponseDoc + ? { + type: ActivityLogItemTypes.RESPONSE, + item: { id: e._id, data: e._source as LogsEndpointActionResponse }, + } + : { + type: ActivityLogItemTypes.FLEET_RESPONSE, + item: { id: e._id, data: e._source as EndpointActionResponse }, + }; + }) + : []; +}; + +export const categorizeActionResults = ({ + results, +}: { + results: Array>; +}): Array => { + return results?.length + ? results?.map((e) => { + const isActionDoc: boolean = matchesIndexPattern({ + regexPattern: logsEndpointActionsRegex, + index: e._index, + }); + return isActionDoc + ? { + type: ActivityLogItemTypes.ACTION, + item: { id: e._id, data: e._source as LogsEndpointAction }, + } + : { + type: ActivityLogItemTypes.FLEET_ACTION, + item: { id: e._id, data: e._source as EndpointAction }, + }; + }) + : []; +}; + +export const getTimeSortedData = (data: ActivityLog['data']): ActivityLog['data'] => { + return data.sort((a, b) => + new Date(b.item.data['@timestamp']) > new Date(a.item.data['@timestamp']) ? 1 : -1 + ); +}; + +export const getActionRequestsResult = async ({ + context, + logger, + elasticAgentId, + startDate, + endDate, + size, + from, +}: { + context: SecuritySolutionRequestHandlerContext; + logger: Logger; + elasticAgentId: string; + startDate: string; + endDate: string; + size: number; + from: number; +}): Promise<{ + actionIds: string[]; + actionRequests: ApiResponse, unknown>; +}> => { + const dateFilters = getDateFilters({ startDate, endDate }); + const baseActionFilters = [ + { term: { agents: elasticAgentId } }, + { term: { input_type: 'endpoint' } }, + { term: { type: 'INPUT_ACTION' } }, + ]; + const actionsFilters = [...baseActionFilters, ...dateFilters]; + + const hasLogsEndpointActionsIndex = await doesLogsEndpointActionsIndexExist({ + context, + logger, + indexName: ENDPOINT_ACTIONS_INDEX, + }); + + const actionsSearchQuery: SearchRequest = { + index: hasLogsEndpointActionsIndex ? actionsIndices : AGENT_ACTIONS_INDEX, + size, + from, + body: { + query: { + bool: { + filter: actionsFilters, + }, + }, + sort: [ + { + '@timestamp': { + order: 'desc', + }, + }, + ], + }, + }; + + let actionRequests: ApiResponse, unknown>; + try { + const esClient = context.core.elasticsearch.client.asCurrentUser; + actionRequests = await esClient.search(actionsSearchQuery, queryOptions); + const actionIds = actionRequests?.body?.hits?.hits?.map((e) => { + return logsEndpointActionsRegex.test(e._index) + ? (e._source as LogsEndpointAction).EndpointActions.action_id + : (e._source as EndpointAction).action_id; + }); + + return { actionIds, actionRequests }; + } catch (error) { + logger.error(error); + throw error; + } +}; + +export const getActionResponsesResult = async ({ + context, + logger, + elasticAgentId, + actionIds, + startDate, + endDate, +}: { + context: SecuritySolutionRequestHandlerContext; + logger: Logger; + elasticAgentId: string; + actionIds: string[]; + startDate: string; + endDate: string; +}): Promise, unknown>> => { + const dateFilters = getDateFilters({ startDate, endDate }); + const baseResponsesFilter = [ + { term: { agent_id: elasticAgentId } }, + { terms: { action_id: actionIds } }, + ]; + const responsesFilters = [...baseResponsesFilter, ...dateFilters]; + + const hasLogsEndpointActionResponsesIndex = await doesLogsEndpointActionsIndexExist({ + context, + logger, + indexName: ENDPOINT_ACTION_RESPONSES_INDEX, + }); + + const responsesSearchQuery: SearchRequest = { + index: hasLogsEndpointActionResponsesIndex ? responseIndices : AGENT_ACTIONS_RESULTS_INDEX, + size: 1000, + body: { + query: { + bool: { + filter: responsesFilters, + }, + }, + }, + }; + + let actionResponses: ApiResponse, unknown>; + try { + const esClient = context.core.elasticsearch.client.asCurrentUser; + actionResponses = await esClient.search(responsesSearchQuery, queryOptions); + } catch (error) { + logger.error(error); + throw error; + } + return actionResponses; +}; + +const matchesIndexPattern = ({ + regexPattern, + index, +}: { + regexPattern: RegExp; + index: string; +}): boolean => regexPattern.test(index); diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/index.ts b/x-pack/plugins/security_solution/server/endpoint/utils/index.ts index 34cabf79aff0e..6c40073f8c654 100644 --- a/x-pack/plugins/security_solution/server/endpoint/utils/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/utils/index.ts @@ -7,3 +7,5 @@ export * from './fleet_agent_status_to_endpoint_host_status'; export * from './wrap_errors'; +export * from './audit_log_helpers'; +export * from './yes_no_data_stream'; diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.test.ts b/x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.test.ts new file mode 100644 index 0000000000000..d2894c8c64c14 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.test.ts @@ -0,0 +1,100 @@ +/* + * 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 { + elasticsearchServiceMock, + savedObjectsClientMock, + loggingSystemMock, +} from 'src/core/server/mocks'; +import { SecuritySolutionRequestHandlerContext } from '../../types'; +import { createRouteHandlerContext } from '../mocks'; +import { + doLogsEndpointActionDsExists, + doesLogsEndpointActionsIndexExist, +} from './yes_no_data_stream'; + +describe('Accurately answers if index template for data stream exists', () => { + let ctxt: jest.Mocked; + + beforeEach(() => { + ctxt = createRouteHandlerContext( + elasticsearchServiceMock.createScopedClusterClient(), + savedObjectsClientMock.create() + ); + }); + + const mockEsApiResponse = (response: { body: boolean; statusCode: number }) => { + return jest.fn().mockImplementationOnce(() => Promise.resolve(response)); + }; + + it('Returns FALSE for a non-existent data stream index template', async () => { + ctxt.core.elasticsearch.client.asInternalUser.indices.existsIndexTemplate = mockEsApiResponse({ + body: false, + statusCode: 404, + }); + const doesItExist = await doLogsEndpointActionDsExists({ + context: ctxt, + logger: loggingSystemMock.create().get('host-isolation'), + dataStreamName: '.test-stream.name', + }); + expect(doesItExist).toBeFalsy(); + }); + + it('Returns TRUE for an existing index', async () => { + ctxt.core.elasticsearch.client.asInternalUser.indices.existsIndexTemplate = mockEsApiResponse({ + body: true, + statusCode: 200, + }); + const doesItExist = await doLogsEndpointActionDsExists({ + context: ctxt, + logger: loggingSystemMock.create().get('host-isolation'), + dataStreamName: '.test-stream.name', + }); + expect(doesItExist).toBeTruthy(); + }); +}); + +describe('Accurately answers if index exists', () => { + let ctxt: jest.Mocked; + + beforeEach(() => { + ctxt = createRouteHandlerContext( + elasticsearchServiceMock.createScopedClusterClient(), + savedObjectsClientMock.create() + ); + }); + + const mockEsApiResponse = (response: { body: boolean; statusCode: number }) => { + return jest.fn().mockImplementationOnce(() => Promise.resolve(response)); + }; + + it('Returns FALSE for a non-existent index', async () => { + ctxt.core.elasticsearch.client.asInternalUser.indices.exists = mockEsApiResponse({ + body: false, + statusCode: 404, + }); + const doesItExist = await doesLogsEndpointActionsIndexExist({ + context: ctxt, + logger: loggingSystemMock.create().get('host-isolation'), + indexName: '.test-index.name-default', + }); + expect(doesItExist).toBeFalsy(); + }); + + it('Returns TRUE for an existing index', async () => { + ctxt.core.elasticsearch.client.asInternalUser.indices.exists = mockEsApiResponse({ + body: true, + statusCode: 200, + }); + const doesItExist = await doesLogsEndpointActionsIndexExist({ + context: ctxt, + logger: loggingSystemMock.create().get('host-isolation'), + indexName: '.test-index.name-default', + }); + expect(doesItExist).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.ts b/x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.ts new file mode 100644 index 0000000000000..dea2e46c3c258 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.ts @@ -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 { Logger } from 'src/core/server'; +import { SecuritySolutionRequestHandlerContext } from '../../types'; + +export const doLogsEndpointActionDsExists = async ({ + context, + logger, + dataStreamName, +}: { + context: SecuritySolutionRequestHandlerContext; + logger: Logger; + dataStreamName: string; +}): Promise => { + try { + const esClient = context.core.elasticsearch.client.asInternalUser; + const doesIndexTemplateExist = await esClient.indices.existsIndexTemplate({ + name: dataStreamName, + }); + return doesIndexTemplateExist.statusCode === 404 ? false : true; + } catch (error) { + const errorType = error?.type ?? ''; + if (errorType !== 'resource_not_found_exception') { + logger.error(error); + throw error; + } + return false; + } +}; + +export const doesLogsEndpointActionsIndexExist = async ({ + context, + logger, + indexName, +}: { + context: SecuritySolutionRequestHandlerContext; + logger: Logger; + indexName: string; +}): Promise => { + try { + const esClient = context.core.elasticsearch.client.asInternalUser; + const doesIndexExist = await esClient.indices.exists({ + index: indexName, + }); + return doesIndexExist.statusCode === 404 ? false : true; + } catch (error) { + const errorType = error?.type ?? ''; + if (errorType !== 'index_not_found_exception') { + logger.error(error); + throw error; + } + return false; + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts index 81f229c636bd8..964df3c91eb08 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts @@ -278,7 +278,7 @@ describe('schedule_throttle_notification_actions', () => { }); expect(logger.error).toHaveBeenCalledWith( - 'The notification throttle "from" and/or "to" range values could not be constructed as valid. Tried to construct the values of "from": now-invalid "to": 2021-08-24T19:19:22.094Z. This will cause a reset of the notification throttle. Expect either missing alert notifications or alert notifications happening earlier than expected.' + 'The notification throttle "from" and/or "to" range values could not be constructed as valid. Tried to construct the values of "from": now-invalid "to": 2021-08-24T19:19:22.094Z. This will cause a reset of the notification throttle. Expect either missing alert notifications or alert notifications happening earlier than expected. Check your rule with ruleId: "rule-123" for data integrity issues' ); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts index 5bf18496e6375..7b4b314cc8911 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts @@ -145,6 +145,7 @@ export const scheduleThrottledNotificationActions = async ({ ` "from": now-${throttle}`, ` "to": ${startedAt.toISOString()}.`, ' This will cause a reset of the notification throttle. Expect either missing alert notifications or alert notifications happening earlier than expected.', + ` Check your rule with ruleId: "${ruleId}" for data integrity issues`, ].join('') ); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_saved_object_mappings.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_saved_object_mappings.ts index 3d6a405225fe6..835ccd92f9cc4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_saved_object_mappings.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_saved_object_mappings.ts @@ -59,9 +59,10 @@ const legacyRuleActionsSavedObjectMappings: SavedObjectsType['mappings'] = { * @deprecated Remove this once we no longer need legacy migrations for rule actions (8.0.0) */ export const legacyType: SavedObjectsType = { + convertToMultiNamespaceTypeVersion: '8.0.0', name: legacyRuleActionsSavedObjectType, hidden: false, - namespaceType: 'single', + namespaceType: 'multiple-isolated', mappings: legacyRuleActionsSavedObjectMappings, migrations: legacyRuleActionsSavedObjectMigration, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index 0ad416e86e31a..0fe7cbdc9bd9f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -375,20 +375,22 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = ); } else { // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early - await scheduleThrottledNotificationActions({ - alertInstance: services.alertInstanceFactory(alertId), - throttle: ruleSO.attributes.throttle, - startedAt, - id: ruleSO.id, - kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) - ?.kibana_siem_app_url, - outputIndex: ruleDataClient.indexName, - ruleId, - esClient: services.scopedClusterClient.asCurrentUser, - notificationRuleParams, - signals: result.createdSignals, - logger, - }); + if (ruleSO.attributes.throttle != null) { + await scheduleThrottledNotificationActions({ + alertInstance: services.alertInstanceFactory(alertId), + throttle: ruleSO.attributes.throttle, + startedAt, + id: ruleSO.id, + kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) + ?.kibana_siem_app_url, + outputIndex: ruleDataClient.indexName, + ruleId, + esClient: services.scopedClusterClient.asCurrentUser, + notificationRuleParams, + signals: result.createdSignals, + logger, + }); + } const errorMessage = buildRuleMessage( 'Bulk Indexing of signals failed:', truncateMessageList(result.errors).join() @@ -407,20 +409,22 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = } } catch (error) { // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early - await scheduleThrottledNotificationActions({ - alertInstance: services.alertInstanceFactory(alertId), - throttle: ruleSO.attributes.throttle, - startedAt, - id: ruleSO.id, - kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) - ?.kibana_siem_app_url, - outputIndex: ruleDataClient.indexName, - ruleId, - esClient: services.scopedClusterClient.asCurrentUser, - notificationRuleParams, - signals: result.createdSignals, - logger, - }); + if (ruleSO.attributes.throttle != null) { + await scheduleThrottledNotificationActions({ + alertInstance: services.alertInstanceFactory(alertId), + throttle: ruleSO.attributes.throttle, + startedAt, + id: ruleSO.id, + kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) + ?.kibana_siem_app_url, + outputIndex: ruleDataClient.indexName, + ruleId, + esClient: services.scopedClusterClient.asCurrentUser, + notificationRuleParams, + signals: result.createdSignals, + logger, + }); + } const errorMessage = error.message ?? '(no error message given)'; const message = buildRuleMessage( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_migrations.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_migrations.ts index 92d7487be0cdb..72ab4a2237ba1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_migrations.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_migrations.ts @@ -18,34 +18,19 @@ import { IRuleSavedAttributesSavedObjectAttributes } from '../types'; import { legacyGetRuleReference } from './legacy_utils'; export const truncateMessageFields: SavedObjectMigrationFn> = (doc) => { - const { lastFailureMessage, lastSuccessMessage, ...restAttributes } = doc.attributes; + const { lastFailureMessage, lastSuccessMessage, ...otherAttributes } = doc.attributes; return { ...doc, attributes: { + ...otherAttributes, lastFailureMessage: truncateMessage(lastFailureMessage), lastSuccessMessage: truncateMessage(lastSuccessMessage), - ...restAttributes, }, references: doc.references ?? [], }; }; -/** - * This side-car rule status SO is deprecated and is to be replaced by the RuleExecutionLog on Event-Log and - * additional fields on the Alerting Framework Rule SO. - * - * @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x) - */ -export const legacyRuleStatusSavedObjectMigration = { - '7.15.2': truncateMessageFields, - '7.16.0': ( - doc: SavedObjectUnsanitizedDoc - ): SavedObjectSanitizedDoc => { - return legacyMigrateRuleAlertIdSOReferences(doc); - }, -}; - /** * This migrates alertId within legacy `siem-detection-engine-rule-status` to saved object references on an upgrade. * We only migrate alertId if we find these conditions: @@ -62,29 +47,24 @@ export const legacyRuleStatusSavedObjectMigration = { export const legacyMigrateRuleAlertIdSOReferences = ( doc: SavedObjectUnsanitizedDoc ): SavedObjectSanitizedDoc => { - const { references } = doc; - - // Isolate alertId from the doc - const { alertId, ...attributesWithoutAlertId } = doc.attributes; - const existingReferences = references ?? []; + const { alertId, ...otherAttributes } = doc.attributes; + const existingReferences = doc.references ?? []; + // early return if alertId is not a string as expected if (!isString(alertId)) { - // early return if alertId is not a string as expected return { ...doc, references: existingReferences }; - } else { - const alertReferences = legacyMigrateAlertId({ - alertId, - existingReferences, - }); - - return { - ...doc, - attributes: { - ...attributesWithoutAlertId.attributes, - }, - references: [...existingReferences, ...alertReferences], - }; } + + const alertReferences = legacyMigrateAlertId({ + alertId, + existingReferences, + }); + + return { + ...doc, + attributes: otherAttributes, + references: [...existingReferences, ...alertReferences], + }; }; /** @@ -113,3 +93,14 @@ export const legacyMigrateAlertId = ({ return [legacyGetRuleReference(alertId)]; } }; + +/** + * This side-car rule status SO is deprecated and is to be replaced by the RuleExecutionLog on Event-Log and + * additional fields on the Alerting Framework Rule SO. + * + * @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x) + */ +export const legacyRuleStatusSavedObjectMigration = { + '7.15.2': truncateMessageFields, + '7.16.0': legacyMigrateRuleAlertIdSOReferences, +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_rule_status_saved_object_mappings.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_rule_status_saved_object_mappings.ts index 3fe3fc06cc7d6..298c75b8b7d51 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_rule_status_saved_object_mappings.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_rule_status_saved_object_mappings.ts @@ -65,9 +65,10 @@ export const ruleStatusSavedObjectMappings: SavedObjectsType['mappings'] = { * @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x) */ export const legacyRuleStatusType: SavedObjectsType = { + convertToMultiNamespaceTypeVersion: '8.0.0', name: legacyRuleStatusSavedObjectType, hidden: false, - namespaceType: 'single', + namespaceType: 'multiple-isolated', mappings: ruleStatusSavedObjectMappings, migrations: legacyRuleStatusSavedObjectMigration, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/legacy_notifications/one_action.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/legacy_notifications/one_action.json index 1966dcf5ff53c..bf980e370e3a3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/legacy_notifications/one_action.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/legacy_notifications/one_action.json @@ -3,7 +3,7 @@ "interval": "1m", "actions": [ { - "id": "42534430-2092-11ec-99a6-05d79563c01a", + "id": "1fa31c30-3046-11ec-8971-1f3f7bae65af", "group": "default", "params": { "message": "Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 88b276358a705..6a84776ccee5d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -536,14 +536,74 @@ describe('signal_rule_alert_type', () => { errors: ['Error that bubbled up.'], }; (queryExecutor as jest.Mock).mockResolvedValue(result); - await alert.executor(payload); + const ruleAlert = getAlertMock(false, getQueryRuleParams()); + ruleAlert.throttle = '1h'; + const payLoadWithThrottle = getPayload( + ruleAlert, + alertServices + ) as jest.Mocked; + payLoadWithThrottle.rule.throttle = '1h'; + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + await alert.executor(payLoadWithThrottle); expect(scheduleThrottledNotificationActions).toHaveBeenCalledTimes(1); }); + it('should NOT call scheduleThrottledNotificationActions if result is false and the throttle is not set', async () => { + const result: SearchAfterAndBulkCreateReturnType = { + success: false, + warning: false, + searchAfterTimes: [], + bulkCreateTimes: [], + lastLookBackDate: null, + createdSignalsCount: 0, + createdSignals: [], + warningMessages: [], + errors: ['Error that bubbled up.'], + }; + (queryExecutor as jest.Mock).mockResolvedValue(result); + await alert.executor(payload); + expect(scheduleThrottledNotificationActions).toHaveBeenCalledTimes(0); + }); + it('should call scheduleThrottledNotificationActions if an error was thrown to prevent the throttle from being reset', async () => { (queryExecutor as jest.Mock).mockRejectedValue({}); - await alert.executor(payload); + const ruleAlert = getAlertMock(false, getQueryRuleParams()); + ruleAlert.throttle = '1h'; + const payLoadWithThrottle = getPayload( + ruleAlert, + alertServices + ) as jest.Mocked; + payLoadWithThrottle.rule.throttle = '1h'; + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + await alert.executor(payLoadWithThrottle); expect(scheduleThrottledNotificationActions).toHaveBeenCalledTimes(1); }); + + it('should NOT call scheduleThrottledNotificationActions if an error was thrown to prevent the throttle from being reset if throttle is not defined', async () => { + const result: SearchAfterAndBulkCreateReturnType = { + success: false, + warning: false, + searchAfterTimes: [], + bulkCreateTimes: [], + lastLookBackDate: null, + createdSignalsCount: 0, + createdSignals: [], + warningMessages: [], + errors: ['Error that bubbled up.'], + }; + (queryExecutor as jest.Mock).mockRejectedValue(result); + await alert.executor(payload); + expect(scheduleThrottledNotificationActions).toHaveBeenCalledTimes(0); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 4e98bee83aeb5..90220814fb928 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -474,21 +474,22 @@ export const signalRulesAlertType = ({ ); } else { // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early - await scheduleThrottledNotificationActions({ - alertInstance: services.alertInstanceFactory(alertId), - throttle: savedObject.attributes.throttle, - startedAt, - id: savedObject.id, - kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) - ?.kibana_siem_app_url, - outputIndex, - ruleId, - signals: result.createdSignals, - esClient: services.scopedClusterClient.asCurrentUser, - notificationRuleParams, - logger, - }); - + if (savedObject.attributes.throttle != null) { + await scheduleThrottledNotificationActions({ + alertInstance: services.alertInstanceFactory(alertId), + throttle: savedObject.attributes.throttle, + startedAt, + id: savedObject.id, + kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) + ?.kibana_siem_app_url, + outputIndex, + ruleId, + signals: result.createdSignals, + esClient: services.scopedClusterClient.asCurrentUser, + notificationRuleParams, + logger, + }); + } const errorMessage = buildRuleMessage( 'Bulk Indexing of signals failed:', truncateMessageList(result.errors).join() @@ -507,20 +508,22 @@ export const signalRulesAlertType = ({ } } catch (error) { // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early - await scheduleThrottledNotificationActions({ - alertInstance: services.alertInstanceFactory(alertId), - throttle: savedObject.attributes.throttle, - startedAt, - id: savedObject.id, - kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) - ?.kibana_siem_app_url, - outputIndex, - ruleId, - signals: result.createdSignals, - esClient: services.scopedClusterClient.asCurrentUser, - notificationRuleParams, - logger, - }); + if (savedObject.attributes.throttle != null) { + await scheduleThrottledNotificationActions({ + alertInstance: services.alertInstanceFactory(alertId), + throttle: savedObject.attributes.throttle, + startedAt, + id: savedObject.id, + kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) + ?.kibana_siem_app_url, + outputIndex, + ruleId, + signals: result.createdSignals, + esClient: services.scopedClusterClient.asCurrentUser, + notificationRuleParams, + logger, + }); + } const errorMessage = error.message ?? '(no error message given)'; const message = buildRuleMessage( 'An error occurred during rule execution:', diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3df5e4ee6c48a..852b01977b78b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3009,11 +3009,7 @@ "home.tutorial.savedObject.unableToAddErrorMessage": "{savedObjectsLength} 件中 {errorsLength} 件の kibana オブジェクトが追加できません。エラー:{errorMessage}", "home.tutorial.selectionLegend": "デプロイタイプ", "home.tutorial.selfManagedButtonLabel": "自己管理", - "home.tutorial.tabs.allTitle": "すべて", - "home.tutorial.tabs.loggingTitle": "ログ", - "home.tutorial.tabs.metricsTitle": "メトリック", "home.tutorial.tabs.sampleDataTitle": "サンプルデータ", - "home.tutorial.tabs.securitySolutionTitle": "セキュリティ", "home.tutorial.unexpectedStatusCheckStateErrorDescription": "予期せぬステータス確認ステータス {statusCheckState}", "home.tutorial.unhandledInstructionTypeErrorDescription": "予期せぬ指示タイプ {visibleInstructions}", "home.tutorialDirectory.featureCatalogueDescription": "一般的なアプリやサービスからデータを取り込みます。", @@ -4204,8 +4200,6 @@ "kibana-react.noDataPage.cantDecide.link": "詳細については、ドキュメントをご確認ください。", "kibana-react.noDataPage.elasticAgentCard.description": "Elasticエージェントを使用すると、シンプルで統一された方法でコンピューターからデータを収集するできます。", "kibana-react.noDataPage.elasticAgentCard.title": "Elasticエージェントの追加", - "kibana-react.noDataPage.elasticBeatsCard.description": "Beatsを使用して、さまざまなシステムのデータをElasticsearchに追加します。", - "kibana-react.noDataPage.elasticBeatsCard.title": "データの追加", "kibana-react.noDataPage.intro": "データを追加して開始するか、{solution}については{link}をご覧ください。", "kibana-react.noDataPage.intro.link": "詳細", "kibana-react.noDataPage.noDataPage.recommended": "推奨", @@ -11076,12 +11070,7 @@ "xpack.fleet.fleetServerUpgradeModal.modalTitle": "エージェントをFleetサーバーに登録", "xpack.fleet.fleetServerUpgradeModal.onPremDescriptionMessage": "Fleetサーバーが使用できます。スケーラビリティとセキュリティが改善されています。{existingAgentsMessage} Fleetを使用し続けるには、Fleetサーバーと新しいバージョンのElasticエージェントを各ホストにインストールする必要があります。詳細については、{link}をご覧ください。", "xpack.fleet.genericActionsMenuText": "開く", - "xpack.fleet.homeIntegration.tutorialDirectory.dismissNoticeButtonText": "メッセージを消去", "xpack.fleet.homeIntegration.tutorialDirectory.fleetAppButtonText": "統合を試す", - "xpack.fleet.homeIntegration.tutorialDirectory.noticeText": "Elasticエージェント統合では、シンプルかつ統合された方法で、ログ、メトリック、他の種類のデータの監視をホストに追加することができます。複数のBeatsをインストールする必要はありません。このため、インフラストラクチャ全体でのポリシーのデプロイが簡単で高速になりました。詳細については、{blogPostLink}をお読みください。", - "xpack.fleet.homeIntegration.tutorialDirectory.noticeText.blogPostLink": "発表ブログ投稿", - "xpack.fleet.homeIntegration.tutorialDirectory.noticeTitle": "{newPrefix} Elasticエージェント統合", - "xpack.fleet.homeIntegration.tutorialDirectory.noticeTitle.newPrefix": "一般公開へ:", "xpack.fleet.homeIntegration.tutorialModule.noticeText": "{notePrefix}このモジュールの新しいバージョンは{availableAsIntegrationLink}です。統合と新しいElasticエージェントの詳細については、{blogPostLink}をお読みください。", "xpack.fleet.homeIntegration.tutorialModule.noticeText.blogPostLink": "発表ブログ投稿", "xpack.fleet.homeIntegration.tutorialModule.noticeText.integrationLink": "Elasticエージェント統合として提供", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d9af3edb8101d..9d88c757f1e58 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3038,11 +3038,7 @@ "home.tutorial.savedObject.unableToAddErrorMessage": "{savedObjectsLength} 个 kibana 对象中有 {errorsLength} 个无法添加,错误:{errorMessage}", "home.tutorial.selectionLegend": "部署类型", "home.tutorial.selfManagedButtonLabel": "自管型", - "home.tutorial.tabs.allTitle": "全部", - "home.tutorial.tabs.loggingTitle": "日志", - "home.tutorial.tabs.metricsTitle": "指标", "home.tutorial.tabs.sampleDataTitle": "样例数据", - "home.tutorial.tabs.securitySolutionTitle": "安全", "home.tutorial.unexpectedStatusCheckStateErrorDescription": "意外的状态检查状态 {statusCheckState}", "home.tutorial.unhandledInstructionTypeErrorDescription": "未处理的指令类型 {visibleInstructions}", "home.tutorialDirectory.featureCatalogueDescription": "从热门应用和服务中采集数据。", @@ -4244,8 +4240,6 @@ "kibana-react.noDataPage.cantDecide.link": "请参阅我们的文档以了解更多信息。", "kibana-react.noDataPage.elasticAgentCard.description": "使用 Elastic 代理以简单统一的方式从您的计算机中收集数据。", "kibana-react.noDataPage.elasticAgentCard.title": "添加 Elastic 代理", - "kibana-react.noDataPage.elasticBeatsCard.description": "使用 Beats 将各种系统的数据添加到 Elasticsearch。", - "kibana-react.noDataPage.elasticBeatsCard.title": "添加数据", "kibana-react.noDataPage.intro": "添加您的数据以开始,或{link}{solution}。", "kibana-react.noDataPage.intro.link": "了解详情", "kibana-react.noDataPage.noDataPage.recommended": "推荐", @@ -11191,12 +11185,7 @@ "xpack.fleet.fleetServerUpgradeModal.modalTitle": "将代理注册到 Fleet 服务器", "xpack.fleet.fleetServerUpgradeModal.onPremDescriptionMessage": "Fleet 服务器现在可用且提供改善的可扩展性和安全性。{existingAgentsMessage}要继续使用 Fleet,必须在各个主机上安装 Fleet 服务器和新版 Elastic 代理。详细了解我们的 {link}。", "xpack.fleet.genericActionsMenuText": "打开", - "xpack.fleet.homeIntegration.tutorialDirectory.dismissNoticeButtonText": "关闭消息", "xpack.fleet.homeIntegration.tutorialDirectory.fleetAppButtonText": "试用集成", - "xpack.fleet.homeIntegration.tutorialDirectory.noticeText": "通过 Elastic 代理集成,可以简单统一的方式将日志、指标和其他类型数据的监测添加到主机。不再需要安装多个 Beats,这样将策略部署到整个基础架构更容易也更快速。有关更多信息,请阅读我们的{blogPostLink}。", - "xpack.fleet.homeIntegration.tutorialDirectory.noticeText.blogPostLink": "公告博客", - "xpack.fleet.homeIntegration.tutorialDirectory.noticeTitle": "{newPrefix}Elastic 代理集成", - "xpack.fleet.homeIntegration.tutorialDirectory.noticeTitle.newPrefix": "已正式发布:", "xpack.fleet.homeIntegration.tutorialModule.noticeText": "{notePrefix}此模块的较新版本{availableAsIntegrationLink}。要详细了解集成和新 Elastic 代理,请阅读我们的{blogPostLink}。", "xpack.fleet.homeIntegration.tutorialModule.noticeText.blogPostLink": "公告博客", "xpack.fleet.homeIntegration.tutorialModule.noticeText.integrationLink": "将作为 Elastic 代理集成来提供", diff --git a/x-pack/plugins/uptime/public/apps/use_no_data_config.ts b/x-pack/plugins/uptime/public/apps/use_no_data_config.ts index dc00a25e3a111..6e73a6d5e8268 100644 --- a/x-pack/plugins/uptime/public/apps/use_no_data_config.ts +++ b/x-pack/plugins/uptime/public/apps/use_no_data_config.ts @@ -31,13 +31,13 @@ export function useNoDataConfig(): KibanaPageTemplateProps['noDataConfig'] { actions: { beats: { title: i18n.translate('xpack.uptime.noDataConfig.beatsCard.title', { - defaultMessage: 'Add monitors with Heartbeat', + defaultMessage: 'Add monitors with the Elastic Synthetics integration', }), description: i18n.translate('xpack.uptime.noDataConfig.beatsCard.description', { defaultMessage: 'Proactively monitor the availability of your sites and services. Receive alerts and resolve issues faster to optimize your users experience.', }), - href: basePath + `/app/home#/tutorial/uptimeMonitors`, + href: basePath + `/app/integrations/detail/synthetics/overview`, }, }, docsLink: docLinks!.links.observability.guide, diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx index 0265588c3fdeb..76b9378ca4ff6 100644 --- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx @@ -45,11 +45,13 @@ describe('ActionMenuContent', () => { it('renders Add Data link', () => { const { getByLabelText, getByText } = render(); - const addDataAnchor = getByLabelText('Navigate to a tutorial about adding Uptime data'); + const addDataAnchor = getByLabelText( + 'Navigate to the Elastic Synthetics integration to add Uptime data' + ); // this href value is mocked, so it doesn't correspond to the real link // that Kibana core services will provide - expect(addDataAnchor.getAttribute('href')).toBe('/home#/tutorial/uptimeMonitors'); + expect(addDataAnchor.getAttribute('href')).toBe('/integrations/detail/synthetics/overview'); expect(getByText('Add data')); }); }); diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx index 21ef3428696e9..789953258750b 100644 --- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx @@ -56,9 +56,8 @@ export function ActionMenuContent(): React.ReactElement { time: { from: dateRangeStart, to: dateRangeEnd }, breakdown: monitorId ? 'observer.geo.name' : 'monitor.type', reportDefinitions: { - 'monitor.name': selectedMonitor?.monitor?.name - ? [selectedMonitor?.monitor?.name] - : ['ALL_VALUES'], + 'monitor.name': selectedMonitor?.monitor?.name ? [selectedMonitor?.monitor?.name] : [], + 'url.full': ['ALL_VALUES'], }, name: monitorId ? `${monitorId}-response-duration` : 'All monitors response duration', }, @@ -100,9 +99,11 @@ export function ActionMenuContent(): React.ReactElement { diff --git a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx index 26ee26cc8ed7f..62c6f5598adb4 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx @@ -313,11 +313,11 @@ describe.skip('', () => { // resolve errors fireEvent.click(monitorType); - waitFor(() => { - expect(getByText('http')).toBeInTheDocument(); - expect(getByText('tcp')).toBeInTheDocument(); - expect(getByText('icmp')).toBeInTheDocument(); - expect(queryByText('browser')).not.toBeInTheDocument(); + await waitFor(() => { + expect(getByText('HTTP')).toBeInTheDocument(); + expect(getByText('TCP')).toBeInTheDocument(); + expect(getByText('ICMP')).toBeInTheDocument(); + expect(queryByText('Browser')).not.toBeInTheDocument(); }); }); }); diff --git a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx index ac129bdb327d9..60ccec84c3bb1 100644 --- a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx @@ -83,7 +83,7 @@ const createMockStore = () => { const mockAppUrls: Record = { uptime: '/app/uptime', observability: '/app/observability', - '/home#/tutorial/uptimeMonitors': '/home#/tutorial/uptimeMonitors', + '/integrations/detail/synthetics/overview': '/integrations/detail/synthetics/overview', }; /* default mock core */ diff --git a/x-pack/test/accessibility/apps/home.ts b/x-pack/test/accessibility/apps/home.ts index a7158d9579b60..61297859c29f8 100644 --- a/x-pack/test/accessibility/apps/home.ts +++ b/x-pack/test/accessibility/apps/home.ts @@ -64,33 +64,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await a11y.testAppSnapshot(); }); - it('Add data page meets a11y requirements ', async () => { - await home.clickGoHome(); - await testSubjects.click('homeAddData'); - await a11y.testAppSnapshot(); - }); - - it('Sample data page meets a11y requirements ', async () => { - await testSubjects.click('homeTab-sampleData'); - await a11y.testAppSnapshot(); - }); - - it('click on Add logs panel to open all log examples page meets a11y requirements ', async () => { - await testSubjects.click('sampleDataSetCardlogs'); - await a11y.testAppSnapshot(); - }); - - it('click on ActiveMQ logs panel to open tutorial meets a11y requirements', async () => { - await testSubjects.click('homeTab-all'); - await testSubjects.click('homeSynopsisLinkactivemqlogs'); - await a11y.testAppSnapshot(); - }); - - it('click on cloud tutorial meets a11y requirements', async () => { - await testSubjects.click('onCloudTutorial'); - await a11y.testAppSnapshot(); - }); - it('passes with searchbox open', async () => { await testSubjects.click('nav-search-popover'); await a11y.testAppSnapshot(); diff --git a/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts index 6e2025a7fa2ca..3388d5b4aa379 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts @@ -10,7 +10,7 @@ import expect from '@kbn/expect'; import { IKibanaSearchRequest } from '../../../../../src/plugins/data/common'; import type { FailedTransactionsCorrelationsParams } from '../../../../plugins/apm/common/search_strategies/failed_transactions_correlations/types'; -import type { SearchStrategyClientParams } from '../../../../plugins/apm/common/search_strategies/types'; +import type { RawSearchStrategyClientParams } from '../../../../plugins/apm/common/search_strategies/types'; import { APM_SEARCH_STRATEGIES } from '../../../../plugins/apm/common/search_strategies/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -23,7 +23,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const getRequestBody = () => { const request: IKibanaSearchRequest< - FailedTransactionsCorrelationsParams & SearchStrategyClientParams + FailedTransactionsCorrelationsParams & RawSearchStrategyClientParams > = { params: { environment: 'ENVIRONMENT_ALL', diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency.ts b/x-pack/test/apm_api_integration/tests/correlations/latency.ts index 99aee770c625d..75a4edd447c70 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/latency.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/latency.ts @@ -10,7 +10,7 @@ import expect from '@kbn/expect'; import { IKibanaSearchRequest } from '../../../../../src/plugins/data/common'; import type { LatencyCorrelationsParams } from '../../../../plugins/apm/common/search_strategies/latency_correlations/types'; -import type { SearchStrategyClientParams } from '../../../../plugins/apm/common/search_strategies/types'; +import type { RawSearchStrategyClientParams } from '../../../../plugins/apm/common/search_strategies/types'; import { APM_SEARCH_STRATEGIES } from '../../../../plugins/apm/common/search_strategies/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -22,16 +22,17 @@ export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('legacySupertestAsApmReadUser'); const getRequestBody = () => { - const request: IKibanaSearchRequest = { - params: { - environment: 'ENVIRONMENT_ALL', - start: '2020', - end: '2021', - kuery: '', - percentileThreshold: 95, - analyzeCorrelations: true, - }, - }; + const request: IKibanaSearchRequest = + { + params: { + environment: 'ENVIRONMENT_ALL', + start: '2020', + end: '2021', + kuery: '', + percentileThreshold: 95, + analyzeCorrelations: true, + }, + }; return { batch: [ diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 09f4e2596ea46..f68a49658f2ee 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -175,6 +175,10 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte loadTestFile(require.resolve('./transactions/error_rate')); }); + describe('transactions/latency_overall_distribution', function () { + loadTestFile(require.resolve('./transactions/latency_overall_distribution')); + }); + describe('transactions/latency', function () { loadTestFile(require.resolve('./transactions/latency')); }); diff --git a/x-pack/test/apm_api_integration/tests/transactions/latency_overall_distribution.ts b/x-pack/test/apm_api_integration/tests/transactions/latency_overall_distribution.ts new file mode 100644 index 0000000000000..c915ac8911e37 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/transactions/latency_overall_distribution.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const apmApiClient = getService('apmApiClient'); + + const endpoint = 'GET /internal/apm/latency/overall_distribution'; + + // This matches the parameters used for the other tab's search strategy approach in `../correlations/*`. + const getOptions = () => ({ + params: { + query: { + environment: 'ENVIRONMENT_ALL', + start: '2020', + end: '2021', + kuery: '', + percentileThreshold: '95', + }, + }, + }); + + registry.when( + 'latency overall distribution without data', + { config: 'trial', archives: [] }, + () => { + it('handles the empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint, + ...getOptions(), + }); + + expect(response.status).to.be(200); + expect(response.body?.percentileThresholdValue).to.be(undefined); + expect(response.body?.overallHistogram?.length).to.be(undefined); + }); + } + ); + + registry.when( + 'latency overall distribution with data and default args', + // This uses the same archive used for the other tab's search strategy approach in `../correlations/*`. + { config: 'trial', archives: ['8.0.0'] }, + () => { + it('returns percentileThresholdValue and overall histogram', async () => { + const response = await apmApiClient.readUser({ + endpoint, + ...getOptions(), + }); + + expect(response.status).to.eql(200); + // This matches the values returned for the other tab's search strategy approach in `../correlations/*`. + expect(response.body?.percentileThresholdValue).to.be(1309695.875); + expect(response.body?.overallHistogram?.length).to.be(101); + }); + } + ); +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts index 6d1d64a04cd93..cfae7532ba496 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import { IRuleStatusSOAttributes } from '../../../../plugins/security_solution/server/lib/detection_engine/rules/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -113,6 +114,30 @@ export default ({ getService }: FtrProviderContext): void => { undefined ); }); + + it('migrates legacy siem-detection-engine-rule-status and retains other attributes as the same attributes as before', async () => { + const response = await es.get<{ + 'siem-detection-engine-rule-status': IRuleStatusSOAttributes; + }>({ + index: '.kibana', + id: 'siem-detection-engine-rule-status:d62d2980-27c4-11ec-92b0-f7b47106bb35', + }); + expect(response.statusCode).to.eql(200); + + expect(response.body._source?.['siem-detection-engine-rule-status']).to.eql({ + statusDate: '2021-10-11T20:51:26.622Z', + status: 'succeeded', + lastFailureAt: '2021-10-11T18:10:08.982Z', + lastSuccessAt: '2021-10-11T20:51:26.622Z', + lastFailureMessage: + '4 days (323690920ms) were not queried between this rule execution and the last execution, so signals may have been missed. Consider increasing your look behind time or adding more Kibana instances. name: "Threshy" id: "fb1046a0-0452-11ec-9b15-d13d79d162f3" rule id: "b789c80f-f6d8-41f1-8b4f-b4a23342cde2" signals index: ".siem-signals-spong-default"', + lastSuccessMessage: 'succeeded', + gap: '4 days', + bulkCreateTimeDurations: ['34.49'], + searchAfterTimeDurations: ['62.58'], + lastLookBackDate: null, + }); + }); }); }); }; diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts index 35fe0cdd6da25..2dcf36cc42ae2 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts @@ -24,7 +24,8 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); - describe('test metadata api', () => { + // Failing: See https://github.com/elastic/kibana/issues/115488 + describe.skip('test metadata api', () => { // TODO add this after endpoint package changes are merged and in snapshot // describe('with .metrics-endpoint.metadata_united_default index', () => { // });