diff --git a/packages/elastic-apm-generator/README.md b/packages/elastic-apm-generator/README.md
index e69de29bb2d1d..e43187a8155d3 100644
--- a/packages/elastic-apm-generator/README.md
+++ b/packages/elastic-apm-generator/README.md
@@ -0,0 +1,93 @@
+# @elastic/apm-generator
+
+`@elastic/apm-generator` is an experimental tool to generate synthetic APM data. It is intended to be used for development and testing of the Elastic APM app in Kibana.
+
+At a high-level, the module works by modeling APM events/metricsets with [a fluent API](https://en.wikipedia.org/wiki/Fluent_interface). The models can then be serialized and converted to Elasticsearch documents. In the future we might support APM Server as an output as well.
+
+## Usage
+
+This section assumes that you've installed Kibana's dependencies by running `yarn kbn bootstrap` in the repository's root folder.
+
+This library can currently be used in two ways:
+
+- Imported as a Node.js module, for instance to be used in Kibana's functional test suite.
+- With a command line interface, to index data based on some example scenarios.
+
+### Using the Node.js module
+
+#### Concepts
+
+- `Service`: a logical grouping for a monitored service. A `Service` object contains fields like `service.name`, `service.environment` and `agent.name`.
+- `Instance`: a single instance of a monitored service. E.g., the workload for a monitored service might be spread across multiple containers. An `Instance` object contains fields like `service.node.name` and `container.id`.
+- `Timerange`: an object that will return an array of timestamps based on an interval and a rate. These timestamps can be used to generate events/metricsets.
+- `Transaction`, `Span`, `APMError` and `Metricset`: events/metricsets that occur on an instance. For more background, see the [explanation of the APM data model](https://www.elastic.co/guide/en/apm/get-started/7.15/apm-data-model.html)
+
+
+#### Example
+
+```ts
+import { service, timerange, toElasticsearchOutput } from '@elastic/apm-generator';
+
+const instance = service('synth-go', 'production', 'go')
+ .instance('instance-a');
+
+const from = new Date('2021-01-01T12:00:00.000Z').getTime();
+const to = new Date('2021-01-01T12:00:00.000Z').getTime() - 1;
+
+const traceEvents = timerange(from, to)
+ .interval('1m')
+ .rate(10)
+ .flatMap(timestamp => instance.transaction('GET /api/product/list')
+ .timestamp(timestamp)
+ .duration(1000)
+ .success()
+ .children(
+ instance.span('GET apm-*/_search', 'db', 'elasticsearch')
+ .timestamp(timestamp + 50)
+ .duration(900)
+ .destination('elasticsearch')
+ .success()
+ ).serialize()
+ );
+
+const metricsets = timerange(from, to)
+ .interval('30s')
+ .rate(1)
+ .flatMap(timestamp => instance.appMetrics({
+ 'system.memory.actual.free': 800,
+ 'system.memory.total': 1000,
+ 'system.cpu.total.norm.pct': 0.6,
+ 'system.process.cpu.total.norm.pct': 0.7,
+ }).timestamp(timestamp)
+ .serialize()
+ );
+
+const esEvents = toElasticsearchOutput(traceEvents.concat(metricsets));
+```
+
+#### Generating metricsets
+
+`@elastic/apm-generator` can also automatically generate transaction metrics, span destination metrics and transaction breakdown metrics based on the generated trace events. If we expand on the previous example:
+
+```ts
+import { getTransactionMetrics, getSpanDestinationMetrics, getBreakdownMetrics } from '@elastic/apm-generator';
+
+const esEvents = toElasticsearchOutput([
+ ...traceEvents,
+ ...getTransactionMetrics(traceEvents),
+ ...getSpanDestinationMetrics(traceEvents),
+ ...getBreakdownMetrics(traceEvents)
+]);
+```
+
+### CLI
+
+Via the CLI, you can upload examples. The supported examples are listed in `src/lib/es.ts`. A `--target` option that specifies the Elasticsearch URL should be defined when running the `example` command. Here's an example:
+
+`$ node packages/elastic-apm-generator/src/scripts/es.js example simple-trace --target=http://admin:changeme@localhost:9200`
+
+The following options are supported:
+- `to`: the end of the time range, in ISO format. By default, the current time will be used.
+- `from`: the start of the time range, in ISO format. By default, `to` minus 15 minutes will be used.
+- `apm-server-version`: the version used in the index names bootstrapped by APM Server, e.g. `7.16.0`. __If these indices do not exist, the script will exit with an error. It will not bootstrap the indices itself.__
+
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/discover/public/application/apps/main/components/top_nav/discover_topnav.test.tsx b/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.test.tsx
index 4b572f6e348b8..808346b53304c 100644
--- a/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.test.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.test.tsx
@@ -42,7 +42,7 @@ describe('Discover topnav component', () => {
const props = getProps(true);
const component = shallowWithIntl();
const topMenuConfig = component.props().config.map((obj: TopNavMenuData) => obj.id);
- expect(topMenuConfig).toEqual(['options', 'new', 'save', 'open', 'share', 'inspect']);
+ expect(topMenuConfig).toEqual(['options', 'new', 'open', 'share', 'inspect', 'save']);
});
test('generated config of TopNavMenu config is correct when no discover save permissions are assigned', () => {
diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.test.ts b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.test.ts
index d31ac6e0f2fea..20c5b9bae332d 100644
--- a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.test.ts
+++ b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.test.ts
@@ -53,13 +53,6 @@ test('getTopNavLinks result', () => {
"run": [Function],
"testId": "discoverNewButton",
},
- Object {
- "description": "Save Search",
- "id": "save",
- "label": "Save",
- "run": [Function],
- "testId": "discoverSaveButton",
- },
Object {
"description": "Open Saved Search",
"id": "open",
@@ -81,6 +74,15 @@ test('getTopNavLinks result', () => {
"run": [Function],
"testId": "openInspectorButton",
},
+ Object {
+ "description": "Save Search",
+ "emphasize": true,
+ "iconType": "save",
+ "id": "save",
+ "label": "Save",
+ "run": [Function],
+ "testId": "discoverSaveButton",
+ },
]
`);
});
diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts
index 81be662470306..44d2999947f41 100644
--- a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts
+++ b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts
@@ -76,6 +76,8 @@ export const getTopNavLinks = ({
defaultMessage: 'Save Search',
}),
testId: 'discoverSaveButton',
+ iconType: 'save',
+ emphasize: true,
run: () => onSaveSearch({ savedSearch, services, indexPattern, navigateTo, state }),
};
@@ -153,9 +155,9 @@ export const getTopNavLinks = ({
return [
...(services.capabilities.advancedSettings.save ? [options] : []),
newSearch,
- ...(services.capabilities.discover.save ? [saveSearch] : []),
openSearch,
shareSearch,
inspectSearch,
+ ...(services.capabilities.discover.save ? [saveSearch] : []),
];
};
diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx b/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx
index 18766b5df7f33..25b04e12c650a 100644
--- a/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx
@@ -98,16 +98,19 @@ export async function onSaveSearch({
const onSave = async ({
newTitle,
newCopyOnSave,
+ newDescription,
isTitleDuplicateConfirmed,
onTitleDuplicate,
}: {
newTitle: string;
newCopyOnSave: boolean;
+ newDescription: string;
isTitleDuplicateConfirmed: boolean;
onTitleDuplicate: () => void;
}) => {
const currentTitle = savedSearch.title;
savedSearch.title = newTitle;
+ savedSearch.description = newDescription;
const saveOptions: SaveSavedSearchOptions = {
onTitleDuplicate,
copyOnSave: newCopyOnSave,
@@ -136,14 +139,11 @@ export async function onSaveSearch({
onClose={() => {}}
title={savedSearch.title ?? ''}
showCopyOnSave={!!savedSearch.id}
+ description={savedSearch.description}
objectType={i18n.translate('discover.localMenu.saveSaveSearchObjectType', {
defaultMessage: 'search',
})}
- description={i18n.translate('discover.localMenu.saveSaveSearchDescription', {
- defaultMessage:
- 'Save your Discover search so you can use it in visualizations and dashboards',
- })}
- showDescription={false}
+ showDescription={true}
/>
);
showSaveModal(saveModal, services.core.i18n.Context);
diff --git a/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx
index 8849806cf5959..89c47559d7b4c 100644
--- a/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx
+++ b/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx
@@ -402,6 +402,10 @@ export class SavedSearchEmbeddable
return this.inspectorAdapters;
}
+ public getDescription() {
+ return this.savedSearch.description;
+ }
+
public destroy() {
super.destroy();
if (this.searchProps) {
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_legacy/server/index.ts b/src/plugins/kibana_legacy/server/index.ts
index 15ae8547c73e1..3f731efbfe857 100644
--- a/src/plugins/kibana_legacy/server/index.ts
+++ b/src/plugins/kibana_legacy/server/index.ts
@@ -18,7 +18,10 @@ export const config: PluginConfigDescriptor = {
schema: configSchema,
deprecations: ({ renameFromRoot }) => [
// TODO: Remove deprecation once defaultAppId is deleted
- renameFromRoot('kibana.defaultAppId', 'kibana_legacy.defaultAppId', { silent: true }),
+ renameFromRoot('kibana.defaultAppId', 'kibana_legacy.defaultAppId', {
+ silent: true,
+ level: 'critical',
+ }),
(completeConfig, rootPath, addDeprecation) => {
if (
get(completeConfig, 'kibana.defaultAppId') === undefined &&
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 (
= {
schema: configSchema,
deprecations: ({ renameFromRoot }) => [
- renameFromRoot('metric_vis.enabled', 'vis_type_metric.enabled'),
+ renameFromRoot('metric_vis.enabled', 'vis_type_metric.enabled', { level: 'critical' }),
],
};
diff --git a/src/plugins/vis_types/table/server/index.ts b/src/plugins/vis_types/table/server/index.ts
index eed1134f3ff48..103586305139c 100644
--- a/src/plugins/vis_types/table/server/index.ts
+++ b/src/plugins/vis_types/table/server/index.ts
@@ -15,9 +15,9 @@ import { registerVisTypeTableUsageCollector } from './usage_collector';
export const config: PluginConfigDescriptor = {
schema: configSchema,
deprecations: ({ renameFromRoot, unused }) => [
- renameFromRoot('table_vis.enabled', 'vis_type_table.enabled'),
+ renameFromRoot('table_vis.enabled', 'vis_type_table.enabled', { level: 'critical' }),
// Unused property which should be removed after releasing Kibana v8.0:
- unused('legacyVisEnabled'),
+ unused('legacyVisEnabled', { level: 'critical' }),
],
};
diff --git a/src/plugins/vis_types/tagcloud/server/index.ts b/src/plugins/vis_types/tagcloud/server/index.ts
index 6899a333a8812..35c0a127c873f 100644
--- a/src/plugins/vis_types/tagcloud/server/index.ts
+++ b/src/plugins/vis_types/tagcloud/server/index.ts
@@ -13,7 +13,7 @@ import { configSchema, ConfigSchema } from '../config';
export const config: PluginConfigDescriptor = {
schema: configSchema,
deprecations: ({ renameFromRoot }) => [
- renameFromRoot('tagcloud.enabled', 'vis_type_tagcloud.enabled'),
+ renameFromRoot('tagcloud.enabled', 'vis_type_tagcloud.enabled', { level: 'critical' }),
],
};
diff --git a/src/plugins/vis_types/timelion/server/index.ts b/src/plugins/vis_types/timelion/server/index.ts
index ef7baf981de1a..b5985932188fb 100644
--- a/src/plugins/vis_types/timelion/server/index.ts
+++ b/src/plugins/vis_types/timelion/server/index.ts
@@ -13,12 +13,17 @@ import { TimelionPlugin } from './plugin';
export const config: PluginConfigDescriptor = {
schema: configSchema,
deprecations: ({ renameFromRoot, unused }) => [
- renameFromRoot('timelion_vis.enabled', 'vis_type_timelion.enabled'),
- renameFromRoot('timelion.enabled', 'vis_type_timelion.enabled'),
- renameFromRoot('timelion.graphiteUrls', 'vis_type_timelion.graphiteUrls'),
+ renameFromRoot('timelion_vis.enabled', 'vis_type_timelion.enabled', { level: 'critical' }),
+ renameFromRoot('timelion.enabled', 'vis_type_timelion.enabled', { level: 'critical' }),
+ renameFromRoot('timelion.graphiteUrls', 'vis_type_timelion.graphiteUrls', {
+ level: 'critical',
+ }),
// Unused properties which should be removed after releasing Kibana v8.0:
- renameFromRoot('timelion.ui.enabled', 'vis_type_timelion.ui.enabled', { silent: true }),
- unused('ui.enabled'),
+ renameFromRoot('timelion.ui.enabled', 'vis_type_timelion.ui.enabled', {
+ silent: true,
+ level: 'critical',
+ }),
+ unused('ui.enabled', { level: 'critical' }),
],
};
diff --git a/src/plugins/vis_types/timeseries/server/index.ts b/src/plugins/vis_types/timeseries/server/index.ts
index 0890b37e77926..ff2e54b31084b 100644
--- a/src/plugins/vis_types/timeseries/server/index.ts
+++ b/src/plugins/vis_types/timeseries/server/index.ts
@@ -15,17 +15,19 @@ export { VisTypeTimeseriesSetup } from './plugin';
export const config: PluginConfigDescriptor = {
deprecations: ({ unused, renameFromRoot }) => [
// In Kibana v7.8 plugin id was renamed from 'metrics' to 'vis_type_timeseries':
- renameFromRoot('metrics.enabled', 'vis_type_timeseries.enabled'),
+ renameFromRoot('metrics.enabled', 'vis_type_timeseries.enabled', { level: 'critical' }),
renameFromRoot('metrics.chartResolution', 'vis_type_timeseries.chartResolution', {
silent: true,
+ level: 'critical',
}),
renameFromRoot('metrics.minimumBucketSize', 'vis_type_timeseries.minimumBucketSize', {
silent: true,
+ level: 'critical',
}),
// Unused properties which should be removed after releasing Kibana v8.0:
- unused('chartResolution'),
- unused('minimumBucketSize'),
+ unused('chartResolution', { level: 'critical' }),
+ unused('minimumBucketSize', { level: 'critical' }),
],
schema: configSchema,
};
diff --git a/src/plugins/vis_types/vega/server/index.ts b/src/plugins/vis_types/vega/server/index.ts
index 156dec027372a..220d049c739ea 100644
--- a/src/plugins/vis_types/vega/server/index.ts
+++ b/src/plugins/vis_types/vega/server/index.ts
@@ -17,8 +17,10 @@ export const config: PluginConfigDescriptor = {
},
schema: configSchema,
deprecations: ({ renameFromRoot }) => [
- renameFromRoot('vega.enableExternalUrls', 'vis_type_vega.enableExternalUrls'),
- renameFromRoot('vega.enabled', 'vis_type_vega.enabled'),
+ renameFromRoot('vega.enableExternalUrls', 'vis_type_vega.enableExternalUrls', {
+ level: 'critical',
+ }),
+ renameFromRoot('vega.enabled', 'vis_type_vega.enabled', { level: 'critical' }),
],
};
diff --git a/x-pack/plugins/apm/common/fleet.ts b/x-pack/plugins/apm/common/fleet.ts
index 618cd20d66159..97551cc16b4be 100644
--- a/x-pack/plugins/apm/common/fleet.ts
+++ b/x-pack/plugins/apm/common/fleet.ts
@@ -6,3 +6,5 @@
*/
export const POLICY_ELASTIC_AGENT_ON_CLOUD = 'policy-elastic-agent-on-cloud';
+
+export const SUPPORTED_APM_PACKAGE_VERSION = '7.16.0';
diff --git a/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts
index 266d7246c35d4..28ce2ff24b961 100644
--- a/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts
+++ b/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts
@@ -5,12 +5,7 @@
* 2.0.
*/
-import {
- FieldValuePair,
- HistogramItem,
- RawResponseBase,
- SearchStrategyClientParams,
-} from '../types';
+import { FieldValuePair, HistogramItem } from '../types';
import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from './constants';
import { FieldStats } from '../field_stats_types';
@@ -33,11 +28,7 @@ export interface FailedTransactionsCorrelationsParams {
percentileThreshold: number;
}
-export type FailedTransactionsCorrelationsRequestParams =
- FailedTransactionsCorrelationsParams & SearchStrategyClientParams;
-
-export interface FailedTransactionsCorrelationsRawResponse
- extends RawResponseBase {
+export interface FailedTransactionsCorrelationsRawResponse {
log: string[];
failedTransactionsCorrelations?: FailedTransactionsCorrelation[];
percentileThresholdValue?: number;
diff --git a/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts
index 2eb2b37159459..ea74175a3dacb 100644
--- a/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts
+++ b/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts
@@ -5,12 +5,7 @@
* 2.0.
*/
-import {
- FieldValuePair,
- HistogramItem,
- RawResponseBase,
- SearchStrategyClientParams,
-} from '../types';
+import { FieldValuePair, HistogramItem } from '../types';
import { FieldStats } from '../field_stats_types';
export interface LatencyCorrelation extends FieldValuePair {
@@ -33,10 +28,7 @@ export interface LatencyCorrelationsParams {
analyzeCorrelations: boolean;
}
-export type LatencyCorrelationsRequestParams = LatencyCorrelationsParams &
- SearchStrategyClientParams;
-
-export interface LatencyCorrelationsRawResponse extends RawResponseBase {
+export interface LatencyCorrelationsRawResponse {
log: string[];
overallHistogram?: HistogramItem[];
percentileThresholdValue?: number;
diff --git a/x-pack/plugins/apm/common/search_strategies/types.ts b/x-pack/plugins/apm/common/search_strategies/types.ts
index d7c6eab1f07c1..ff925f70fc9b0 100644
--- a/x-pack/plugins/apm/common/search_strategies/types.ts
+++ b/x-pack/plugins/apm/common/search_strategies/types.ts
@@ -31,16 +31,26 @@ export interface RawResponseBase {
took: number;
}
-export interface SearchStrategyClientParams {
+export interface SearchStrategyClientParamsBase {
environment: string;
kuery: string;
serviceName?: string;
transactionName?: string;
transactionType?: string;
+}
+
+export interface RawSearchStrategyClientParams
+ extends SearchStrategyClientParamsBase {
start?: string;
end?: string;
}
+export interface SearchStrategyClientParams
+ extends SearchStrategyClientParamsBase {
+ start: number;
+ end: number;
+}
+
export interface SearchStrategyServerParams {
index: string;
includeFrozen?: boolean;
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx
index 2de6f1d063522..cc4bd0d14e290 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx
@@ -37,18 +37,18 @@ export function RumHome() {
defaultMessage: 'Observability',
}),
actions: {
- beats: {
+ elasticAgent: {
title: i18n.translate('xpack.apm.ux.overview.beatsCard.title', {
- defaultMessage: 'Add RUM data',
+ defaultMessage: 'Add the APM integration',
}),
description: i18n.translate(
'xpack.apm.ux.overview.beatsCard.description',
{
defaultMessage:
- 'Use the RUM (JS) agent to collect user experience data.',
+ 'Enable RUM with the APM agent to collect user experience data.',
}
),
- href: core.http.basePath.prepend(`/app/home#/tutorial/apm`),
+ href: core.http.basePath.prepend(`integrations/detail/apm`),
},
},
docsLink: core.docLinks.links.observability.guide,
diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/index.tsx
index ac32e22fa3ded..b13046d34be94 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/schema/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/schema/index.tsx
@@ -54,7 +54,8 @@ export function Schema() {
const isLoading = status !== FETCH_STATUS.SUCCESS;
const cloudApmMigrationEnabled = !!data.cloud_apm_migration_enabled;
const hasCloudAgentPolicy = !!data.has_cloud_agent_policy;
- const hasCloudApmPackagePolicy = !!data.has_cloud_apm_package_policy;
+ const cloudApmPackagePolicy = data.cloud_apm_package_policy;
+ const hasCloudApmPackagePolicy = !!cloudApmPackagePolicy;
const hasRequiredRole = !!data.has_required_role;
function updateLocalStorage(newStatus: FETCH_STATUS) {
@@ -90,6 +91,7 @@ export function Schema() {
cloudApmMigrationEnabled={cloudApmMigrationEnabled}
hasCloudAgentPolicy={hasCloudAgentPolicy}
hasRequiredRole={hasRequiredRole}
+ cloudApmPackagePolicy={cloudApmPackagePolicy}
/>
{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/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/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: (
-
+
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 {
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/infra/server/lib/alerting/common/messages.ts b/x-pack/plugins/infra/server/lib/alerting/common/messages.ts
index 084043f357bb1..23c89abf4a7aa 100644
--- a/x-pack/plugins/infra/server/lib/alerting/common/messages.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/common/messages.ts
@@ -109,15 +109,17 @@ const thresholdToI18n = ([a, b]: Array) => {
};
export const buildFiredAlertReason: (alertResult: {
+ group: string;
metric: string;
comparator: Comparator;
threshold: Array;
currentValue: number | string;
-}) => string = ({ metric, comparator, threshold, currentValue }) =>
+}) => string = ({ group, metric, comparator, threshold, currentValue }) =>
i18n.translate('xpack.infra.metrics.alerting.threshold.firedAlertReason', {
defaultMessage:
- '{metric} is {comparator} a threshold of {threshold} (current value is {currentValue})',
+ '{metric} is {comparator} a threshold of {threshold} (current value is {currentValue}) for {group}',
values: {
+ group,
metric,
comparator: comparatorToI18n(comparator, threshold.map(toNumber), toNumber(currentValue)),
threshold: thresholdToI18n(threshold),
@@ -126,14 +128,15 @@ export const buildFiredAlertReason: (alertResult: {
});
export const buildRecoveredAlertReason: (alertResult: {
+ group: string;
metric: string;
comparator: Comparator;
threshold: Array;
currentValue: number | string;
-}) => string = ({ metric, comparator, threshold, currentValue }) =>
+}) => string = ({ group, metric, comparator, threshold, currentValue }) =>
i18n.translate('xpack.infra.metrics.alerting.threshold.recoveredAlertReason', {
defaultMessage:
- '{metric} is now {comparator} a threshold of {threshold} (current value is {currentValue})',
+ '{metric} is now {comparator} a threshold of {threshold} (current value is {currentValue}) for {group}',
values: {
metric,
comparator: recoveredComparatorToI18n(
@@ -143,19 +146,22 @@ export const buildRecoveredAlertReason: (alertResult: {
),
threshold: thresholdToI18n(threshold),
currentValue,
+ group,
},
});
export const buildNoDataAlertReason: (alertResult: {
+ group: string;
metric: string;
timeSize: number;
timeUnit: string;
-}) => string = ({ metric, timeSize, timeUnit }) =>
+}) => string = ({ group, metric, timeSize, timeUnit }) =>
i18n.translate('xpack.infra.metrics.alerting.threshold.noDataAlertReason', {
- defaultMessage: '{metric} has reported no data over the past {interval}',
+ defaultMessage: '{metric} has reported no data over the past {interval} for {group}',
values: {
metric,
interval: `${timeSize}${timeUnit}`,
+ group,
},
});
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
index 72d9ea9e39def..d8b66b35c703b 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
@@ -111,18 +111,18 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
)
);
const inventoryItems = Object.keys(first(results)!);
- for (const item of inventoryItems) {
+ for (const group of inventoryItems) {
// AND logic; all criteria must be across the threshold
const shouldAlertFire = results.every((result) => {
// Grab the result of the most recent bucket
- return last(result[item].shouldFire);
+ return last(result[group].shouldFire);
});
- const shouldAlertWarn = results.every((result) => last(result[item].shouldWarn));
+ const shouldAlertWarn = results.every((result) => last(result[group].shouldWarn));
// AND logic; because we need to evaluate all criteria, if one of them reports no data then the
// whole alert is in a No Data/Error state
- const isNoData = results.some((result) => last(result[item].isNoData));
- const isError = results.some((result) => result[item].isError);
+ const isNoData = results.some((result) => last(result[group].isNoData));
+ const isError = results.some((result) => result[group].isError);
const nextState = isError
? AlertStates.ERROR
@@ -138,7 +138,8 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
reason = results
.map((result) =>
buildReasonWithVerboseMetricName(
- result[item],
+ group,
+ result[group],
buildFiredAlertReason,
nextState === AlertStates.WARNING
)
@@ -151,19 +152,23 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
*/
// } else if (nextState === AlertStates.OK && prevState?.alertState === AlertStates.ALERT) {
// reason = results
- // .map((result) => buildReasonWithVerboseMetricName(result[item], buildRecoveredAlertReason))
+ // .map((result) => buildReasonWithVerboseMetricName(group, result[group], buildRecoveredAlertReason))
// .join('\n');
}
if (alertOnNoData) {
if (nextState === AlertStates.NO_DATA) {
reason = results
- .filter((result) => result[item].isNoData)
- .map((result) => buildReasonWithVerboseMetricName(result[item], buildNoDataAlertReason))
+ .filter((result) => result[group].isNoData)
+ .map((result) =>
+ buildReasonWithVerboseMetricName(group, result[group], buildNoDataAlertReason)
+ )
.join('\n');
} else if (nextState === AlertStates.ERROR) {
reason = results
- .filter((result) => result[item].isError)
- .map((result) => buildReasonWithVerboseMetricName(result[item], buildErrorAlertReason))
+ .filter((result) => result[group].isError)
+ .map((result) =>
+ buildReasonWithVerboseMetricName(group, result[group], buildErrorAlertReason)
+ )
.join('\n');
}
}
@@ -175,7 +180,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
? WARNING_ACTIONS.id
: FIRED_ACTIONS.id;
- const alertInstance = alertInstanceFactory(`${item}`, reason);
+ const alertInstance = alertInstanceFactory(`${group}`, reason);
alertInstance.scheduleActions(
/**
* TODO: We're lying to the compiler here as explicitly calling `scheduleActions` on
@@ -183,12 +188,12 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
*/
actionGroupId as unknown as InventoryMetricThresholdAllowedActionGroups,
{
- group: item,
+ group,
alertState: stateToAlertMessage[nextState],
reason,
timestamp: moment().toISOString(),
value: mapToConditionsLookup(results, (result) =>
- formatMetric(result[item].metric, result[item].currentValue)
+ formatMetric(result[group].metric, result[group].currentValue)
),
threshold: mapToConditionsLookup(criteria, (c) => c.threshold),
metric: mapToConditionsLookup(criteria, (c) => c.metric),
@@ -199,6 +204,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
});
const buildReasonWithVerboseMetricName = (
+ group: string,
resultItem: any,
buildReason: (r: any) => string,
useWarningThreshold?: boolean
@@ -206,6 +212,7 @@ const buildReasonWithVerboseMetricName = (
if (!resultItem) return '';
const resultWithVerboseMetricName = {
...resultItem,
+ group,
metric:
toMetricOpt(resultItem.metric)?.text ||
(resultItem.metric === 'custom'
diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/reason_formatters.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/reason_formatters.ts
index cd579b9965b66..f70e0a0140ce8 100644
--- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/reason_formatters.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/reason_formatters.ts
@@ -34,7 +34,7 @@ export const getReasonMessageForGroupedCountAlert = (
) =>
i18n.translate('xpack.infra.logs.alerting.threshold.groupedCountAlertReasonDescription', {
defaultMessage:
- '{groupName}: {actualCount, plural, one {{actualCount} log entry} other {{actualCount} log entries} } ({translatedComparator} {expectedCount}) match the conditions.',
+ '{actualCount, plural, one {{actualCount} log entry} other {{actualCount} log entries} } ({translatedComparator} {expectedCount}) match the conditions for {groupName}.',
values: {
actualCount,
expectedCount,
@@ -66,7 +66,7 @@ export const getReasonMessageForGroupedRatioAlert = (
) =>
i18n.translate('xpack.infra.logs.alerting.threshold.groupedRatioAlertReasonDescription', {
defaultMessage:
- '{groupName}: The log entries ratio is {actualRatio} ({translatedComparator} {expectedRatio}).',
+ 'The log entries ratio is {actualRatio} ({translatedComparator} {expectedRatio}) for {groupName}.',
values: {
actualRatio,
expectedRatio,
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
index af5f945eeb4bb..e4887e922bb66 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
@@ -143,9 +143,10 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) =>
if (nextState === AlertStates.ALERT || nextState === AlertStates.WARNING) {
reason = alertResults
.map((result) =>
- buildFiredAlertReason(
- formatAlertResult(result[group], nextState === AlertStates.WARNING)
- )
+ buildFiredAlertReason({
+ ...formatAlertResult(result[group], nextState === AlertStates.WARNING),
+ group,
+ })
)
.join('\n');
/*
@@ -181,7 +182,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) =>
if (nextState === AlertStates.NO_DATA) {
reason = alertResults
.filter((result) => result[group].isNoData)
- .map((result) => buildNoDataAlertReason(result[group]))
+ .map((result) => buildNoDataAlertReason({ ...result[group], group }))
.join('\n');
} else if (nextState === AlertStates.ERROR) {
reason = alertResults
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/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/cypress/integration/detection_alerts/building_block_alerts.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/building_block_alerts.spec.ts
new file mode 100644
index 0000000000000..262ffe8163e57
--- /dev/null
+++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/building_block_alerts.spec.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 { getBuildingBlockRule } from '../../objects/rule';
+import { OVERVIEW_ALERTS_HISTOGRAM } from '../../screens/overview';
+import { OVERVIEW } from '../../screens/security_header';
+import { goToRuleDetails } from '../../tasks/alerts_detection_rules';
+import { createCustomRuleActivated } from '../../tasks/api_calls/rules';
+import { cleanKibana } from '../../tasks/common';
+import { waitForAlertsToPopulate, waitForTheRuleToBeExecuted } from '../../tasks/create_new_rule';
+import { loginAndWaitForPage } from '../../tasks/login';
+import { navigateFromHeaderTo } from '../../tasks/security_header';
+import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation';
+
+const EXPECTED_NUMBER_OF_ALERTS = 16;
+
+describe('Alerts generated by building block rules', () => {
+ beforeEach(() => {
+ cleanKibana();
+ });
+
+ it('Alerts should be visible on the Rule Detail page and not visible on the Overview page', () => {
+ createCustomRuleActivated(getBuildingBlockRule());
+ loginAndWaitForPage(DETECTIONS_RULE_MANAGEMENT_URL);
+ goToRuleDetails();
+ waitForTheRuleToBeExecuted();
+
+ // Check that generated events are visible on the Details page
+ waitForAlertsToPopulate(EXPECTED_NUMBER_OF_ALERTS);
+
+ navigateFromHeaderTo(OVERVIEW);
+
+ // Check that generated events are hidden on the Overview page
+ cy.get(OVERVIEW_ALERTS_HISTOGRAM).should('contain.text', 'No data to display');
+ });
+});
diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts
index 4b061865d632b..27973854097db 100644
--- a/x-pack/plugins/security_solution/cypress/objects/rule.ts
+++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts
@@ -58,6 +58,7 @@ export interface CustomRule {
lookBack: Interval;
timeline: CompleteTimeline;
maxSignals: number;
+ buildingBlockType?: string;
}
export interface ThresholdRule extends CustomRule {
@@ -188,6 +189,25 @@ export const getNewRule = (): CustomRule => ({
maxSignals: 100,
});
+export const getBuildingBlockRule = (): CustomRule => ({
+ customQuery: 'host.name: *',
+ index: getIndexPatterns(),
+ name: 'Building Block Rule Test',
+ description: 'The new rule description.',
+ severity: 'High',
+ riskScore: '17',
+ tags: ['test', 'newRule'],
+ referenceUrls: ['http://example.com/', 'https://example.com/'],
+ falsePositivesExamples: ['False1', 'False2'],
+ mitre: [getMitre1(), getMitre2()],
+ note: '# test markdown',
+ runsEvery: getRunsEvery(),
+ lookBack: getLookBack(),
+ timeline: getTimeline(),
+ maxSignals: 100,
+ buildingBlockType: 'default',
+});
+
export const getUnmappedRule = (): CustomRule => ({
customQuery: '*:*',
index: ['unmapped*'],
diff --git a/x-pack/plugins/security_solution/cypress/screens/overview.ts b/x-pack/plugins/security_solution/cypress/screens/overview.ts
index 1376a39e5ee79..1945b7e3ce3e7 100644
--- a/x-pack/plugins/security_solution/cypress/screens/overview.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/overview.ts
@@ -166,3 +166,5 @@ export const OVERVIEW_RISKY_HOSTS_VIEW_DASHBOARD_BUTTON =
export const OVERVIEW_RISKY_HOSTS_TOTAL_EVENT_COUNT = `${OVERVIEW_RISKY_HOSTS_LINKS} [data-test-subj="header-panel-subtitle"]`;
export const OVERVIEW_RISKY_HOSTS_ENABLE_MODULE_BUTTON =
'[data-test-subj="risky-hosts-enable-module-button"]';
+
+export const OVERVIEW_ALERTS_HISTOGRAM = '[data-test-subj="alerts-histogram-panel"]';
diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts
index 04ff0fcabc081..fd2838e5b3caa 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts
@@ -114,6 +114,7 @@ export const createCustomRuleActivated = (
enabled: true,
tags: ['rule1'],
max_signals: maxSignals,
+ building_block_type: rule.buildingBlockType,
},
headers: { 'kbn-xsrf': 'cypress-creds' },
failOnStatusCode: false,
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx
index 29324d186784e..c8d45ca67068a 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx
@@ -94,7 +94,7 @@ export const AlertsCountPanel = memo(
{i18n.COUNT_TABLE_TITLE}}
titleSize="s"
hideSubtitle
>
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx
index 0613c619d89b9..07fa81f27684c 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx
@@ -257,7 +257,11 @@ export const AlertsHistogramPanel = memo(
}, [showLinkToAlerts, goToDetectionEngine, formatUrl]);
const titleText = useMemo(
- () => (onlyField == null ? title : i18n.TOP(onlyField)),
+ () => (
+
+ {onlyField == null ? title : i18n.TOP(onlyField)}
+
+ ),
[onlyField, title]
);
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 d7db249475df7..084978d35d03a 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
@@ -20,7 +20,6 @@ jest.mock('../../../common/components/user_privileges/use_endpoint_privileges');
let onSearchMock: jest.Mock;
const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock;
-// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699
describe('Search exceptions', () => {
let appTestContext: AppContextTestRender;
let renderResult: ReturnType;
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts
index e0b5837c2f78a..c724773593f53 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts
@@ -122,30 +122,27 @@ export const endpointActivityLogHttpMock =
const responseData = fleetActionGenerator.generateResponse({
agent_id: endpointMetadata.agent.id,
});
-
return {
- body: {
- page: 1,
- pageSize: 50,
- startDate: 'now-1d',
- endDate: 'now',
- data: [
- {
- type: 'response',
- item: {
- id: '',
- data: responseData,
- },
+ page: 1,
+ pageSize: 50,
+ startDate: 'now-1d',
+ endDate: 'now',
+ data: [
+ {
+ type: 'response',
+ item: {
+ id: '',
+ data: responseData,
},
- {
- type: 'action',
- item: {
- id: '',
- data: actionData,
- },
+ },
+ {
+ type: 'action',
+ item: {
+ id: '',
+ data: actionData,
},
- ],
- },
+ },
+ ],
};
},
},
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts
index 43fa4e104067f..81c4dc6f2f7de 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts
@@ -61,8 +61,7 @@ jest.mock('../../../../common/lib/kibana');
type EndpointListStore = Store, Immutable>;
-// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699
-describe.skip('endpoint list middleware', () => {
+describe('endpoint list middleware', () => {
const getKibanaServicesMock = KibanaServices.get as jest.Mock;
let fakeCoreStart: jest.Mocked;
let depsStart: DepsStartMock;
@@ -390,7 +389,6 @@ describe.skip('endpoint list middleware', () => {
it('should call get Activity Log API with correct paging options', async () => {
dispatchUserChangedUrl();
-
const updatePagingDispatched = waitForAction('endpointDetailsActivityLogUpdatePaging');
dispatchGetActivityLogPaging({ page: 3 });
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/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 e519d19d60fdc..43e19c00bcc8e 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,20 +19,14 @@ import { createLoadedResourceState, isLoadedResourceState } from '../../../../..
import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing';
import { EndpointDocGenerator } from '../../../../../../../common/endpoint/generate_data';
import { policyListApiPathHandlers } from '../../../store/test_mock_utils';
-import { licenseService } from '../../../../../../common/hooks/use_license';
+import {
+ EndpointPrivileges,
+ useEndpointPrivileges,
+} from '../../../../../../common/components/user_privileges/use_endpoint_privileges';
jest.mock('../../../../trusted_apps/service');
-jest.mock('../../../../../../common/hooks/use_license', () => {
- const licenseServiceInstance = {
- isPlatinumPlus: jest.fn(),
- };
- return {
- licenseService: licenseServiceInstance,
- useLicense: () => {
- return licenseServiceInstance;
- },
- };
-});
+jest.mock('../../../../../../common/components/user_privileges/use_endpoint_privileges');
+const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock;
let mockedContext: AppContextTestRender;
let waitForAction: MiddlewareActionSpyHelper['waitForAction'];
@@ -42,8 +36,17 @@ let coreStart: AppContextTestRender['coreStart'];
let http: typeof coreStart.http;
const generator = new EndpointDocGenerator();
-// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699
-describe.skip('Policy trusted apps layout', () => {
+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;
@@ -59,6 +62,14 @@ describe.skip('Policy trusted apps layout', () => {
});
}
+ // GET Agent status for agent policy
+ if (path === '/api/fleet/agent-status') {
+ return Promise.resolve({
+ results: { events: 0, total: 5, online: 3, error: 1, offline: 1 },
+ success: true,
+ });
+ }
+
// Get package data
// Used in tests that route back to the list
if (policyListApiHandlers[path]) {
@@ -78,6 +89,10 @@ describe.skip('Policy trusted apps layout', () => {
render = () => mockedContext.render();
});
+ afterAll(() => {
+ mockUseEndpointPrivileges.mockReset();
+ });
+
afterEach(() => reactTestingLibrary.cleanup());
it('should renders layout with no existing TA data', async () => {
@@ -121,7 +136,11 @@ describe.skip('Policy trusted apps layout', () => {
});
it('should hide assign button on empty state with unassigned policies when downgraded to a gold or below license', async () => {
- (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false);
+ mockUseEndpointPrivileges.mockReturnValue(
+ loadedUserEndpointPrivilegesState({
+ isPlatinumPlus: false,
+ })
+ );
const component = render();
mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234'));
@@ -133,8 +152,13 @@ describe.skip('Policy trusted apps layout', () => {
});
expect(component.queryByTestId('assign-ta-button')).toBeNull();
});
+
it('should hide the `Assign trusted applications` button when there is data and the license is downgraded to gold or below', async () => {
- (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false);
+ mockUseEndpointPrivileges.mockReturnValue(
+ loadedUserEndpointPrivilegesState({
+ isPlatinumPlus: false,
+ })
+ );
TrustedAppsHttpServiceMock.mockImplementation(() => {
return {
getTrustedAppsList: () => getMockListResponse(),
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..b5bfc16db2899 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
@@ -207,7 +207,7 @@ describe('when rendering the PolicyTrustedAppsList', () => {
expect(appTestContext.coreStart.application.navigateToApp).toHaveBeenCalledWith(
APP_ID,
expect.objectContaining({
- path: '/administration/trusted_apps?show=edit&id=89f72d8a-05b5-4350-8cad-0dc3661d6e67',
+ path: '/administration/trusted_apps?filter=89f72d8a-05b5-4350-8cad-0dc3661d6e67',
})
);
});
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..7b1f8753831c8 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
@@ -113,7 +113,7 @@ export const PolicyTrustedAppsList = memo(
for (const trustedApp of trustedAppItems) {
const isGlobal = trustedApp.effectScope.type === 'global';
- const viewUrlPath = getTrustedAppsListPath({ id: trustedApp.id, show: 'edit' });
+ const viewUrlPath = getTrustedAppsListPath({ filter: trustedApp.id });
const assignedPoliciesMenuItems: ArtifactEntryCollapsibleCardProps['policies'] =
trustedApp.effectScope.type === 'global'
? undefined
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts
index 0602ae18c1408..beefb8587d787 100644
--- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts
@@ -8,6 +8,7 @@
export const SEARCHABLE_FIELDS: Readonly = [
`name`,
`description`,
+ 'item_id',
`entries.value`,
`entries.entries.value`,
];
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/components/signals_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx
index 321e6d00b5301..cbeb1464e1b41 100644
--- a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx
@@ -7,19 +7,24 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
+import { Filter, Query } from '@kbn/es-query';
import { AlertsHistogramPanel } from '../../../detections/components/alerts_kpis/alerts_histogram_panel';
import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index';
import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions';
-import { Filter, Query } from '../../../../../../../src/plugins/data/public';
+
import { InputsModelId } from '../../../common/store/inputs/constants';
-import * as i18n from '../../pages/translations';
import { UpdateDateRange } from '../../../common/components/charts/common';
+
import { AlertsStackByField } from '../../../detections/components/alerts_kpis/common/types';
+import * as i18n from '../../pages/translations';
+
+import { useFiltersForSignalsByCategory } from './use_filters_for_signals_by_category';
+
interface Props {
combinedQueries?: string;
- filters?: Filter[];
+ filters: Filter[];
headerChildren?: React.ReactNode;
/** Override all defaults, and only display this field */
onlyField?: AlertsStackByField;
@@ -43,6 +48,8 @@ const SignalsByCategoryComponent: React.FC = ({
}) => {
const dispatch = useDispatch();
const { signalIndexName } = useSignalIndex();
+ const filtersForSignalsByCategory = useFiltersForSignalsByCategory(filters);
+
const updateDateRangeCallback = useCallback(
({ x }) => {
if (!x) {
@@ -63,7 +70,7 @@ const SignalsByCategoryComponent: React.FC = ({
return (
{
+ // TODO: Once we are past experimental phase this code should be removed
+ const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled');
+
+ const resultingFilters = useMemo(
+ () => [
+ ...baseFilters,
+ ...(ruleRegistryEnabled
+ ? buildShowBuildingBlockFilterRuleRegistry(SHOW_BUILDING_BLOCK_ALERTS) // TODO: Once we are past experimental phase this code should be removed
+ : buildShowBuildingBlockFilter(SHOW_BUILDING_BLOCK_ALERTS)),
+ ],
+ [baseFilters, ruleRegistryEnabled]
+ );
+
+ return resultingFilters;
+};
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/routes/trusted_apps/handlers.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts
index 547c1f6a2e5ff..614ad4fb548ea 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts
@@ -110,7 +110,7 @@ const Gold = licenseMock.createLicense({ license: { type: 'gold', mode: 'gold' }
const packagePolicyClient =
createPackagePolicyServiceMock() as jest.Mocked;
-describe('handlers', () => {
+describe('TrustedApps API Handlers', () => {
beforeEach(() => {
packagePolicyClient.getByIDs.mockReset();
});
@@ -195,6 +195,7 @@ describe('handlers', () => {
const mockResponse = httpServerMock.createResponseFactory();
exceptionsListClient.deleteExceptionListItem.mockResolvedValue(null);
+ exceptionsListClient.getExceptionListItem.mockResolvedValue(null);
await deleteTrustedAppHandler(
createHandlerContextMock(),
@@ -582,7 +583,7 @@ describe('handlers', () => {
});
it('should return 404 if trusted app does not exist', async () => {
- exceptionsListClient.getExceptionListItem.mockResolvedValueOnce(null);
+ exceptionsListClient.getExceptionListItem.mockResolvedValue(null);
await updateHandler(
createHandlerContextMock(),
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts
index 2c085c14db009..08c1a3a809d4a 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts
@@ -122,7 +122,7 @@ export const exceptionListItemToTrustedApp = (
const grouped = entriesToConditionEntriesMap(exceptionListItem.entries);
return {
- id: exceptionListItem.id,
+ id: exceptionListItem.item_id,
version: exceptionListItem._version || '',
name: exceptionListItem.name,
description: exceptionListItem.description,
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts
index dce84df735929..c57416ff1c974 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts
@@ -85,9 +85,10 @@ const TRUSTED_APP: TrustedApp = {
],
};
-describe('service', () => {
+describe('TrustedApps service', () => {
beforeEach(() => {
exceptionsListClient.deleteExceptionListItem.mockReset();
+ exceptionsListClient.getExceptionListItem.mockReset();
exceptionsListClient.createExceptionListItem.mockReset();
exceptionsListClient.findExceptionListItem.mockReset();
exceptionsListClient.createTrustedAppsList.mockReset();
@@ -96,6 +97,7 @@ describe('service', () => {
describe('deleteTrustedApp', () => {
it('should delete existing trusted app', async () => {
+ exceptionsListClient.getExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM);
exceptionsListClient.deleteExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM);
expect(await deleteTrustedApp(exceptionsListClient, { id: '123' })).toBeUndefined();
@@ -107,6 +109,7 @@ describe('service', () => {
});
it('should throw for non existing trusted app', async () => {
+ exceptionsListClient.getExceptionListItem.mockResolvedValue(null);
exceptionsListClient.deleteExceptionListItem.mockResolvedValue(null);
await expect(deleteTrustedApp(exceptionsListClient, { id: '123' })).rejects.toBeInstanceOf(
@@ -393,7 +396,7 @@ describe('service', () => {
});
it('should throw a Not Found error if trusted app is not found prior to making update', async () => {
- exceptionsListClient.getExceptionListItem.mockResolvedValueOnce(null);
+ exceptionsListClient.getExceptionListItem.mockResolvedValue(null);
await expect(
updateTrustedApp(
exceptionsListClient,
@@ -489,5 +492,22 @@ describe('service', () => {
TrustedAppNotFoundError
);
});
+
+ it('should try to find trusted app by `itemId` and then by `id`', async () => {
+ exceptionsListClient.getExceptionListItem.mockResolvedValue(null);
+ await getTrustedApp(exceptionsListClient, '123').catch(() => Promise.resolve());
+
+ expect(exceptionsListClient.getExceptionListItem).toHaveBeenCalledTimes(2);
+ expect(exceptionsListClient.getExceptionListItem).toHaveBeenNthCalledWith(1, {
+ itemId: '123',
+ id: undefined,
+ namespaceType: 'agnostic',
+ });
+ expect(exceptionsListClient.getExceptionListItem).toHaveBeenNthCalledWith(2, {
+ itemId: undefined,
+ id: '123',
+ namespaceType: 'agnostic',
+ });
+ });
});
});
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts
index 856a615c1ffa2..7a4b2372ece8f 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts
@@ -15,13 +15,13 @@ import {
DeleteTrustedAppsRequestParams,
GetOneTrustedAppResponse,
GetTrustedAppsListRequest,
- GetTrustedAppsSummaryResponse,
GetTrustedAppsListResponse,
+ GetTrustedAppsSummaryRequest,
+ GetTrustedAppsSummaryResponse,
PostTrustedAppCreateRequest,
PostTrustedAppCreateResponse,
PutTrustedAppUpdateRequest,
PutTrustedAppUpdateResponse,
- GetTrustedAppsSummaryRequest,
TrustedApp,
} from '../../../../common/endpoint/types';
@@ -33,8 +33,8 @@ import {
} from './mapping';
import {
TrustedAppNotFoundError,
- TrustedAppVersionConflictError,
TrustedAppPolicyNotExistsError,
+ TrustedAppVersionConflictError,
} from './errors';
import { PackagePolicyServiceInterface } from '../../../../../fleet/server';
import { PackagePolicy } from '../../../../../fleet/common';
@@ -87,30 +87,61 @@ const isUserTryingToModifyEffectScopeWithoutPermissions = (
}
};
-export const deleteTrustedApp = async (
+/**
+ * Attempts to first fine the ExceptionItem using `item_id` and if not found, then a second attempt wil be done
+ * against the Saved Object `id`.
+ * @param exceptionsListClient
+ * @param id
+ */
+export const findTrustedAppExceptionItemByIdOrItemId = async (
exceptionsListClient: ExceptionListClient,
- { id }: DeleteTrustedAppsRequestParams
-) => {
- const exceptionListItem = await exceptionsListClient.deleteExceptionListItem({
- id,
+ id: string
+): Promise => {
+ const trustedAppExceptionItem = await exceptionsListClient.getExceptionListItem({
+ itemId: id,
+ id: undefined,
+ namespaceType: 'agnostic',
+ });
+
+ if (trustedAppExceptionItem) {
+ return trustedAppExceptionItem;
+ }
+
+ return exceptionsListClient.getExceptionListItem({
itemId: undefined,
+ id,
namespaceType: 'agnostic',
});
+};
- if (!exceptionListItem) {
+export const deleteTrustedApp = async (
+ exceptionsListClient: ExceptionListClient,
+ { id }: DeleteTrustedAppsRequestParams
+): Promise => {
+ const trustedAppExceptionItem = await findTrustedAppExceptionItemByIdOrItemId(
+ exceptionsListClient,
+ id
+ );
+
+ if (!trustedAppExceptionItem) {
throw new TrustedAppNotFoundError(id);
}
+
+ await exceptionsListClient.deleteExceptionListItem({
+ id: trustedAppExceptionItem.id,
+ itemId: undefined,
+ namespaceType: 'agnostic',
+ });
};
export const getTrustedApp = async (
exceptionsListClient: ExceptionListClient,
id: string
): Promise => {
- const trustedAppExceptionItem = await exceptionsListClient.getExceptionListItem({
- itemId: '',
- id,
- namespaceType: 'agnostic',
- });
+ const trustedAppExceptionItem = await findTrustedAppExceptionItemByIdOrItemId(
+ exceptionsListClient,
+ id
+ );
if (!trustedAppExceptionItem) {
throw new TrustedAppNotFoundError(id);
@@ -189,19 +220,18 @@ export const updateTrustedApp = async (
updatedTrustedApp: PutTrustedAppUpdateRequest,
isAtLeastPlatinum: boolean
): Promise => {
- const currentTrustedApp = await exceptionsListClient.getExceptionListItem({
- itemId: '',
- id,
- namespaceType: 'agnostic',
- });
+ const currentTrustedAppExceptionItem = await findTrustedAppExceptionItemByIdOrItemId(
+ exceptionsListClient,
+ id
+ );
- if (!currentTrustedApp) {
+ if (!currentTrustedAppExceptionItem) {
throw new TrustedAppNotFoundError(id);
}
if (
isUserTryingToModifyEffectScopeWithoutPermissions(
- exceptionListItemToTrustedApp(currentTrustedApp),
+ exceptionListItemToTrustedApp(currentTrustedAppExceptionItem),
updatedTrustedApp,
isAtLeastPlatinum
)
@@ -226,7 +256,10 @@ export const updateTrustedApp = async (
try {
updatedTrustedAppExceptionItem = await exceptionsListClient.updateExceptionListItem(
- updatedTrustedAppToUpdateExceptionListItemOptions(currentTrustedApp, updatedTrustedApp)
+ updatedTrustedAppToUpdateExceptionListItemOptions(
+ currentTrustedAppExceptionItem,
+ updatedTrustedApp
+ )
);
} catch (e) {
if (e?.output?.statusCode === 409) {
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/signals/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts
index 169a820392a6e..677a2028acdf7 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts
@@ -11,7 +11,7 @@ import { getThreatList, getThreatListCount } from './get_threat_list';
import { CreateThreatSignalsOptions } from './types';
import { createThreatSignal } from './create_threat_signal';
import { SearchAfterAndBulkCreateReturnType } from '../types';
-import { combineConcurrentResults } from './utils';
+import { buildExecutionIntervalValidator, combineConcurrentResults } from './utils';
import { buildThreatEnrichment } from './build_threat_enrichment';
export const createThreatSignals = async ({
@@ -46,6 +46,9 @@ export const createThreatSignals = async ({
const params = ruleSO.attributes.params;
logger.debug(buildRuleMessage('Indicator matching rule starting'));
const perPage = concurrentSearches * itemsPerSearch;
+ const verifyExecutionCanProceed = buildExecutionIntervalValidator(
+ ruleSO.attributes.schedule.interval
+ );
let results: SearchAfterAndBulkCreateReturnType = {
success: true,
@@ -99,6 +102,7 @@ export const createThreatSignals = async ({
});
while (threatList.hits.hits.length !== 0) {
+ verifyExecutionCanProceed();
const chunks = chunk(itemsPerSearch, threatList.hits.hits);
logger.debug(buildRuleMessage(`${chunks.length} concurrent indicator searches are starting.`));
const concurrentSearchesPerformed = chunks.map>(
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts
index ec826b44023f6..f029b02127b08 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts
@@ -10,6 +10,7 @@ import { sampleSignalHit } from '../__mocks__/es_results';
import { ThreatMatchNamedQuery } from './types';
import {
+ buildExecutionIntervalValidator,
calculateAdditiveMax,
calculateMax,
calculateMaxLookBack,
@@ -712,4 +713,26 @@ describe('utils', () => {
});
});
});
+
+ describe('buildExecutionIntervalValidator', () => {
+ it('succeeds if the validator is called within the specified interval', () => {
+ const validator = buildExecutionIntervalValidator('1m');
+ expect(() => validator()).not.toThrowError();
+ });
+
+ it('throws an error if the validator is called after the specified interval', async () => {
+ const validator = buildExecutionIntervalValidator('1s');
+
+ await new Promise((r) => setTimeout(r, 1001));
+ expect(() => validator()).toThrowError(
+ 'Current rule execution has exceeded its allotted interval (1s) and has been stopped.'
+ );
+ });
+
+ it('throws an error if the interval cannot be parsed', () => {
+ expect(() => buildExecutionIntervalValidator('badString')).toThrowError(
+ 'Unable to parse rule interval (badString); stopping rule execution since allotted duration is undefined'
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts
index 4d9fda43f032e..99f6609faec91 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts
@@ -5,7 +5,10 @@
* 2.0.
*/
+import moment from 'moment';
+
import { SearchAfterAndBulkCreateReturnType, SignalSourceHit } from '../types';
+import { parseInterval } from '../utils';
import { ThreatMatchNamedQuery } from './types';
/**
@@ -146,3 +149,21 @@ export const decodeThreatMatchNamedQuery = (encoded: string): ThreatMatchNamedQu
export const extractNamedQueries = (hit: SignalSourceHit): ThreatMatchNamedQuery[] =>
hit.matched_queries?.map((match) => decodeThreatMatchNamedQuery(match)) ?? [];
+
+export const buildExecutionIntervalValidator: (interval: string) => () => void = (interval) => {
+ const intervalDuration = parseInterval(interval);
+
+ if (intervalDuration == null) {
+ throw new Error(
+ `Unable to parse rule interval (${interval}); stopping rule execution since allotted duration is undefined.`
+ );
+ }
+
+ const executionEnd = moment().add(intervalDuration);
+ return () => {
+ if (moment().isAfter(executionEnd)) {
+ const message = `Current rule execution has exceeded its allotted interval (${interval}) and has been stopped.`;
+ throw new Error(message);
+ }
+ };
+};
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 113eae5d08e07..f86167046c4c2 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -2534,7 +2534,6 @@
"discover.localMenu.openSavedSearchDescription": "保存された検索を開きます",
"discover.localMenu.openTitle": "開く",
"discover.localMenu.optionsDescription": "オプション",
- "discover.localMenu.saveSaveSearchDescription": "ビジュアライゼーションとダッシュボードで使用できるように Discover の検索を保存します",
"discover.localMenu.saveSaveSearchObjectType": "検索",
"discover.localMenu.saveSearchDescription": "検索を保存します",
"discover.localMenu.saveTitle": "保存",
@@ -3015,11 +3014,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": "一般的なアプリやサービスからデータを取り込みます。",
@@ -4210,8 +4205,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": "推奨",
@@ -11206,12 +11199,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エージェント統合として提供",
@@ -13592,15 +13580,12 @@
"xpack.infra.metrics.alerting.threshold.errorAlertReason": "{metric}のデータのクエリを試行しているときに、Elasticsearchが失敗しました",
"xpack.infra.metrics.alerting.threshold.errorState": "エラー",
"xpack.infra.metrics.alerting.threshold.fired": "アラート",
- "xpack.infra.metrics.alerting.threshold.firedAlertReason": "{metric}は{comparator} {threshold}のしきい値です(現在の値は{currentValue})",
"xpack.infra.metrics.alerting.threshold.gtComparator": "より大きい",
"xpack.infra.metrics.alerting.threshold.ltComparator": "より小さい",
- "xpack.infra.metrics.alerting.threshold.noDataAlertReason": "{metric}は過去{interval}にデータを報告していません",
"xpack.infra.metrics.alerting.threshold.noDataFormattedValue": "[データなし]",
"xpack.infra.metrics.alerting.threshold.noDataState": "データなし",
"xpack.infra.metrics.alerting.threshold.okState": "OK [回復済み]",
"xpack.infra.metrics.alerting.threshold.outsideRangeComparator": "の間にない",
- "xpack.infra.metrics.alerting.threshold.recoveredAlertReason": "{metric}は{comparator} {threshold}のしきい値です(現在の値は{currentValue})",
"xpack.infra.metrics.alerting.threshold.thresholdRange": "{a}と{b}",
"xpack.infra.metrics.alerting.threshold.warning": "警告",
"xpack.infra.metrics.alerting.threshold.warningState": "警告",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 15933599699eb..6cfdc69c9e897 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -2560,7 +2560,6 @@
"discover.localMenu.openSavedSearchDescription": "打开已保存搜索",
"discover.localMenu.openTitle": "打开",
"discover.localMenu.optionsDescription": "选项",
- "discover.localMenu.saveSaveSearchDescription": "保存您的 Discover 搜索,以便可以在可视化和仪表板中使用该搜索",
"discover.localMenu.saveSaveSearchObjectType": "搜索",
"discover.localMenu.saveSearchDescription": "保存搜索",
"discover.localMenu.saveTitle": "保存",
@@ -3044,11 +3043,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": "从热门应用和服务中采集数据。",
@@ -4250,8 +4245,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": "推荐",
@@ -11322,12 +11315,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 代理集成来提供",
@@ -13781,15 +13769,12 @@
"xpack.infra.metrics.alerting.threshold.errorAlertReason": "Elasticsearch 尝试查询 {metric} 的数据时出现故障",
"xpack.infra.metrics.alerting.threshold.errorState": "错误",
"xpack.infra.metrics.alerting.threshold.fired": "告警",
- "xpack.infra.metrics.alerting.threshold.firedAlertReason": "{metric} {comparator}阈值 {threshold}(当前值为 {currentValue})",
"xpack.infra.metrics.alerting.threshold.gtComparator": "大于",
"xpack.infra.metrics.alerting.threshold.ltComparator": "小于",
- "xpack.infra.metrics.alerting.threshold.noDataAlertReason": "{metric} 在过去 {interval}中未报告数据",
"xpack.infra.metrics.alerting.threshold.noDataFormattedValue": "[无数据]",
"xpack.infra.metrics.alerting.threshold.noDataState": "无数据",
"xpack.infra.metrics.alerting.threshold.okState": "正常 [已恢复]",
"xpack.infra.metrics.alerting.threshold.outsideRangeComparator": "不介于",
- "xpack.infra.metrics.alerting.threshold.recoveredAlertReason": "{metric} 现在{comparator}阈值 {threshold}(当前值为 {currentValue})",
"xpack.infra.metrics.alerting.threshold.thresholdRange": "{a} 和 {b}",
"xpack.infra.metrics.alerting.threshold.warning": "警告",
"xpack.infra.metrics.alerting.threshold.warningState": "警告",
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/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/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts
index 0aad3c699805a..223529fce54f6 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts
@@ -411,6 +411,52 @@ export default ({ getService }: FtrProviderContext) => {
expect(signalsOpen.hits.hits.length).equal(0);
});
+ describe('timeout behavior', () => {
+ it('will return an error if a rule execution exceeds the rule interval', async () => {
+ const rule: CreateRulesSchema = {
+ description: 'Detecting root and admin users',
+ name: 'Query with a short interval',
+ severity: 'high',
+ index: ['auditbeat-*'],
+ type: 'threat_match',
+ risk_score: 55,
+ language: 'kuery',
+ rule_id: 'rule-1',
+ from: '1900-01-01T00:00:00.000Z',
+ query: '*:*',
+ threat_query: '*:*', // broad query to take more time
+ threat_index: ['auditbeat-*'], // We use auditbeat as both the matching index and the threat list for simplicity
+ threat_mapping: [
+ {
+ entries: [
+ {
+ field: 'host.name',
+ value: 'host.name',
+ type: 'mapping',
+ },
+ ],
+ },
+ ],
+ threat_filters: [],
+ concurrent_searches: 1,
+ interval: '1s', // short interval
+ items_per_search: 1, // iterate only 1 threat item per loop to ensure we're slow
+ };
+
+ const { id } = await createRule(supertest, rule);
+ await waitForRuleSuccessOrStatus(supertest, id, 'failed');
+
+ const { body } = await supertest
+ .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`)
+ .set('kbn-xsrf', 'true')
+ .send({ ids: [id] })
+ .expect(200);
+ expect(body[id].current_status.last_failure_message).to.contain(
+ 'execution has exceeded its allotted interval'
+ );
+ });
+ });
+
describe('indicator enrichment', () => {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/filebeat/threat_intel');
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', () => {
// });