<_EuiSplitPanelOuter
@@ -79,8 +124,8 @@ exports[`TelemetryManagementSectionComponent renders as expected 1`] = `
,
+ "endpointSecurityData":
+
+ ,
}
}
/>
@@ -232,6 +287,19 @@ exports[`TelemetryManagementSectionComponent renders null because allowChangingO
"timeZone": null,
}
}
+ isSecurityExampleEnabled={
+ [MockFunction] {
+ "calls": Array [
+ Array [],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": true,
+ },
+ ],
+ }
+ }
onQueryMatchChange={[MockFunction]}
showAppliesSettingMessage={true}
telemetryService={
diff --git a/src/plugins/expression_reveal_image/common/i18n/expression_functions/function_errors.ts b/src/plugins/telemetry_management_section/public/components/example_security_payload.test.tsx
similarity index 54%
rename from src/plugins/expression_reveal_image/common/i18n/expression_functions/function_errors.ts
rename to src/plugins/telemetry_management_section/public/components/example_security_payload.test.tsx
index 09cd26c9e620..0b22ad5b9c20 100644
--- a/src/plugins/expression_reveal_image/common/i18n/expression_functions/function_errors.ts
+++ b/src/plugins/telemetry_management_section/public/components/example_security_payload.test.tsx
@@ -6,8 +6,12 @@
* Side Public License, v 1.
*/
-import { errors as revealImage } from './dict/reveal_image';
+import React from 'react';
+import { shallowWithIntl } from '@kbn/test/jest';
+import ExampleSecurityPayload from './example_security_payload';
-export const getFunctionErrors = () => ({
- revealImage,
+describe('example security payload', () => {
+ it('renders as expected', () => {
+ expect(shallowWithIntl()).toMatchSnapshot();
+ });
});
diff --git a/src/plugins/telemetry_management_section/public/components/example_security_payload.tsx b/src/plugins/telemetry_management_section/public/components/example_security_payload.tsx
new file mode 100644
index 000000000000..6a18ccac59ee
--- /dev/null
+++ b/src/plugins/telemetry_management_section/public/components/example_security_payload.tsx
@@ -0,0 +1,124 @@
+/*
+ * 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 { EuiCodeBlock } from '@elastic/eui';
+import * as React from 'react';
+
+const exampleSecurityPayload = {
+ '@timestamp': '2020-09-22T14:34:56.82202300Z',
+ agent: {
+ build: {
+ original:
+ 'version: 7.9.1, compiled: Thu Aug 27 14:50:21 2020, branch: 7.9, commit: b594beb958817dee9b9d908191ed766d483df3ea',
+ },
+ id: '22dd8544-bcac-46cb-b970-5e681bb99e0b',
+ type: 'endpoint',
+ version: '7.9.1',
+ },
+ Endpoint: {
+ policy: {
+ applied: {
+ artifacts: {
+ global: {
+ identifiers: [
+ {
+ sha256: '6a546aade5563d3e8dffc1fe2d93d33edda8f9ca3e17ac3cc9ac707620cb9ecd',
+ name: 'endpointpe-v4-blocklist',
+ },
+ {
+ sha256: '04f9f87accc5d5aea433427bd1bd4ec6908f8528c78ceed26f70df7875a99385',
+ name: 'endpointpe-v4-exceptionlist',
+ },
+ {
+ sha256: '1471838597fcd79a54ea4a3ec9a9beee1a86feaedab6c98e61102559ced822a8',
+ name: 'endpointpe-v4-model',
+ },
+ {
+ sha256: '824859b0c6749cc31951d92a73bbdddfcfe9f38abfe432087934d4dab9766ce8',
+ name: 'global-exceptionlist-windows',
+ },
+ ],
+ version: '1.0.0',
+ },
+ user: {
+ identifiers: [
+ {
+ sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658',
+ name: 'endpoint-exceptionlist-windows-v1',
+ },
+ ],
+ version: '1.0.0',
+ },
+ },
+ },
+ },
+ },
+ ecs: {
+ version: '1.5.0',
+ },
+ elastic: {
+ agent: {
+ id: 'b2e88aea-2671-402a-828a-957526bac315',
+ },
+ },
+ file: {
+ path: 'C:\\Windows\\Temp\\mimikatz.exe',
+ size: 1263880,
+ created: '2020-05-19T07:50:06.0Z',
+ accessed: '2020-09-22T14:29:19.93531400Z',
+ mtime: '2020-09-22T14:29:03.6040000Z',
+ directory: 'C:\\Windows\\Temp',
+ hash: {
+ sha1: 'c9fb7f8a4c6b7b12b493a99a8dc6901d17867388',
+ sha256: 'cb1553a3c88817e4cc774a5a93f9158f6785bd3815447d04b6c3f4c2c4b21ed7',
+ md5: '465d5d850f54d9cde767bda90743df30',
+ },
+ Ext: {
+ code_signature: {
+ trusted: true,
+ subject_name: 'Open Source Developer, Benjamin Delpy',
+ exists: true,
+ status: 'trusted',
+ },
+ malware_classification: {
+ identifier: 'endpointpe-v4-model',
+ score: 0.99956864118576,
+ threshold: 0.71,
+ version: '0.0.0',
+ },
+ },
+ },
+ host: {
+ os: {
+ Ext: {
+ variant: 'Windows 10 Enterprise Evaluation',
+ },
+ kernel: '2004 (10.0.19041.388)',
+ name: 'Windows',
+ family: 'windows',
+ version: '2004 (10.0.19041.388)',
+ platform: 'windows',
+ full: 'Windows 10 Enterprise Evaluation 2004 (10.0.19041.388)',
+ },
+ },
+ event: {
+ kind: 'alert',
+ },
+ cluster_uuid: 'kLbKvSMcRiiFAR0t8LebDA',
+ cluster_name: 'elasticsearch',
+};
+
+const ExampleSecurityPayload: React.FC = () => {
+ return (
+ {JSON.stringify(exampleSecurityPayload, null, 2)}
+ );
+};
+
+// Used for lazy import
+// eslint-disable-next-line import/no-default-export
+export default ExampleSecurityPayload;
diff --git a/src/plugins/telemetry_management_section/public/components/opt_in_security_example_flyout.test.tsx b/src/plugins/telemetry_management_section/public/components/opt_in_security_example_flyout.test.tsx
new file mode 100644
index 000000000000..74fd7ddd56cb
--- /dev/null
+++ b/src/plugins/telemetry_management_section/public/components/opt_in_security_example_flyout.test.tsx
@@ -0,0 +1,17 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import { shallowWithIntl } from '@kbn/test/jest';
+import { OptInSecurityExampleFlyout } from './opt_in_security_example_flyout';
+
+describe('security flyout renders as expected', () => {
+ it('renders as expected', () => {
+ expect(shallowWithIntl()).toMatchSnapshot();
+ });
+});
diff --git a/src/plugins/telemetry_management_section/public/components/opt_in_security_example_flyout.tsx b/src/plugins/telemetry_management_section/public/components/opt_in_security_example_flyout.tsx
new file mode 100644
index 000000000000..58a82487c25d
--- /dev/null
+++ b/src/plugins/telemetry_management_section/public/components/opt_in_security_example_flyout.tsx
@@ -0,0 +1,58 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import * as React from 'react';
+
+import {
+ EuiFlyout,
+ EuiFlyoutHeader,
+ EuiFlyoutBody,
+ EuiPortal, // EuiPortal is a temporary requirement to use EuiFlyout with "ownFocus"
+ EuiText,
+ EuiTextColor,
+ EuiTitle,
+} from '@elastic/eui';
+import { loadingSpinner } from './loading_spinner';
+
+interface Props {
+ onClose: () => void;
+}
+
+const LazyExampleSecurityPayload = React.lazy(() => import('./example_security_payload'));
+
+/**
+ * React component for displaying the example data associated with the Telemetry opt-in banner.
+ */
+export class OptInSecurityExampleFlyout extends React.PureComponent {
+ render() {
+ return (
+
+
+
+
+ Endpoint security data
+
+
+
+ This is a representative sample of the endpoint security alert event that we
+ collect. Endpoint security data is collected only when the Elastic Endpoint is
+ enabled. It includes information about the endpoint configuration and detection
+ events.
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx
index a6ad9d4c3dc0..fe6f8e254142 100644
--- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx
+++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx
@@ -21,6 +21,7 @@ describe('TelemetryManagementSectionComponent', () => {
it('renders as expected', () => {
const onQueryMatchChange = jest.fn();
+ const isSecurityExampleEnabled = jest.fn().mockReturnValue(true);
const telemetryService = new TelemetryService({
config: {
enabled: true,
@@ -45,6 +46,7 @@ describe('TelemetryManagementSectionComponent', () => {
onQueryMatchChange={onQueryMatchChange}
showAppliesSettingMessage={true}
enableSaving={true}
+ isSecurityExampleEnabled={isSecurityExampleEnabled}
toasts={coreStart.notifications.toasts}
docLinks={docLinks}
/>
@@ -54,6 +56,7 @@ describe('TelemetryManagementSectionComponent', () => {
it('renders null because query does not match the SEARCH_TERMS', () => {
const onQueryMatchChange = jest.fn();
+ const isSecurityExampleEnabled = jest.fn().mockReturnValue(true);
const telemetryService = new TelemetryService({
config: {
enabled: true,
@@ -78,6 +81,7 @@ describe('TelemetryManagementSectionComponent', () => {
onQueryMatchChange={onQueryMatchChange}
showAppliesSettingMessage={false}
enableSaving={true}
+ isSecurityExampleEnabled={isSecurityExampleEnabled}
toasts={coreStart.notifications.toasts}
docLinks={docLinks}
/>
@@ -94,6 +98,7 @@ describe('TelemetryManagementSectionComponent', () => {
showAppliesSettingMessage={false}
enableSaving={true}
toasts={coreStart.notifications.toasts}
+ isSecurityExampleEnabled={isSecurityExampleEnabled}
docLinks={docLinks}
/>
@@ -107,6 +112,7 @@ describe('TelemetryManagementSectionComponent', () => {
it('renders because query matches the SEARCH_TERMS', () => {
const onQueryMatchChange = jest.fn();
+ const isSecurityExampleEnabled = jest.fn().mockReturnValue(true);
const telemetryService = new TelemetryService({
config: {
enabled: true,
@@ -129,6 +135,7 @@ describe('TelemetryManagementSectionComponent', () => {
telemetryService={telemetryService}
onQueryMatchChange={onQueryMatchChange}
showAppliesSettingMessage={false}
+ isSecurityExampleEnabled={isSecurityExampleEnabled}
enableSaving={true}
toasts={coreStart.notifications.toasts}
docLinks={docLinks}
@@ -154,6 +161,7 @@ describe('TelemetryManagementSectionComponent', () => {
it('renders null because allowChangingOptInStatus is false', () => {
const onQueryMatchChange = jest.fn();
+ const isSecurityExampleEnabled = jest.fn().mockReturnValue(true);
const telemetryService = new TelemetryService({
config: {
enabled: true,
@@ -177,6 +185,7 @@ describe('TelemetryManagementSectionComponent', () => {
onQueryMatchChange={onQueryMatchChange}
showAppliesSettingMessage={true}
enableSaving={true}
+ isSecurityExampleEnabled={isSecurityExampleEnabled}
toasts={coreStart.notifications.toasts}
docLinks={docLinks}
/>
@@ -192,6 +201,7 @@ describe('TelemetryManagementSectionComponent', () => {
it('shows the OptInExampleFlyout', () => {
const onQueryMatchChange = jest.fn();
+ const isSecurityExampleEnabled = jest.fn().mockReturnValue(true);
const telemetryService = new TelemetryService({
config: {
enabled: true,
@@ -215,6 +225,7 @@ describe('TelemetryManagementSectionComponent', () => {
onQueryMatchChange={onQueryMatchChange}
showAppliesSettingMessage={false}
enableSaving={true}
+ isSecurityExampleEnabled={isSecurityExampleEnabled}
toasts={coreStart.notifications.toasts}
docLinks={docLinks}
/>
@@ -229,8 +240,91 @@ describe('TelemetryManagementSectionComponent', () => {
}
});
+ it('shows the OptInSecurityExampleFlyout', () => {
+ const onQueryMatchChange = jest.fn();
+ const isSecurityExampleEnabled = jest.fn().mockReturnValue(true);
+ const telemetryService = new TelemetryService({
+ config: {
+ enabled: true,
+ url: '',
+ banner: true,
+ allowChangingOptInStatus: true,
+ optIn: false,
+ optInStatusUrl: '',
+ sendUsageFrom: 'browser',
+ },
+ isScreenshotMode: false,
+ reportOptInStatusChange: false,
+ notifications: coreStart.notifications,
+ currentKibanaVersion: 'mock_kibana_version',
+ http: coreSetup.http,
+ });
+
+ const component = mountWithIntl(
+
+ );
+ try {
+ const toggleExampleComponent = component.find('FormattedMessage > EuiLink[onClick]').at(1);
+ const updatedView = toggleExampleComponent.simulate('click');
+ updatedView.find('OptInSecurityExampleFlyout');
+ updatedView.simulate('close');
+ } finally {
+ component.unmount();
+ }
+ });
+
+ it('does not show the endpoint link when isSecurityExampleEnabled returns false', () => {
+ const onQueryMatchChange = jest.fn();
+ const isSecurityExampleEnabled = jest.fn().mockReturnValue(false);
+ const telemetryService = new TelemetryService({
+ config: {
+ enabled: true,
+ url: '',
+ banner: true,
+ allowChangingOptInStatus: true,
+ optIn: false,
+ optInStatusUrl: '',
+ sendUsageFrom: 'browser',
+ },
+ isScreenshotMode: false,
+ reportOptInStatusChange: false,
+ currentKibanaVersion: 'mock_kibana_version',
+ notifications: coreStart.notifications,
+ http: coreSetup.http,
+ });
+
+ const component = mountWithIntl(
+
+ );
+
+ try {
+ const description = (component.instance() as TelemetryManagementSection).renderDescription();
+ expect(isSecurityExampleEnabled).toBeCalled();
+ expect(description).toMatchSnapshot();
+ } finally {
+ component.unmount();
+ }
+ });
+
it('toggles the OptIn button', async () => {
const onQueryMatchChange = jest.fn();
+ const isSecurityExampleEnabled = jest.fn().mockReturnValue(true);
const telemetryService = new TelemetryService({
config: {
enabled: true,
@@ -254,6 +348,7 @@ describe('TelemetryManagementSectionComponent', () => {
onQueryMatchChange={onQueryMatchChange}
showAppliesSettingMessage={false}
enableSaving={true}
+ isSecurityExampleEnabled={isSecurityExampleEnabled}
toasts={coreStart.notifications.toasts}
docLinks={docLinks}
/>
@@ -280,6 +375,7 @@ describe('TelemetryManagementSectionComponent', () => {
it('test the wrapper (for coverage purposes)', () => {
const onQueryMatchChange = jest.fn();
+ const isSecurityExampleEnabled = jest.fn().mockReturnValue(true);
const telemetryService = new TelemetryService({
config: {
enabled: true,
@@ -305,6 +401,7 @@ describe('TelemetryManagementSectionComponent', () => {
onQueryMatchChange={onQueryMatchChange}
enableSaving={true}
toasts={coreStart.notifications.toasts}
+ isSecurityExampleEnabled={isSecurityExampleEnabled}
docLinks={docLinks}
/>
).html()
diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx
index f46632cb35c7..b0d1b42a9b89 100644
--- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx
+++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx
@@ -15,6 +15,7 @@ import type { TelemetryPluginSetup } from 'src/plugins/telemetry/public';
import type { DocLinksStart, ToastsStart } from 'src/core/public';
import { PRIVACY_STATEMENT_URL } from '../../../telemetry/common/constants';
import { OptInExampleFlyout } from './opt_in_example_flyout';
+import { OptInSecurityExampleFlyout } from './opt_in_security_example_flyout';
import { LazyField } from '../../../advanced_settings/public';
import { TrackApplicationView } from '../../../usage_collection/public';
@@ -25,6 +26,7 @@ const SEARCH_TERMS = ['telemetry', 'usage', 'data', 'usage data'];
interface Props {
telemetryService: TelemetryService;
onQueryMatchChange: (searchTermMatches: boolean) => void;
+ isSecurityExampleEnabled: () => boolean;
showAppliesSettingMessage: boolean;
enableSaving: boolean;
query?: { text: string };
@@ -35,6 +37,7 @@ interface Props {
interface State {
processing: boolean;
showExample: boolean;
+ showSecurityExample: boolean;
queryMatches: boolean | null;
enabled: boolean;
}
@@ -46,6 +49,7 @@ export class TelemetryManagementSection extends Component {
this.state = {
processing: false,
showExample: false,
+ showSecurityExample: false,
queryMatches: props.query ? this.checkQueryMatch(props.query) : null,
enabled: this.props.telemetryService.getIsOptedIn() || false,
};
@@ -76,8 +80,9 @@ export class TelemetryManagementSection extends Component {
}
render() {
- const { telemetryService } = this.props;
- const { showExample, queryMatches, enabled, processing } = this.state;
+ const { telemetryService, isSecurityExampleEnabled } = this.props;
+ const { showExample, showSecurityExample, queryMatches, enabled, processing } = this.state;
+ const securityExampleEnabled = isSecurityExampleEnabled();
if (!telemetryService.getCanChangeOptInStatus()) {
return null;
@@ -97,6 +102,11 @@ export class TelemetryManagementSection extends Component {
/>
)}
+ {showSecurityExample && securityExampleEnabled && (
+
+
+
+ )}
@@ -172,12 +182,20 @@ export class TelemetryManagementSection extends Component {
};
renderDescription = () => {
+ const { isSecurityExampleEnabled } = this.props;
+ const securityExampleEnabled = isSecurityExampleEnabled();
const clusterDataLink = (
);
+ const endpointSecurityDataLink = (
+
+
+
+ );
+
return (
@@ -198,13 +216,24 @@ export class TelemetryManagementSection extends Component {
/>
-
+ {securityExampleEnabled ? (
+
+ ) : (
+
+ )}
);
@@ -248,6 +277,15 @@ export class TelemetryManagementSection extends Component {
showExample: !this.state.showExample,
});
};
+
+ toggleSecurityExample = () => {
+ const { isSecurityExampleEnabled } = this.props;
+ const securityExampleEnabled = isSecurityExampleEnabled();
+ if (!securityExampleEnabled) return;
+ this.setState({
+ showSecurityExample: !this.state.showSecurityExample,
+ });
+ };
}
// required for lazy loading
diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx
index 30769683803f..91881dffa52d 100644
--- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx
+++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx
@@ -12,19 +12,21 @@ import type { TelemetryPluginSetup } from 'src/plugins/telemetry/public';
import type TelemetryManagementSection from './telemetry_management_section';
export type TelemetryManagementSectionWrapperProps = Omit<
TelemetryManagementSection['props'],
- 'telemetryService' | 'showAppliesSettingMessage'
+ 'telemetryService' | 'showAppliesSettingMessage' | 'isSecurityExampleEnabled'
>;
const TelemetryManagementSectionComponent = lazy(() => import('./telemetry_management_section'));
export function telemetryManagementSectionWrapper(
- telemetryService: TelemetryPluginSetup['telemetryService']
+ telemetryService: TelemetryPluginSetup['telemetryService'],
+ shouldShowSecuritySolutionUsageExample: () => boolean
) {
const TelemetryManagementSectionWrapper = (props: TelemetryManagementSectionWrapperProps) => (
}>
diff --git a/src/plugins/telemetry_management_section/public/index.ts b/src/plugins/telemetry_management_section/public/index.ts
index f39d94954019..db6ea17556ed 100644
--- a/src/plugins/telemetry_management_section/public/index.ts
+++ b/src/plugins/telemetry_management_section/public/index.ts
@@ -10,6 +10,7 @@ import { TelemetryManagementSectionPlugin } from './plugin';
export { OptInExampleFlyout } from './components';
+export type { TelemetryManagementSectionPluginSetup } from './plugin';
export function plugin() {
return new TelemetryManagementSectionPlugin();
}
diff --git a/src/plugins/telemetry_management_section/public/plugin.tsx b/src/plugins/telemetry_management_section/public/plugin.tsx
index 6db05dfe812b..24583260329a 100644
--- a/src/plugins/telemetry_management_section/public/plugin.tsx
+++ b/src/plugins/telemetry_management_section/public/plugin.tsx
@@ -10,7 +10,7 @@ import React from 'react';
import type { AdvancedSettingsSetup } from 'src/plugins/advanced_settings/public';
import type { TelemetryPluginSetup } from 'src/plugins/telemetry/public';
import type { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
-import type { CoreStart, CoreSetup } from 'src/core/public';
+import type { Plugin, CoreStart, CoreSetup } from 'src/core/public';
import {
telemetryManagementSectionWrapper,
@@ -34,7 +34,17 @@ export interface TelemetryManagementSectionPluginDepsSetup {
usageCollection?: UsageCollectionSetup;
}
-export class TelemetryManagementSectionPlugin {
+export interface TelemetryManagementSectionPluginSetup {
+ toggleSecuritySolutionExample: (enabled: boolean) => void;
+}
+
+export class TelemetryManagementSectionPlugin
+ implements Plugin {
+ private showSecuritySolutionExample = false;
+ private shouldShowSecuritySolutionExample = () => {
+ return this.showSecuritySolutionExample;
+ };
+
public setup(
core: CoreSetup,
{
@@ -50,16 +60,21 @@ export class TelemetryManagementSectionPlugin {
(props) => {
return (
- {telemetryManagementSectionWrapper(telemetryService)(
- props as TelemetryManagementSectionWrapperProps
- )}
+ {telemetryManagementSectionWrapper(
+ telemetryService,
+ this.shouldShowSecuritySolutionExample
+ )(props as TelemetryManagementSectionWrapperProps)}
);
},
true
);
- return {};
+ return {
+ toggleSecuritySolutionExample: (enabled: boolean) => {
+ this.showSecuritySolutionExample = enabled;
+ },
+ };
}
public start(core: CoreStart) {}
diff --git a/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx b/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx
index fa0e0bd5f48f..c32d15c336cf 100644
--- a/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx
+++ b/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx
@@ -33,14 +33,27 @@ export const VisualizeByValueEditor = ({ onAppLeave }: VisualizeAppProps) => {
const [valueInput, setValueInput] = useState();
useEffect(() => {
- const { originatingApp: value, embeddableId: embeddableIdValue, valueInput: valueInputValue } =
- services.stateTransferService.getIncomingEditorState(VisualizeConstants.APP_ID) || {};
+ const { stateTransferService, history, data } = services;
+ const {
+ originatingApp: value,
+ embeddableId: embeddableIdValue,
+ valueInput: valueInputValue,
+ searchSessionId,
+ } = stateTransferService.getIncomingEditorState(VisualizeConstants.APP_ID) || {};
+
setOriginatingApp(value);
setValueInput(valueInputValue);
setEmbeddableId(embeddableIdValue);
+
if (!valueInputValue) {
// if there is no value input to load, redirect to the visualize listing page.
- services.history.replace(VisualizeConstants.LANDING_PAGE_PATH);
+ history.replace(VisualizeConstants.LANDING_PAGE_PATH);
+ }
+
+ if (searchSessionId) {
+ data.search.session.continue(searchSessionId);
+ } else {
+ data.search.session.start();
}
}, [services]);
diff --git a/src/plugins/visualize/public/application/components/visualize_editor.tsx b/src/plugins/visualize/public/application/components/visualize_editor.tsx
index c6333e978183..546738bf36c3 100644
--- a/src/plugins/visualize/public/application/components/visualize_editor.tsx
+++ b/src/plugins/visualize/public/application/components/visualize_editor.tsx
@@ -27,6 +27,7 @@ import { VisualizeConstants } from '../..';
export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => {
const { id: visualizationIdFromUrl } = useParams<{ id: string }>();
const [originatingApp, setOriginatingApp] = useState();
+ const [embeddableIdValue, setEmbeddableId] = useState();
const { services } = useKibana();
const [eventEmitter] = useState(new EventEmitter());
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(!visualizationIdFromUrl);
@@ -55,8 +56,17 @@ export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => {
useLinkedSearchUpdates(services, eventEmitter, appState, savedVisInstance);
useEffect(() => {
- const { originatingApp: value } =
- services.stateTransferService.getIncomingEditorState(VisualizeConstants.APP_ID) || {};
+ const { stateTransferService, data } = services;
+ const { originatingApp: value, searchSessionId, embeddableId } =
+ stateTransferService.getIncomingEditorState(VisualizeConstants.APP_ID) || {};
+
+ if (searchSessionId) {
+ data.search.session.continue(searchSessionId);
+ } else {
+ data.search.session.start();
+ }
+
+ setEmbeddableId(embeddableId);
setOriginatingApp(value);
}, [services]);
@@ -65,7 +75,7 @@ export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => {
return () => {
eventEmitter.removeAllListeners();
};
- }, [eventEmitter]);
+ }, [eventEmitter, services]);
return (
{
setHasUnsavedChanges={setHasUnsavedChanges}
visEditorRef={visEditorRef}
onAppLeave={onAppLeave}
+ embeddableId={embeddableIdValue}
/>
);
};
diff --git a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx
index f6ef1caf9c9e..ad933e597f0a 100644
--- a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx
+++ b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx
@@ -61,13 +61,21 @@ const TopNav = ({
const session = embeddableHandler.openInspector();
setInspectorSession(session);
}, [embeddableHandler]);
+
+ const doReload = useCallback(async () => {
+ // start a new session to make sure all data is up to date
+ services.data.search.session.start();
+
+ await visInstance.embeddableHandler.reload();
+ }, [visInstance.embeddableHandler, services.data.search.session]);
+
const handleRefresh = useCallback(
(_payload: any, isUpdate?: boolean) => {
if (isUpdate === false) {
- visInstance.embeddableHandler.reload();
+ doReload();
}
},
- [visInstance.embeddableHandler]
+ [doReload]
);
const config = useMemo(() => {
@@ -185,7 +193,7 @@ const TopNav = ({
.getAutoRefreshFetch$()
.subscribe(async (done) => {
try {
- await visInstance.embeddableHandler.reload();
+ await doReload();
} finally {
done();
}
@@ -193,7 +201,7 @@ const TopNav = ({
return () => {
autoRefreshFetchSub.unsubscribe();
};
- }, [services.data.query.timefilter.timefilter, visInstance.embeddableHandler]);
+ }, [services.data.query.timefilter.timefilter, doReload]);
return isChromeVisible ? (
/**
diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx
index 82757e9a8e35..ed361bbdb104 100644
--- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx
+++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx
@@ -81,6 +81,7 @@ export const getTopNavConfig = (
embeddableId,
}: TopNavConfigParams,
{
+ data,
application,
chrome,
history,
@@ -154,17 +155,17 @@ export const getTopNavConfig = (
saveOptions.dashboardId === 'new' ? '#/create' : `#/view/${saveOptions.dashboardId}`;
}
- if (newlyCreated && stateTransfer) {
+ if (stateTransfer) {
stateTransfer.navigateToWithEmbeddablePackage(app, {
state: {
type: VISUALIZE_EMBEDDABLE_TYPE,
input: { savedObjectId: id },
- embeddableId,
+ embeddableId: savedVis.copyOnSave ? undefined : embeddableId,
+ searchSessionId: data.search.session.getSessionId(),
},
path,
});
} else {
- // TODO: need the same thing here?
application.navigateToApp(app, { path });
}
} else {
@@ -214,6 +215,7 @@ export const getTopNavConfig = (
} as VisualizeInput,
embeddableId,
type: VISUALIZE_EMBEDDABLE_TYPE,
+ searchSessionId: data.search.session.getSessionId(),
};
stateTransfer.navigateToWithEmbeddablePackage(originatingApp, { state });
};
@@ -394,6 +396,7 @@ export const getTopNavConfig = (
} as VisualizeInput,
embeddableId,
type: VISUALIZE_EMBEDDABLE_TYPE,
+ searchSessionId: data.search.session.getSessionId(),
};
const path = dashboardId === 'new' ? '#/create' : `#/view/${dashboardId}`;
diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts
index 9eda709e58c3..8898076d7ddb 100644
--- a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts
+++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts
@@ -40,9 +40,10 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async (
savedObjectsPublic,
} = visualizeServices;
const embeddableHandler = (await createVisEmbeddableFromObject(vis, {
+ id: '',
timeRange: data.query.timefilter.timefilter.getTime(),
filters: data.query.filterManager.getFilters(),
- id: '',
+ searchSessionId: data.search.session.getSessionId(),
})) as VisualizeEmbeddableContract;
embeddableHandler.getOutput$().subscribe((output) => {
diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts
index b5ddbdf6d10a..00c3545034b3 100644
--- a/src/plugins/visualize/public/plugin.ts
+++ b/src/plugins/visualize/public/plugin.ts
@@ -215,6 +215,7 @@ export class VisualizePlugin
const { renderApp } = await import('./application');
const unmount = renderApp(params, services);
return () => {
+ data.search.session.clear();
params.element.classList.remove('visAppWrapper');
unlistenParentHistory();
unmount();
diff --git a/test/accessibility/apps/dashboard.ts b/test/accessibility/apps/dashboard.ts
index 20b18583d0d7..5a3ec9d8fc86 100644
--- a/test/accessibility/apps/dashboard.ts
+++ b/test/accessibility/apps/dashboard.ts
@@ -100,7 +100,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('Add one more saved object to cancel it', async () => {
- await testSubjects.click('savedObjectTitle[Flights]-Average-Ticket-Price');
+ await testSubjects.click('savedObjectTitle[Flights]-Destination-Weather');
await a11y.testAppSnapshot();
});
diff --git a/test/accessibility/apps/dashboard_panel.ts b/test/accessibility/apps/dashboard_panel.ts
index 2a6c290172a9..c1c8ff402a32 100644
--- a/test/accessibility/apps/dashboard_panel.ts
+++ b/test/accessibility/apps/dashboard_panel.ts
@@ -26,7 +26,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('dashboard panel open ', async () => {
- const header = await dashboardPanelActions.getPanelHeading('[Flights] Airline Carrier');
+ const header = await dashboardPanelActions.getPanelHeading('[Flights] Flight count');
await dashboardPanelActions.toggleContextMenu(header);
await a11y.testAppSnapshot();
// doing this again will close the Context Menu, so that next snapshot can start clean.
@@ -34,7 +34,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('dashboard panel inspect', async () => {
- await dashboardPanelActions.openInspectorByTitle('[Flights] Airline Carrier');
+ await dashboardPanelActions.openInspectorByTitle('[Flights] Flight count');
await a11y.testAppSnapshot();
});
@@ -61,9 +61,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('dashboard panel full screen', async () => {
- const header = await dashboardPanelActions.getPanelHeading('[Flights] Airline Carrier');
+ const header = await dashboardPanelActions.getPanelHeading('[Flights] Flight count');
await dashboardPanelActions.toggleContextMenu(header);
- await dashboardPanelActions.clickContextMenuMoreItem();
await testSubjects.click('embeddablePanelAction-togglePanel');
await a11y.testAppSnapshot();
diff --git a/test/api_integration/apis/home/sample_data.ts b/test/api_integration/apis/home/sample_data.ts
index c681ad325e56..1e029bc1e04d 100644
--- a/test/api_integration/apis/home/sample_data.ts
+++ b/test/api_integration/apis/home/sample_data.ts
@@ -43,7 +43,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.body).to.eql({
elasticsearchIndicesCreated: { kibana_sample_data_flights: 13059 },
- kibanaSavedObjectsLoaded: 23,
+ kibanaSavedObjectsLoaded: 11,
});
});
diff --git a/test/functional/apps/home/_sample_data.ts b/test/functional/apps/home/_sample_data.ts
index a35fda2f53ed..adb99d0d42d0 100644
--- a/test/functional/apps/home/_sample_data.ts
+++ b/test/functional/apps/home/_sample_data.ts
@@ -15,7 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const find = getService('find');
const log = getService('log');
const security = getService('security');
- const pieChart = getService('pieChart');
+ const elasticChart = getService('elasticChart');
const renderable = getService('renderable');
const dashboardExpect = getService('dashboardExpect');
const PageObjects = getPageObjects(['common', 'header', 'home', 'dashboard', 'timePicker']);
@@ -89,17 +89,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const toTime = `${todayYearMonthDay} @ 23:59:59.999`;
await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
const panelCount = await PageObjects.dashboard.getPanelCount();
- expect(panelCount).to.be(18);
+ expect(panelCount).to.be(17);
});
it('should render visualizations', async () => {
await PageObjects.home.launchSampleDashboard('flights');
await PageObjects.header.waitUntilLoadingHasFinished();
await renderable.waitForRender();
- log.debug('Checking pie charts rendered');
- await pieChart.expectPieSliceCount(4);
- log.debug('Checking area, bar and heatmap charts rendered');
- await dashboardExpect.seriesElementCount(15);
+ log.debug('Checking charts rendered');
+ await elasticChart.waitForRenderComplete('lnsVisualizationContainer');
log.debug('Checking saved searches rendered');
await dashboardExpect.savedSearchRowCount(10);
log.debug('Checking input controls rendered');
@@ -107,8 +105,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
log.debug('Checking tag cloud rendered');
await dashboardExpect.tagCloudWithValuesFound(['Sunny', 'Rain', 'Clear', 'Cloudy', 'Hail']);
log.debug('Checking vega chart rendered');
- const tsvb = await find.existsByCssSelector('.vgaVis__view');
- expect(tsvb).to.be(true);
+ expect(await find.existsByCssSelector('.vgaVis__view')).to.be(true);
});
it('should launch sample logs data set dashboard', async () => {
diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts
index 49b2ad8f9646..91aec66966df 100644
--- a/test/functional/apps/visualize/_tsvb_chart.ts
+++ b/test/functional/apps/visualize/_tsvb_chart.ts
@@ -12,7 +12,6 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const browser = getService('browser');
- const esArchiver = getService('esArchiver');
const log = getService('log');
const inspector = getService('inspector');
const retry = getService('retry');
@@ -198,14 +197,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
});
- // FLAKY: https://github.com/elastic/kibana/issues/103252
- describe.skip('switch index patterns', () => {
- before(async () => {
- await esArchiver.loadIfNeeded(
- 'test/functional/fixtures/es_archiver/index_pattern_without_timefield'
- );
- });
-
+ describe('switch index pattern mode', () => {
beforeEach(async () => {
await PageObjects.visualBuilder.resetPage();
await PageObjects.visualBuilder.clickMetric();
@@ -215,41 +207,32 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.visualBuilder.setDropLastBucket(true);
await PageObjects.visualBuilder.clickDataTab('metric');
await PageObjects.timePicker.setAbsoluteRange(
- 'Sep 22, 2019 @ 00:00:00.000',
- 'Sep 23, 2019 @ 00:00:00.000'
+ 'Sep 19, 2015 @ 06:31:44.000',
+ 'Sep 22, 2015 @ 18:31:44.000'
);
});
- after(async () => {
- await security.testUser.restoreDefaults();
- await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana');
- await PageObjects.visualize.initTests();
- });
-
const switchIndexTest = async (useKibanaIndexes: boolean) => {
await PageObjects.visualBuilder.clickPanelOptions('metric');
await PageObjects.visualBuilder.setIndexPatternValue('', false);
- const value = await PageObjects.visualBuilder.getMetricValue();
- expect(value).to.eql('0');
-
// Sometimes popovers take some time to appear in Firefox (#71979)
await retry.tryForTime(20000, async () => {
- await PageObjects.visualBuilder.setIndexPatternValue('with-timefield', useKibanaIndexes);
+ await PageObjects.visualBuilder.setIndexPatternValue('logstash-*', useKibanaIndexes);
await PageObjects.visualBuilder.waitForIndexPatternTimeFieldOptionsLoaded();
- await PageObjects.visualBuilder.selectIndexPatternTimeField('timestamp');
+ await PageObjects.visualBuilder.selectIndexPatternTimeField('@timestamp');
});
const newValue = await PageObjects.visualBuilder.getMetricValue();
- expect(newValue).to.eql('1');
+ expect(newValue).to.eql('156');
};
- it('should be able to switch using text mode selection', async () => {
- await switchIndexTest(false);
- });
-
it('should be able to switch combo box mode selection', async () => {
await switchIndexTest(true);
});
+
+ it('should be able to switch using text mode selection', async () => {
+ await switchIndexTest(false);
+ });
});
describe('browser history changes', () => {
diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts
index 5e3dd2019d0a..8cd98b17f5d3 100644
--- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts
+++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts
@@ -17,6 +17,7 @@ const createAlertingAuthorizationMock = () => {
filterByRuleTypeAuthorization: jest.fn(),
getFindAuthorizationFilter: jest.fn(),
getAugmentedRuleTypesWithAuthorization: jest.fn(),
+ getSpaceId: jest.fn(),
};
return mocked;
};
diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts
index 50a1b9d84ff6..b3cd47d47dbc 100644
--- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts
+++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts
@@ -81,6 +81,7 @@ export class AlertingAuthorization {
private readonly featuresIds: Promise>;
private readonly allPossibleConsumers: Promise;
private readonly exemptConsumerIds: string[];
+ private readonly spaceId: Promise;
constructor({
alertTypeRegistry,
@@ -101,6 +102,8 @@ export class AlertingAuthorization {
// manually authorize each rule type in the management UI.
this.exemptConsumerIds = exemptConsumerIds;
+ this.spaceId = getSpace(request).then((maybeSpace) => maybeSpace?.id);
+
this.featuresIds = getSpace(request)
.then((maybeSpace) => new Set(maybeSpace?.disabledFeatures ?? []))
.then(
@@ -138,6 +141,10 @@ export class AlertingAuthorization {
return this.authorization?.mode?.useRbacForRequest(this.request) ?? false;
}
+ public async getSpaceId(): Promise {
+ return this.spaceId;
+ }
+
/*
* This method exposes the private 'augmentRuleTypesWithAuthorization' to be
* used by the RAC/Alerts client
diff --git a/x-pack/plugins/apm/common/search_strategies/correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/correlations/types.ts
index 1698708aeb77..6e1fd115aace 100644
--- a/x-pack/plugins/apm/common/search_strategies/correlations/types.ts
+++ b/x-pack/plugins/apm/common/search_strategies/correlations/types.ts
@@ -19,7 +19,6 @@ export interface ResponseHit {
}
export interface SearchServiceParams {
- index: string;
environment?: string;
kuery?: string;
serviceName?: string;
@@ -31,6 +30,10 @@ export interface SearchServiceParams {
percentileThresholdValue?: number;
}
+export interface SearchServiceFetchParams extends SearchServiceParams {
+ index: string;
+}
+
export interface SearchServiceValue {
histogram: HistogramItem[];
value: string;
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 88d1823f05cc..26a44180b837 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx
@@ -27,7 +27,7 @@ export function RumHome() {
const { isSmall, isXXL } = useBreakPoints();
- const envStyle = isSmall ? {} : { maxWidth: 200 };
+ const envStyle = isSmall ? {} : { maxWidth: 500 };
return (
@@ -59,7 +59,7 @@ export function RumHome() {
function PageHeader() {
const { isSmall } = useBreakPoints();
- const envStyle = isSmall ? {} : { maxWidth: 200 };
+ const envStyle = isSmall ? {} : { maxWidth: 400 };
return (
diff --git a/x-pack/plugins/apm/public/components/app/correlations/correlations_chart.tsx b/x-pack/plugins/apm/public/components/app/correlations/correlations_chart.tsx
index f4e39c37e289..cfc57d3b3e4a 100644
--- a/x-pack/plugins/apm/public/components/app/correlations/correlations_chart.tsx
+++ b/x-pack/plugins/apm/public/components/app/correlations/correlations_chart.tsx
@@ -70,6 +70,11 @@ const chartTheme: PartialTheme = {
},
};
+// Log based axis cannot start a 0. Use a small positive number instead.
+const yAxisDomain = {
+ min: 0.00001,
+};
+
interface CorrelationsChartProps {
field?: string;
value?: string;
@@ -140,7 +145,10 @@ export function CorrelationsChart({
const histogram = replaceHistogramDotsWithBars(originalHistogram);
return (
-
+
{
setIsFlyoutVisible(true);
@@ -147,13 +148,17 @@ export function Correlations() {
{isFlyoutVisible && (
setIsFlyoutVisible(false)}
>
-
+
{CORRELATIONS_TITLE}
(enableInspectEsQueries);
+
const {
+ ccsWarning,
+ log,
error,
histograms,
percentileThresholdValue,
@@ -76,7 +87,6 @@ export function MlLatencyCorrelations({ onClose }: Props) {
cancelFetch,
overallHistogram: originalOverallHistogram,
} = useCorrelations({
- index: 'apm-*',
...{
...{
environment,
@@ -286,9 +296,10 @@ export function MlLatencyCorrelations({ onClose }: Props) {
-
+
+ {ccsWarning && (
+ <>
+
+
+
+ {i18n.translate(
+ 'xpack.apm.correlations.latencyCorrelations.ccsWarningCalloutBody',
+ {
+ defaultMessage:
+ 'Data for the correlation analysis could not be fully retrieved. This feature is supported only for 7.14 and later versions.',
+ }
+ )}
+
+
+ >
+ )}
{overallHistogram !== undefined ? (
<>
-
+
{i18n.translate(
'xpack.apm.correlations.latencyCorrelations.chartTitle',
{
@@ -341,32 +376,58 @@ export function MlLatencyCorrelations({ onClose }: Props) {
>
) : null}
- {histograms.length > 0 && selectedHistogram !== undefined && (
-
+
+ {histograms.length > 0 && selectedHistogram !== undefined && (
+
+ )}
+ {histograms.length < 1 && progress > 0.99 ? (
+ <>
+
+
+
+
+ >
+ ) : null}
+
+ {log.length > 0 && displayLog && (
+
+
+ {log.map((d, i) => {
+ const splitItem = d.split(': ');
+ return (
+
+
+ {splitItem[0]} {splitItem[1]}
+
+
+ );
+ })}
+
+
)}
- {histograms.length < 1 && progress > 0.99 ? (
- <>
-
-
-
-
- >
- ) : null}
>
);
}
diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_correlations.ts b/x-pack/plugins/apm/public/components/app/correlations/use_correlations.ts
index 2baeb63fa4a2..05cb367a9fde 100644
--- a/x-pack/plugins/apm/public/components/app/correlations/use_correlations.ts
+++ b/x-pack/plugins/apm/public/components/app/correlations/use_correlations.ts
@@ -21,7 +21,6 @@ import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'
import { ApmPluginStartDeps } from '../../../plugin';
interface CorrelationsOptions {
- index: string;
environment?: string;
kuery?: string;
serviceName?: string;
@@ -37,6 +36,7 @@ interface RawResponse {
values: SearchServiceValue[];
overallHistogram: HistogramItem[];
log: string[];
+ ccsWarning: boolean;
}
export const useCorrelations = (params: CorrelationsOptions) => {
@@ -106,6 +106,8 @@ export const useCorrelations = (params: CorrelationsOptions) => {
};
return {
+ ccsWarning: rawResponse?.ccsWarning ?? false,
+ log: rawResponse?.log ?? [],
error,
histograms: rawResponse?.values ?? [],
percentileThresholdValue:
diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx
index b4644068fd78..a3b0ec0ac66d 100644
--- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx
@@ -117,6 +117,7 @@ export function getServiceColumns({
)}
{
+ const { agentName, canShowDashboard, environment, serviceName } = args;
+
+ const KibanaContext = createKibanaReactContext(({
+ application: {
+ capabilities: { dashboard: { show: canShowDashboard } },
+ },
+ http: { basePath: { get: () => '' } },
+ } as unknown) as Partial);
+
+ return (
+
+
+
+
+
+
+
+ );
+ },
+ ],
+};
+
+export const Example: Story = () => {
+ return ;
+};
+Example.args = {
+ agentName: 'iOS/swift',
+ canShowDashboard: true,
+ environment: 'testEnvironment',
+ serviceName: 'testServiceName',
+};
diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.test.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.test.tsx
new file mode 100644
index 000000000000..b8b0cfa3054d
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.test.tsx
@@ -0,0 +1,85 @@
+/*
+ * 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 { composeStories } from '@storybook/testing-react';
+import { render, screen } from '@testing-library/react';
+import React from 'react';
+import {
+ ENVIRONMENT_ALL,
+ ENVIRONMENT_NOT_DEFINED,
+} from '../../../../../common/environment_filter_values';
+import * as stories from './analyze_data_button.stories';
+
+const { Example } = composeStories(stories);
+
+describe('AnalyzeDataButton', () => {
+ describe('with a non-RUM and non-mobile agent', () => {
+ it('renders nothing', () => {
+ render();
+
+ expect(screen.queryByRole('link')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('with no dashboard show capabilities', () => {
+ it('renders nothing', () => {
+ render();
+
+ expect(screen.queryByRole('link')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('with a RUM agent', () => {
+ it('uses a ux dataType', () => {
+ render();
+
+ expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual(
+ 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:ux,isNew:!t,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))'
+ );
+ });
+ });
+
+ describe('with a mobile agent', () => {
+ it('uses a mobile dataType', () => {
+ render();
+
+ expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual(
+ 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))'
+ );
+ });
+ });
+
+ describe('with no environment', () => {
+ it('does not include the environment', () => {
+ render();
+
+ expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual(
+ 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))'
+ );
+ });
+ });
+
+ describe('with environment not defined', () => {
+ it('does not include the environment', () => {
+ render();
+
+ expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual(
+ 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))'
+ );
+ });
+ });
+
+ describe('with environment all', () => {
+ it('uses ALL_VALUES', () => {
+ render();
+
+ expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual(
+ 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.environment:!(ALL_VALUES),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))'
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx
new file mode 100644
index 000000000000..d8ff7fdf47c5
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx
@@ -0,0 +1,91 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
+import {
+ createExploratoryViewUrl,
+ SeriesUrl,
+} from '../../../../../../observability/public';
+import { ALL_VALUES_SELECTED } from '../../../../../../observability/public';
+import {
+ isIosAgentName,
+ isRumAgentName,
+} from '../../../../../common/agent_name';
+import {
+ SERVICE_ENVIRONMENT,
+ SERVICE_NAME,
+} from '../../../../../common/elasticsearch_fieldnames';
+import {
+ ENVIRONMENT_ALL,
+ ENVIRONMENT_NOT_DEFINED,
+} from '../../../../../common/environment_filter_values';
+import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
+import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
+
+function getEnvironmentDefinition(environment?: string) {
+ switch (environment) {
+ case ENVIRONMENT_ALL.value:
+ return { [SERVICE_ENVIRONMENT]: [ALL_VALUES_SELECTED] };
+ case ENVIRONMENT_NOT_DEFINED.value:
+ case undefined:
+ return {};
+ default:
+ return { [SERVICE_ENVIRONMENT]: [environment] };
+ }
+}
+
+export function AnalyzeDataButton() {
+ const { agentName, serviceName } = useApmServiceContext();
+ const { services } = useKibana();
+ const { urlParams } = useUrlParams();
+ const { rangeTo, rangeFrom, environment } = urlParams;
+ const basepath = services.http?.basePath.get();
+ const canShowDashboard = services.application?.capabilities.dashboard.show;
+
+ if (
+ (isRumAgentName(agentName) || isIosAgentName(agentName)) &&
+ canShowDashboard
+ ) {
+ const href = createExploratoryViewUrl(
+ {
+ 'apm-series': {
+ dataType: isRumAgentName(agentName) ? 'ux' : 'mobile',
+ time: { from: rangeFrom, to: rangeTo },
+ reportType: 'kpi-over-time',
+ reportDefinitions: {
+ [SERVICE_NAME]: [serviceName],
+ ...getEnvironmentDefinition(environment),
+ },
+ operationType: 'average',
+ isNew: true,
+ } as SeriesUrl,
+ },
+ basepath
+ );
+
+ return (
+
+
+ {i18n.translate('xpack.apm.analyzeDataButton.label', {
+ defaultMessage: 'Analyze data',
+ })}
+
+
+ );
+ }
+
+ return null;
+}
diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx
similarity index 66%
rename from x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx
rename to x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx
index 2e10c853f542..ee5ed91dfb46 100644
--- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx
+++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx
@@ -5,45 +5,33 @@
* 2.0.
*/
-import React from 'react';
-import { i18n } from '@kbn/i18n';
import {
+ EuiBetaBadge,
EuiFlexGroup,
EuiFlexItem,
EuiPageHeaderProps,
EuiTitle,
- EuiBetaBadge,
- EuiToolTip,
- EuiButtonEmpty,
} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { omit } from 'lodash';
-import { ApmMainTemplate } from './apm_main_template';
-import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
-import { ApmServiceContextProvider } from '../../../context/apm_service/apm_service_context';
-import { enableServiceOverview } from '../../../../common/ui_settings_keys';
+import React from 'react';
import {
+ isIosAgentName,
isJavaAgentName,
isRumAgentName,
- isIosAgentName,
-} from '../../../../common/agent_name';
-import { ServiceIcons } from '../../shared/service_icons';
-import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
-import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
-import { useUrlParams } from '../../../context/url_params_context/use_url_params';
-import { ENVIRONMENT_NOT_DEFINED } from '../../../../common/environment_filter_values';
-import {
- SERVICE_NAME,
- SERVICE_ENVIRONMENT,
-} from '../../../../common/elasticsearch_fieldnames';
-import { Correlations } from '../../app/correlations';
-import { SearchBar } from '../../shared/search_bar';
-import {
- createExploratoryViewUrl,
- SeriesUrl,
-} from '../../../../../observability/public';
-import { useApmParams } from '../../../hooks/use_apm_params';
-import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb';
-import { useApmRouter } from '../../../hooks/use_apm_router';
+} from '../../../../../common/agent_name';
+import { enableServiceOverview } from '../../../../../common/ui_settings_keys';
+import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
+import { ApmServiceContextProvider } from '../../../../context/apm_service/apm_service_context';
+import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
+import { useBreadcrumb } from '../../../../context/breadcrumbs/use_breadcrumb';
+import { useApmParams } from '../../../../hooks/use_apm_params';
+import { useApmRouter } from '../../../../hooks/use_apm_router';
+import { Correlations } from '../../../app/correlations';
+import { SearchBar } from '../../../shared/search_bar';
+import { ServiceIcons } from '../../../shared/service_icons';
+import { ApmMainTemplate } from '../apm_main_template';
+import { AnalyzeDataButton } from './analyze_data_button';
type Tab = NonNullable[0] & {
key:
@@ -105,7 +93,9 @@ function TemplateWithContext({
- {serviceName}
+
+ {serviceName}
+
@@ -115,7 +105,7 @@ function TemplateWithContext({
-
+
@@ -132,53 +122,6 @@ function TemplateWithContext({
);
}
-function AnalyzeDataButton({ serviceName }: { serviceName: string }) {
- const { agentName } = useApmServiceContext();
- const { services } = useKibana();
- const { urlParams } = useUrlParams();
- const { rangeTo, rangeFrom, environment } = urlParams;
- const basepath = services.http?.basePath.get();
-
- if (isRumAgentName(agentName) || isIosAgentName(agentName)) {
- const href = createExploratoryViewUrl(
- {
- 'apm-series': {
- dataType: isRumAgentName(agentName) ? 'ux' : 'mobile',
- time: { from: rangeFrom, to: rangeTo },
- reportType: 'kpi-over-time',
- reportDefinitions: {
- [SERVICE_NAME]: [serviceName],
- ...(!!environment && ENVIRONMENT_NOT_DEFINED.value !== environment
- ? { [SERVICE_ENVIRONMENT]: [environment] }
- : {}),
- },
- operationType: 'average',
- isNew: true,
- } as SeriesUrl,
- },
- basepath
- );
-
- return (
-
-
- {i18n.translate('xpack.apm.analyzeDataButton.label', {
- defaultMessage: 'Analyze data',
- })}
-
-
- );
- }
-
- return null;
-}
-
function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) {
const { agentName } = useApmServiceContext();
const { core, config } = useApmPluginContext();
diff --git a/x-pack/plugins/apm/server/lib/fleet/source_maps.ts b/x-pack/plugins/apm/server/lib/fleet/source_maps.ts
index 6d608f7751f3..10514ddcbdf6 100644
--- a/x-pack/plugins/apm/server/lib/fleet/source_maps.ts
+++ b/x-pack/plugins/apm/server/lib/fleet/source_maps.ts
@@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import * as t from 'io-ts';
import {
CoreSetup,
CoreStart,
@@ -14,7 +13,7 @@ import {
import { promisify } from 'util';
import { unzip } from 'zlib';
import { Artifact } from '../../../../fleet/server';
-import { sourceMapRt } from '../../routes/source_maps';
+import { SourceMap } from '../../routes/source_maps';
import { APMPluginStartDependencies } from '../../types';
import { getApmPackgePolicies } from './get_apm_package_policies';
import { APM_SERVER, PackagePolicy } from './register_fleet_policy_callbacks';
@@ -23,7 +22,7 @@ export interface ApmArtifactBody {
serviceName: string;
serviceVersion: string;
bundleFilepath: string;
- sourceMap: t.TypeOf;
+ sourceMap: SourceMap;
}
export type ArtifactSourceMap = Omit & {
body: ApmArtifactBody;
diff --git a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap
index f939a9c39c63..aab8025a7679 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap
+++ b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap
@@ -64,13 +64,7 @@ Object {
},
},
],
- "must_not": Array [
- Object {
- "term": Object {
- "service.environment": "staging",
- },
- },
- ],
+ "must_not": Array [],
},
},
"size": 0,
@@ -512,13 +506,7 @@ Object {
},
},
],
- "must_not": Array [
- Object {
- "term": Object {
- "service.environment": "staging",
- },
- },
- ],
+ "must_not": Array [],
},
},
"size": 0,
@@ -566,13 +554,7 @@ Object {
},
},
],
- "must_not": Array [
- Object {
- "term": Object {
- "service.environment": "staging",
- },
- },
- ],
+ "must_not": Array [],
},
},
"size": 0,
diff --git a/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.test.ts b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.test.ts
new file mode 100644
index 000000000000..ba5e318a1901
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.test.ts
@@ -0,0 +1,31 @@
+/*
+ * 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 { getEsFilter } from './get_es_filter';
+
+describe('getEfFilters', function () {
+ it('should return environment in include filters', function () {
+ const result = getEsFilter({
+ browser: ['Chrome'],
+ environment: 'production',
+ });
+
+ expect(result).toEqual([
+ { terms: { 'user_agent.name': ['Chrome'] } },
+ { term: { 'service.environment': 'production' } },
+ ]);
+ });
+
+ it('should not return environment in exclude filters', function () {
+ const result = getEsFilter(
+ { browserExcluded: ['Chrome'], environment: 'production' },
+ true
+ );
+
+ expect(result).toEqual([{ terms: { 'user_agent.name': ['Chrome'] } }]);
+ });
+});
diff --git a/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts
index 5f587f82e979..76ef9fb95089 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts
+++ b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts
@@ -34,5 +34,8 @@ export function getEsFilter(uiFilters: UxUIFilters, exclude?: boolean) {
};
}) as ESFilter[];
- return [...mappedFilters, ...environmentQuery(uiFilters.environment)];
+ return [
+ ...mappedFilters,
+ ...(exclude ? [] : environmentQuery(uiFilters.environment)),
+ ];
}
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts
index 155cb1f4615b..90d24b6587f4 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts
@@ -7,6 +7,7 @@
import { shuffle, range } from 'lodash';
import type { ElasticsearchClient } from 'src/core/server';
+import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices';
import { fetchTransactionDurationFieldCandidates } from './query_field_candidates';
import { fetchTransactionDurationFieldValuePairs } from './query_field_value_pairs';
import { fetchTransactionDurationPercentiles } from './query_percentiles';
@@ -16,6 +17,7 @@ import { fetchTransactionDurationRanges, HistogramItem } from './query_ranges';
import type {
AsyncSearchProviderProgress,
SearchServiceParams,
+ SearchServiceFetchParams,
SearchServiceValue,
} from '../../../../common/search_strategies/correlations/types';
import { computeExpectationsAndRanges } from './utils/aggregation_utils';
@@ -28,11 +30,14 @@ const currentTimeAsString = () => new Date().toISOString();
export const asyncSearchServiceProvider = (
esClient: ElasticsearchClient,
- params: SearchServiceParams
+ getApmIndices: () => Promise,
+ searchServiceParams: SearchServiceParams,
+ includeFrozen: boolean
) => {
let isCancelled = false;
let isRunning = true;
let error: Error;
+ let ccsWarning = false;
const log: string[] = [];
const logMessage = (message: string) =>
log.push(`${currentTimeAsString()}: ${message}`);
@@ -63,7 +68,15 @@ export const asyncSearchServiceProvider = (
};
const fetchCorrelations = async () => {
+ let params: SearchServiceFetchParams | undefined;
+
try {
+ const indices = await getApmIndices();
+ params = {
+ ...searchServiceParams,
+ index: indices['apm_oss.transactionIndices'],
+ };
+
// 95th percentile to be displayed as a marker in the log log chart
const {
totalDocs,
@@ -172,7 +185,7 @@ export const asyncSearchServiceProvider = (
async function* fetchTransactionDurationHistograms() {
for (const item of shuffle(fieldValuePairs)) {
- if (item === undefined || isCancelled) {
+ if (params === undefined || item === undefined || isCancelled) {
isRunning = false;
return;
}
@@ -222,10 +235,15 @@ export const asyncSearchServiceProvider = (
yield undefined;
}
} catch (e) {
- // don't fail the whole process for individual correlation queries, just add the error to the internal log.
+ // don't fail the whole process for individual correlation queries,
+ // just add the error to the internal log and check if we'd want to set the
+ // cross-cluster search compatibility warning to true.
logMessage(
`Failed to fetch correlation/kstest for '${item.field}/${item.value}'`
);
+ if (params?.index.includes(':')) {
+ ccsWarning = true;
+ }
yield undefined;
}
}
@@ -247,6 +265,10 @@ export const asyncSearchServiceProvider = (
error = e;
}
+ if (error !== undefined && params?.index.includes(':')) {
+ ccsWarning = true;
+ }
+
isRunning = false;
};
@@ -256,6 +278,7 @@ export const asyncSearchServiceProvider = (
const sortedValues = values.sort((a, b) => b.correlation - a.correlation);
return {
+ ccsWarning,
error,
log,
isRunning,
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.ts
index 5d4af3e80f8b..aeb76c37e526 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.ts
@@ -11,7 +11,7 @@ import { pipe } from 'fp-ts/lib/pipeable';
import * as t from 'io-ts';
import { failure } from 'io-ts/lib/PathReporter';
import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames';
-import type { SearchServiceParams } from '../../../../common/search_strategies/correlations/types';
+import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types';
import { rangeRt } from '../../../routes/default_api_types';
import { getCorrelationsFilters } from '../../correlations/get_filters';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
@@ -40,7 +40,7 @@ export const getTermsQuery = (
};
interface QueryParams {
- params: SearchServiceParams;
+ params: SearchServiceFetchParams;
fieldName?: string;
fieldValue?: string;
}
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.ts
index f63c36f90d72..94a708f67860 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.ts
@@ -10,7 +10,7 @@ import type { estypes } from '@elastic/elasticsearch';
import type { ElasticsearchClient } from 'src/core/server';
import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames';
-import type { SearchServiceParams } from '../../../../common/search_strategies/correlations/types';
+import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types';
import { getQueryWithParams } from './get_query_with_params';
@@ -40,7 +40,7 @@ export interface BucketCorrelation {
}
export const getTransactionDurationCorrelationRequest = (
- params: SearchServiceParams,
+ params: SearchServiceFetchParams,
expectations: number[],
ranges: estypes.AggregationsAggregationRange[],
fractions: number[],
@@ -95,7 +95,7 @@ export const getTransactionDurationCorrelationRequest = (
export const fetchTransactionDurationCorrelation = async (
esClient: ElasticsearchClient,
- params: SearchServiceParams,
+ params: SearchServiceFetchParams,
expectations: number[],
ranges: estypes.AggregationsAggregationRange[],
fractions: number[],
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.ts
index 0fbdfef405e0..8aa54e243eec 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.ts
@@ -9,7 +9,7 @@ import type { estypes } from '@elastic/elasticsearch';
import type { ElasticsearchClient } from 'src/core/server';
-import type { SearchServiceParams } from '../../../../common/search_strategies/correlations/types';
+import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types';
import { getQueryWithParams } from './get_query_with_params';
import { Field } from './query_field_value_pairs';
@@ -37,7 +37,7 @@ export const hasPrefixToInclude = (fieldName: string) => {
};
export const getRandomDocsRequest = (
- params: SearchServiceParams
+ params: SearchServiceFetchParams
): estypes.SearchRequest => ({
index: params.index,
body: {
@@ -56,7 +56,7 @@ export const getRandomDocsRequest = (
export const fetchTransactionDurationFieldCandidates = async (
esClient: ElasticsearchClient,
- params: SearchServiceParams
+ params: SearchServiceFetchParams
): Promise<{ fieldCandidates: Field[] }> => {
const { index } = params;
// Get all fields with keyword mapping
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.ts
index 8fde9d3ab137..23928565da08 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.ts
@@ -11,7 +11,7 @@ import type { estypes } from '@elastic/elasticsearch';
import type {
AsyncSearchProviderProgress,
- SearchServiceParams,
+ SearchServiceFetchParams,
} from '../../../../common/search_strategies/correlations/types';
import { getQueryWithParams } from './get_query_with_params';
@@ -26,7 +26,7 @@ type FieldValuePairs = FieldValuePair[];
export type Field = string;
export const getTermsAggRequest = (
- params: SearchServiceParams,
+ params: SearchServiceFetchParams,
fieldName: string
): estypes.SearchRequest => ({
index: params.index,
@@ -46,7 +46,7 @@ export const getTermsAggRequest = (
export const fetchTransactionDurationFieldValuePairs = async (
esClient: ElasticsearchClient,
- params: SearchServiceParams,
+ params: SearchServiceFetchParams,
fieldCandidates: Field[],
progress: AsyncSearchProviderProgress
): Promise => {
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.ts
index 3d623a4df8c3..e9cec25673c6 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.ts
@@ -7,12 +7,12 @@
import { ElasticsearchClient } from 'kibana/server';
import { estypes } from '@elastic/elasticsearch';
-import { SearchServiceParams } from '../../../../common/search_strategies/correlations/types';
+import { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types';
import { getQueryWithParams } from './get_query_with_params';
import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames';
export const getTransactionDurationRangesRequest = (
- params: SearchServiceParams,
+ params: SearchServiceFetchParams,
ranges: estypes.AggregationsAggregationRange[]
): estypes.SearchRequest => ({
index: params.index,
@@ -35,7 +35,7 @@ export const getTransactionDurationRangesRequest = (
*/
export const fetchTransactionDurationFractions = async (
esClient: ElasticsearchClient,
- params: SearchServiceParams,
+ params: SearchServiceFetchParams,
ranges: estypes.AggregationsAggregationRange[]
): Promise<{ fractions: number[]; totalDocCount: number }> => {
const resp = await esClient.search(
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.ts
index 6f61ecbfdcf0..045caabeab26 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.ts
@@ -13,13 +13,13 @@ import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldname
import type {
HistogramItem,
ResponseHit,
- SearchServiceParams,
+ SearchServiceFetchParams,
} from '../../../../common/search_strategies/correlations/types';
import { getQueryWithParams } from './get_query_with_params';
export const getTransactionDurationHistogramRequest = (
- params: SearchServiceParams,
+ params: SearchServiceFetchParams,
interval: number,
fieldName?: string,
fieldValue?: string
@@ -42,7 +42,7 @@ export const getTransactionDurationHistogramRequest = (
export const fetchTransactionDurationHistogram = async (
esClient: ElasticsearchClient,
- params: SearchServiceParams,
+ params: SearchServiceFetchParams,
interval: number,
fieldName?: string,
fieldValue?: string
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.ts
index c4d1abf24b4d..0f897f2e9236 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.ts
@@ -10,14 +10,14 @@ import type { estypes } from '@elastic/elasticsearch';
import type { ElasticsearchClient } from 'src/core/server';
import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames';
-import type { SearchServiceParams } from '../../../../common/search_strategies/correlations/types';
+import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types';
import { getQueryWithParams } from './get_query_with_params';
const HISTOGRAM_INTERVALS = 1000;
export const getHistogramIntervalRequest = (
- params: SearchServiceParams
+ params: SearchServiceFetchParams
): estypes.SearchRequest => ({
index: params.index,
body: {
@@ -32,7 +32,7 @@ export const getHistogramIntervalRequest = (
export const fetchTransactionDurationHistogramInterval = async (
esClient: ElasticsearchClient,
- params: SearchServiceParams
+ params: SearchServiceFetchParams
): Promise => {
const resp = await esClient.search(getHistogramIntervalRequest(params));
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.ts
index 6ee5dd6bcdf8..ba57de2cfde3 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.ts
@@ -19,7 +19,7 @@ import type { estypes } from '@elastic/elasticsearch';
import type { ElasticsearchClient } from 'src/core/server';
import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames';
-import type { SearchServiceParams } from '../../../../common/search_strategies/correlations/types';
+import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types';
import { getQueryWithParams } from './get_query_with_params';
@@ -32,7 +32,7 @@ const getHistogramRangeSteps = (min: number, max: number, steps: number) => {
};
export const getHistogramIntervalRequest = (
- params: SearchServiceParams
+ params: SearchServiceFetchParams
): estypes.SearchRequest => ({
index: params.index,
body: {
@@ -47,7 +47,7 @@ export const getHistogramIntervalRequest = (
export const fetchTransactionDurationHistogramRangeSteps = async (
esClient: ElasticsearchClient,
- params: SearchServiceParams
+ params: SearchServiceFetchParams
): Promise => {
const steps = 100;
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.ts
index c80f5d836c0e..cb302f19a000 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.ts
@@ -10,7 +10,7 @@ import type { estypes } from '@elastic/elasticsearch';
import type { ElasticsearchClient } from 'src/core/server';
import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames';
-import type { SearchServiceParams } from '../../../../common/search_strategies/correlations/types';
+import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types';
import { getQueryWithParams } from './get_query_with_params';
import { SIGNIFICANT_VALUE_DIGITS } from './constants';
@@ -28,7 +28,7 @@ interface ResponseHit {
}
export const getTransactionDurationPercentilesRequest = (
- params: SearchServiceParams,
+ params: SearchServiceFetchParams,
percents?: number[],
fieldName?: string,
fieldValue?: string
@@ -58,7 +58,7 @@ export const getTransactionDurationPercentilesRequest = (
export const fetchTransactionDurationPercentiles = async (
esClient: ElasticsearchClient,
- params: SearchServiceParams,
+ params: SearchServiceFetchParams,
percents?: number[],
fieldName?: string,
fieldValue?: string
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.ts
index 9074e7e0809b..0e813a18fdf4 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.ts
@@ -10,7 +10,7 @@ import type { estypes } from '@elastic/elasticsearch';
import type { ElasticsearchClient } from 'src/core/server';
import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames';
-import type { SearchServiceParams } from '../../../../common/search_strategies/correlations/types';
+import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types';
import { getQueryWithParams } from './get_query_with_params';
@@ -27,7 +27,7 @@ interface ResponseHit {
}
export const getTransactionDurationRangesRequest = (
- params: SearchServiceParams,
+ params: SearchServiceFetchParams,
rangesSteps: number[],
fieldName?: string,
fieldValue?: string
@@ -65,7 +65,7 @@ export const getTransactionDurationRangesRequest = (
export const fetchTransactionDurationRanges = async (
esClient: ElasticsearchClient,
- params: SearchServiceParams,
+ params: SearchServiceFetchParams,
rangesSteps: number[],
fieldName?: string,
fieldValue?: string
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.test.ts
index 09775cb2eb03..401cda97afef 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.test.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.test.ts
@@ -9,6 +9,8 @@ import type { estypes } from '@elastic/elasticsearch';
import { SearchStrategyDependencies } from 'src/plugins/data/server';
+import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices';
+
import {
apmCorrelationsSearchStrategyProvider,
PartialSearchRequest,
@@ -94,10 +96,19 @@ const clientSearchMock = (
};
};
+const getApmIndicesMock = async () =>
+ ({
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'apm_oss.transactionIndices': 'apm-*',
+ } as ApmIndicesConfig);
+
describe('APM Correlations search strategy', () => {
describe('strategy interface', () => {
it('returns a custom search strategy with a `search` and `cancel` function', async () => {
- const searchStrategy = await apmCorrelationsSearchStrategyProvider();
+ const searchStrategy = await apmCorrelationsSearchStrategyProvider(
+ getApmIndicesMock,
+ false
+ );
expect(typeof searchStrategy.search).toBe('function');
expect(typeof searchStrategy.cancel).toBe('function');
});
@@ -106,12 +117,14 @@ describe('APM Correlations search strategy', () => {
describe('search', () => {
let mockClientFieldCaps: jest.Mock;
let mockClientSearch: jest.Mock;
+ let mockGetApmIndicesMock: jest.Mock;
let mockDeps: SearchStrategyDependencies;
let params: Required['params'];
beforeEach(() => {
mockClientFieldCaps = jest.fn(clientFieldCapsMock);
mockClientSearch = jest.fn(clientSearchMock);
+ mockGetApmIndicesMock = jest.fn(getApmIndicesMock);
mockDeps = ({
esClient: {
asCurrentUser: {
@@ -121,7 +134,6 @@ describe('APM Correlations search strategy', () => {
},
} as unknown) as SearchStrategyDependencies;
params = {
- index: 'apm-*',
start: '2020',
end: '2021',
};
@@ -130,7 +142,13 @@ describe('APM Correlations search strategy', () => {
describe('async functionality', () => {
describe('when no params are provided', () => {
it('throws an error', async () => {
- const searchStrategy = await apmCorrelationsSearchStrategyProvider();
+ const searchStrategy = await apmCorrelationsSearchStrategyProvider(
+ mockGetApmIndicesMock,
+ false
+ );
+
+ expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(0);
+
expect(() => searchStrategy.search({}, {}, mockDeps)).toThrow(
'Invalid request parameters.'
);
@@ -139,8 +157,14 @@ describe('APM Correlations search strategy', () => {
describe('when no ID is provided', () => {
it('performs a client search with params', async () => {
- const searchStrategy = await apmCorrelationsSearchStrategyProvider();
+ const searchStrategy = await apmCorrelationsSearchStrategyProvider(
+ mockGetApmIndicesMock,
+ false
+ );
await searchStrategy.search({ params }, {}, mockDeps).toPromise();
+
+ expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(1);
+
const [[request]] = mockClientSearch.mock.calls;
expect(request.index).toEqual('apm-*');
@@ -179,7 +203,10 @@ describe('APM Correlations search strategy', () => {
describe('when an ID with params is provided', () => {
it('retrieves the current request', async () => {
- const searchStrategy = await apmCorrelationsSearchStrategyProvider();
+ const searchStrategy = await apmCorrelationsSearchStrategyProvider(
+ mockGetApmIndicesMock,
+ false
+ );
const response = await searchStrategy
.search({ params }, {}, mockDeps)
.toPromise();
@@ -190,6 +217,7 @@ describe('APM Correlations search strategy', () => {
.search({ id: searchStrategyId, params }, {}, mockDeps)
.toPromise();
+ expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(1);
expect(response2).toEqual(
expect.objectContaining({ id: searchStrategyId })
);
@@ -201,11 +229,16 @@ describe('APM Correlations search strategy', () => {
mockClientSearch
.mockReset()
.mockRejectedValueOnce(new Error('client error'));
- const searchStrategy = await apmCorrelationsSearchStrategyProvider();
+ const searchStrategy = await apmCorrelationsSearchStrategyProvider(
+ mockGetApmIndicesMock,
+ false
+ );
const response = await searchStrategy
.search({ params }, {}, mockDeps)
.toPromise();
+ expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(1);
+
expect(response).toEqual(
expect.objectContaining({ isRunning: true })
);
@@ -213,11 +246,15 @@ describe('APM Correlations search strategy', () => {
});
it('triggers the subscription only once', async () => {
- expect.assertions(1);
- const searchStrategy = await apmCorrelationsSearchStrategyProvider();
+ expect.assertions(2);
+ const searchStrategy = await apmCorrelationsSearchStrategyProvider(
+ mockGetApmIndicesMock,
+ false
+ );
searchStrategy
.search({ params }, {}, mockDeps)
.subscribe((response) => {
+ expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(1);
expect(response).toEqual(
expect.objectContaining({ loaded: 0, isRunning: true })
);
@@ -227,12 +264,16 @@ describe('APM Correlations search strategy', () => {
describe('response', () => {
it('sends an updated response on consecutive search calls', async () => {
- const searchStrategy = await apmCorrelationsSearchStrategyProvider();
+ const searchStrategy = await apmCorrelationsSearchStrategyProvider(
+ mockGetApmIndicesMock,
+ false
+ );
const response1 = await searchStrategy
.search({ params }, {}, mockDeps)
.toPromise();
+ expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(1);
expect(typeof response1.id).toEqual('string');
expect(response1).toEqual(
expect.objectContaining({ loaded: 0, isRunning: true })
@@ -244,6 +285,7 @@ describe('APM Correlations search strategy', () => {
.search({ id: response1.id, params }, {}, mockDeps)
.toPromise();
+ expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(1);
expect(response2.id).toEqual(response1.id);
expect(response2).toEqual(
expect.objectContaining({ loaded: 100, isRunning: false })
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.ts
index 8f2e6913c0d0..3601f19ad705 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.ts
@@ -19,6 +19,8 @@ import type {
SearchServiceValue,
} from '../../../../common/search_strategies/correlations/types';
+import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices';
+
import { asyncSearchServiceProvider } from './async_search_service';
export type PartialSearchRequest = IKibanaSearchRequest;
@@ -26,10 +28,10 @@ export type PartialSearchResponse = IKibanaSearchResponse<{
values: SearchServiceValue[];
}>;
-export const apmCorrelationsSearchStrategyProvider = (): ISearchStrategy<
- PartialSearchRequest,
- PartialSearchResponse
-> => {
+export const apmCorrelationsSearchStrategyProvider = (
+ getApmIndices: () => Promise,
+ includeFrozen: boolean
+): ISearchStrategy => {
const asyncSearchServiceMap = new Map<
string,
ReturnType
@@ -65,7 +67,9 @@ export const apmCorrelationsSearchStrategyProvider = (): ISearchStrategy<
} else {
getAsyncSearchServiceState = asyncSearchServiceProvider(
deps.esClient.asCurrentUser,
- request.params
+ getApmIndices,
+ request.params,
+ includeFrozen
);
}
@@ -73,6 +77,7 @@ export const apmCorrelationsSearchStrategyProvider = (): ISearchStrategy<
const id = request.id ?? uuid();
const {
+ ccsWarning,
error,
log,
isRunning,
@@ -102,6 +107,7 @@ export const apmCorrelationsSearchStrategyProvider = (): ISearchStrategy<
isRunning,
isPartial: isRunning,
rawResponse: {
+ ccsWarning,
log,
took,
values,
diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts
index 3a7eb738dd3b..d28e43d9cb97 100644
--- a/x-pack/plugins/apm/server/plugin.ts
+++ b/x-pack/plugins/apm/server/plugin.ts
@@ -16,6 +16,7 @@ import {
PluginInitializerContext,
} from 'src/core/server';
import { isEmpty, mapValues, once } from 'lodash';
+import { SavedObjectsClient } from '../../../../src/core/server';
import { TECHNICAL_COMPONENT_TEMPLATE_NAME } from '../../rule_registry/common/assets';
import { mappingFromFieldMap } from '../../rule_registry/common/mapping_from_field_map';
import { APMConfig, APMXPackConfig, APM_SERVER_FEATURE_ID } from '.';
@@ -248,12 +249,24 @@ export class APMPlugin
});
// search strategies for async partial search results
- if (plugins.data?.search?.registerSearchStrategy !== undefined) {
- plugins.data.search.registerSearchStrategy(
- 'apmCorrelationsSearchStrategy',
- apmCorrelationsSearchStrategyProvider()
- );
- }
+ core.getStartServices().then(([coreStart]) => {
+ (async () => {
+ const savedObjectsClient = new SavedObjectsClient(
+ coreStart.savedObjects.createInternalRepository()
+ );
+
+ plugins.data.search.registerSearchStrategy(
+ 'apmCorrelationsSearchStrategy',
+ apmCorrelationsSearchStrategyProvider(
+ boundGetApmIndices,
+ await coreStart.uiSettings
+ .asScopedToClient(savedObjectsClient)
+ .get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN)
+ )
+ );
+ })();
+ });
+
return {
config$: mergedConfig$,
getApmIndices: boundGetApmIndices,
diff --git a/x-pack/plugins/apm/server/routes/fleet.ts b/x-pack/plugins/apm/server/routes/fleet.ts
index a01c9dd1579b..afe18b33c482 100644
--- a/x-pack/plugins/apm/server/routes/fleet.ts
+++ b/x-pack/plugins/apm/server/routes/fleet.ts
@@ -138,10 +138,12 @@ const getMigrationCheckRoute = createApmServerRoute({
const fleetPluginStart = await plugins.fleet.start();
const securityPluginStart = await plugins.security.start();
const hasRequiredRole = isSuperuser({ securityPluginStart, request });
- const cloudAgentPolicy = await getCloudAgentPolicy({
- savedObjectsClient,
- fleetPluginStart,
- });
+ const cloudAgentPolicy = hasRequiredRole
+ ? await getCloudAgentPolicy({
+ savedObjectsClient,
+ fleetPluginStart,
+ })
+ : undefined;
return {
has_cloud_agent_policy: !!cloudAgentPolicy,
has_cloud_apm_package_policy: !!getApmPackagePolicy(cloudAgentPolicy),
diff --git a/x-pack/plugins/apm/server/routes/source_maps.ts b/x-pack/plugins/apm/server/routes/source_maps.ts
index f6d160e68a76..d92bad31cd8d 100644
--- a/x-pack/plugins/apm/server/routes/source_maps.ts
+++ b/x-pack/plugins/apm/server/routes/source_maps.ts
@@ -5,9 +5,9 @@
* 2.0.
*/
import Boom from '@hapi/boom';
-import { jsonRt } from '@kbn/io-ts-utils';
import * as t from 'io-ts';
import { SavedObjectsClientContract } from 'kibana/server';
+import { jsonRt } from '@kbn/io-ts-utils';
import {
createApmArtifact,
deleteApmArtifact,
@@ -17,6 +17,7 @@ import {
import { getInternalSavedObjectsClient } from '../lib/helpers/get_internal_saved_objects_client';
import { createApmServerRoute } from './create_apm_server_route';
import { createApmServerRouteRepository } from './create_apm_server_route_repository';
+import { stringFromBufferRt } from '../utils/string_from_buffer_rt';
export const sourceMapRt = t.intersection([
t.type({
@@ -32,6 +33,8 @@ export const sourceMapRt = t.intersection([
}),
]);
+export type SourceMap = t.TypeOf;
+
const listSourceMapRoute = createApmServerRoute({
endpoint: 'GET /api/apm/sourcemaps',
options: { tags: ['access:apm'] },
@@ -62,7 +65,10 @@ const uploadSourceMapRoute = createApmServerRoute({
service_name: t.string,
service_version: t.string,
bundle_filepath: t.string,
- sourcemap: jsonRt.pipe(sourceMapRt),
+ sourcemap: t
+ .union([t.string, stringFromBufferRt])
+ .pipe(jsonRt)
+ .pipe(sourceMapRt),
}),
}),
handler: async ({ params, plugins, core }) => {
diff --git a/x-pack/plugins/apm/server/utils/string_from_buffer_rt.test.ts b/x-pack/plugins/apm/server/utils/string_from_buffer_rt.test.ts
new file mode 100644
index 000000000000..4e21215cac8b
--- /dev/null
+++ b/x-pack/plugins/apm/server/utils/string_from_buffer_rt.test.ts
@@ -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 { isRight } from 'fp-ts/lib/Either';
+import { stringFromBufferRt } from './string_from_buffer_rt';
+
+const sourceMap = {
+ version: 3,
+ file: 'static/js/main.chunk.js',
+ sources: [
+ '/foo/src/index.css',
+ '/foo/src/App.js',
+ 'webpack:///./src/index.css?bb0a',
+ '/foo/src/index.js',
+ '/foo/src/reportWebVitals.js',
+ ],
+ sourcesContent: [
+ "// Imports\nimport ___CSS_LOADER_API_IMPORT___ from \"../node_modules/css-loader/dist/runtime/api.js\";\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(true);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"body {\\n margin: 0;\\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\\n sans-serif;\\n -webkit-font-smoothing: antialiased;\\n -moz-osx-font-smoothing: grayscale;\\n}\\n\\ncode {\\n font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\\n monospace;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://src/index.css\"],\"names\":[],\"mappings\":\"AAAA;EACE,SAAS;EACT;;cAEY;EACZ,mCAAmC;EACnC,kCAAkC;AACpC;;AAEA;EACE;aACW;AACb\",\"sourcesContent\":[\"body {\\n margin: 0;\\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\\n sans-serif;\\n -webkit-font-smoothing: antialiased;\\n -moz-osx-font-smoothing: grayscale;\\n}\\n\\ncode {\\n font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\\n monospace;\\n}\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n",
+ 'import React from "react";\nimport {\n BrowserRouter as Router,\n Switch,\n Route,\n Link\n} from "react-router-dom";\n\n// This site has 3 pages, all of which are rendered\n// dynamically in the browser (not server rendered).\n//\n// Although the page does not ever refresh, notice how\n// React Router keeps the URL up to date as you navigate\n// through the site. This preserves the browser history,\n// making sure things like the back button and bookmarks\n// work properly.\n\nexport default function App() {\n return (\n \n \n
\n - \n Home\n
\n - \n About\n
\n - \n Dashboard\n
\n - \n Error\n
\n
\n\n
\n\n {/*\n A
looks through all its children \n elements and renders the first one whose path\n matches the current URL. Use a any time\n you have multiple routes, but you want only one\n of them to render at a time\n */}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n );\n}\n\n// You can think of these components as "pages"\n// in your app.\n\nfunction Home() {\n return (\n \n
HOME
\n \n );\n}\n\nfunction About() {\n return (\n \n
about
\n \n );\n}\n\nfunction Dashboard() {\n return (\n \n
Dashboard
\n \n );\n}\n\nfunction ErrorPage() {\n throw new Error(\'Boomm\')\n return (\n \n
error
\n \n );\n}\n',
+ "var api = require(\"!../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js\");\n var content = require(\"!!../node_modules/css-loader/dist/cjs.js??ref--5-oneOf-4-1!../node_modules/postcss-loader/src/index.js??postcss!./index.css\");\n\n content = content.__esModule ? content.default : content;\n\n if (typeof content === 'string') {\n content = [[module.id, content, '']];\n }\n\nvar options = {};\n\noptions.insert = \"head\";\noptions.singleton = false;\n\nvar update = api(content, options);\n\n\nif (module.hot) {\n if (!content.locals || module.hot.invalidate) {\n var isEqualLocals = function isEqualLocals(a, b, isNamedExport) {\n if (!a && b || a && !b) {\n return false;\n }\n\n var p;\n\n for (p in a) {\n if (isNamedExport && p === 'default') {\n // eslint-disable-next-line no-continue\n continue;\n }\n\n if (a[p] !== b[p]) {\n return false;\n }\n }\n\n for (p in b) {\n if (isNamedExport && p === 'default') {\n // eslint-disable-next-line no-continue\n continue;\n }\n\n if (!a[p]) {\n return false;\n }\n }\n\n return true;\n};\n var oldLocals = content.locals;\n\n module.hot.accept(\n \"!!../node_modules/css-loader/dist/cjs.js??ref--5-oneOf-4-1!../node_modules/postcss-loader/src/index.js??postcss!./index.css\",\n function () {\n content = require(\"!!../node_modules/css-loader/dist/cjs.js??ref--5-oneOf-4-1!../node_modules/postcss-loader/src/index.js??postcss!./index.css\");\n\n content = content.__esModule ? content.default : content;\n\n if (typeof content === 'string') {\n content = [[module.id, content, '']];\n }\n\n if (!isEqualLocals(oldLocals, content.locals)) {\n module.hot.invalidate();\n\n return;\n }\n\n oldLocals = content.locals;\n\n update(content);\n }\n )\n }\n\n module.hot.dispose(function() {\n update();\n });\n}\n\nmodule.exports = content.locals || {};",
+ "/*eslint-disable import/first */\nimport { init as initApm } from '@elastic/apm-rum'\ninitApm({\n serviceName: 'fleet-source-map-client',\n serverUrl: 'http://localhost:8200',\n // serverUrl: 'https://776d64ec093b47ff86c752f62baa8f51.apm.us-west1.gcp.cloud.es.io:443',\n serviceVersion: '1.0.0',\n environment: 'production'\n})\nimport React from 'react';\nimport ReactDOM from 'react-dom';\nimport './index.css';\nimport App from './App';\nimport reportWebVitals from './reportWebVitals';\n\nReactDOM.render(\n \n \n ,\n document.getElementById('root')\n);\n\n// If you want to start measuring performance in your app, pass a function\n// to log results (for example: reportWebVitals(console.log))\n// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals\nreportWebVitals();\n",
+ "const reportWebVitals = onPerfEntry => {\n if (onPerfEntry && onPerfEntry instanceof Function) {\n import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {\n getCLS(onPerfEntry);\n getFID(onPerfEntry);\n getFCP(onPerfEntry);\n getLCP(onPerfEntry);\n getTTFB(onPerfEntry);\n });\n }\n};\n\nexport default reportWebVitals;\n",
+ ],
+ mappings:
+ ';;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;;ACNA;AACA;AAQA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAGA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAGA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAGA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAVA;AAAA;AAAA;AAAA;AAAA;AAeA;AAAA;AAAA;AAAA;AASA;AACA;AAAA;AAAA;AACA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAGA;AAAA;AACA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAGA;AAAA;AACA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAGA;AAAA;AACA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAVA;AAAA;AAAA;AAAA;AAAA;AAzBA;AAAA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AA2CA;AAGA;AACA;AAjDA;AACA;AAiDA;AACA;AACA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAIA;AACA;AAPA;AACA;AAOA;AACA;AACA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAIA;AACA;AAPA;AACA;AAOA;AACA;AACA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAIA;AACA;AAPA;AACA;AAOA;AACA;AACA;AACA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAIA;AACA;AARA;AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC5FA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACjFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AALA;AAOA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AAAA;AAAA;AAAA;AADA;AAAA;AAAA;AAAA;AAAA;AAOA;AACA;AACA;AAAA;AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC1BA;AACA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;A',
+ sourceRoot: '',
+};
+
+describe('stringFromBufferRt', () => {
+ describe('decode', () => {
+ it('converts from buffer to string', () => {
+ const sourceMapBuffer = Buffer.from(JSON.stringify(sourceMap));
+ const decoded = stringFromBufferRt.decode(sourceMapBuffer);
+ if (isRight(decoded)) {
+ expect(decoded.right).toEqual(JSON.stringify(sourceMap));
+ } else {
+ expect(true).toBeFalsy();
+ }
+ });
+ });
+ describe('encode', () => {
+ it('converts from string to buffer', () => {
+ const encoded = stringFromBufferRt.encode(JSON.stringify(sourceMap));
+ expect(encoded).toEqual(Buffer.from(JSON.stringify(sourceMap)));
+ });
+ });
+});
diff --git a/x-pack/plugins/apm/server/utils/string_from_buffer_rt.ts b/x-pack/plugins/apm/server/utils/string_from_buffer_rt.ts
new file mode 100644
index 000000000000..3e9304361c86
--- /dev/null
+++ b/x-pack/plugins/apm/server/utils/string_from_buffer_rt.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import * as t from 'io-ts';
+
+export const stringFromBufferRt = new t.Type(
+ 'stringFromBufferRt',
+ t.string.is,
+ (input, context) => {
+ return Buffer.isBuffer(input)
+ ? t.success(input.toString('utf-8'))
+ : t.failure(input, context, 'Input is not a Buffer');
+ },
+ (str) => {
+ return Buffer.from(str);
+ }
+);
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__fixtures__/test_styles.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__fixtures__/test_styles.js
index fa831cacbcb1..4f621bdd94b3 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__fixtures__/test_styles.js
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__fixtures__/test_styles.js
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { elasticLogo } from '../../../../../../../src/plugins/presentation_util/common/lib';
+import { getElasticLogo } from '../../../../../../../src/plugins/presentation_util/common/lib';
export const fontStyle = {
type: 'style',
@@ -23,16 +23,19 @@ export const fontStyle = {
'font-family:Chalkboard, serif;font-weight:bolder;font-style:normal;text-decoration:underline;color:pink;text-align:center;font-size:14px;line-height:21px',
};
-export const containerStyle = {
- type: 'containerStyle',
- border: '3px dotted blue',
- borderRadius: '5px',
- padding: '10px',
- backgroundColor: 'red',
- backgroundImage: `url(${elasticLogo})`,
- opacity: 0.5,
- backgroundSize: 'contain',
- backgroundRepeat: 'no-repeat',
+export const getContainerStyle = async () => {
+ const { elasticLogo } = await getElasticLogo();
+ return {
+ type: 'containerStyle',
+ border: '3px dotted blue',
+ borderRadius: '5px',
+ padding: '10px',
+ backgroundColor: 'red',
+ backgroundImage: `url(${elasticLogo})`,
+ opacity: 0.5,
+ backgroundSize: 'contain',
+ backgroundRepeat: 'no-repeat',
+ };
};
export const defaultStyle = {
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.test.js
index 85e062f454bc..d2a7cd5565ec 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.test.js
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.test.js
@@ -14,6 +14,7 @@ const errors = getFunctionErrors().alterColumn;
describe('alterColumn', () => {
const fn = functionWrapper(alterColumn);
+
const nameColumnIndex = testTable.columns.findIndex(({ name }) => name === 'name');
const timeColumnIndex = testTable.columns.findIndex(({ name }) => name === 'time');
const priceColumnIndex = testTable.columns.findIndex(({ name }) => name === 'price');
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.test.js
index d5621943bcca..5bdc013eff59 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.test.js
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/case.test.js
@@ -11,8 +11,8 @@ import { functionWrapper } from '../../../../../../src/plugins/presentation_util
import { caseFn } from './case';
describe('case', () => {
- const fn = functionWrapper(caseFn);
let testScheduler;
+ const fn = functionWrapper(caseFn);
beforeEach(() => {
testScheduler = new TestScheduler((actual, expected) => expect(actual).toStrictEqual(expected));
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/container_style.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/container_style.test.js
index 7a3599f47ec8..15c7ccdbf5ce 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/container_style.test.js
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/container_style.test.js
@@ -6,8 +6,8 @@
*/
import {
- elasticLogo,
functionWrapper,
+ getElasticLogo,
} from '../../../../../../src/plugins/presentation_util/common/lib';
import { getFunctionErrors } from '../../../i18n';
import { containerStyle } from './containerStyle';
@@ -17,14 +17,21 @@ const errors = getFunctionErrors().containerStyle;
describe('containerStyle', () => {
const fn = functionWrapper(containerStyle);
- describe('default output', () => {
- const result = fn(null);
+ let elasticLogo;
+ beforeEach(async () => {
+ elasticLogo = (await getElasticLogo()).elasticLogo;
+ });
+ describe('default output', () => {
it('returns a containerStyle', () => {
+ const result = fn(null);
+
expect(result).toHaveProperty('type', 'containerStyle');
});
it('all style properties except `overflow` are omitted if args not provided', () => {
+ const result = fn(null);
+
expect(Object.keys(result)).toHaveLength(2);
expect(result).toHaveProperty('type');
expect(result).toHaveProperty('overflow');
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts
index cfef618bee39..6feb22b2ef15 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts
@@ -15,6 +15,7 @@ const errors = getFunctionErrors().csv;
describe('csv', () => {
const fn = functionWrapper(csv);
+
const expected: Datatable = {
type: 'datatable',
columns: [
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.ts
index 254efd9f5f0d..6f785f1b9d47 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.ts
@@ -95,11 +95,5 @@ describe('dropdownControl', () => {
?.value
).toHaveProperty('column', 'price');
});
-
- it('sets column to undefined if no args are provided', () => {
- expect(
- fn(testTable, {}, {} as ExecutionContext)?.value
- ).toHaveProperty('column', undefined);
- });
});
});
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.test.js
index edc2c1db18f6..f81e3ae24130 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.test.js
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/filterrows.test.js
@@ -15,8 +15,8 @@ const inStock = (datatable) => of(datatable.rows[0].in_stock);
const returnFalse = () => of(false);
describe('filterrows', () => {
- const fn = functionWrapper(filterrows);
let testScheduler;
+ const fn = functionWrapper(filterrows);
beforeEach(() => {
testScheduler = new TestScheduler((actual, expected) => expect(actual).toStrictEqual(expected));
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.test.js
index df576a6a2507..fbfcdef07611 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.test.js
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/if.test.js
@@ -12,6 +12,7 @@ import { ifFn } from './if';
describe('if', () => {
const fn = functionWrapper(ifFn);
+
let testScheduler;
beforeEach(() => {
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.test.js
index 45b26cd25937..862560e5643d 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.test.js
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.test.js
@@ -7,61 +7,67 @@
import expect from '@kbn/expect';
import {
- elasticLogo,
- elasticOutline,
+ getElasticLogo,
+ getElasticOutline,
+ functionWrapper,
} from '../../../../../../src/plugins/presentation_util/common/lib';
-// import { image } from './image';
+import { image } from './image';
// TODO: the test was not running and is not up to date
-describe.skip('image', () => {
- const fn = jest.fn();
+describe('image', () => {
+ const fn = functionWrapper(image);
- it('returns an image object using a dataUrl', () => {
- const result = fn(null, { dataurl: elasticOutline, mode: 'cover' });
+ let elasticLogo;
+ let elasticOutline;
+ beforeEach(async () => {
+ elasticLogo = (await getElasticLogo()).elasticLogo;
+ elasticOutline = (await getElasticOutline()).elasticOutline;
+ });
+
+ it('returns an image object using a dataUrl', async () => {
+ const result = await fn(null, { dataurl: elasticOutline, mode: 'cover' });
expect(result).to.have.property('type', 'image');
});
describe('args', () => {
describe('dataurl', () => {
- it('sets the source of the image using dataurl', () => {
- const result = fn(null, { dataurl: elasticOutline });
+ it('sets the source of the image using dataurl', async () => {
+ const result = await fn(null, { dataurl: elasticOutline });
expect(result).to.have.property('dataurl', elasticOutline);
});
- it.skip('sets the source of the image using url', () => {
+ it.skip('sets the source of the image using url', async () => {
// This is skipped because functionWrapper doesn't use the actual
// interpreter and doesn't resolve aliases
- const result = fn(null, { url: elasticOutline });
+ const result = await fn(null, { url: elasticOutline });
expect(result).to.have.property('dataurl', elasticOutline);
});
- it('defaults to the elasticLogo if not provided', () => {
- const result = fn(null);
+ it('defaults to the elasticLogo if not provided', async () => {
+ const result = await fn(null);
expect(result).to.have.property('dataurl', elasticLogo);
});
});
- describe('mode', () => {
- it('sets the mode', () => {
- it('to contain', () => {
- const result = fn(null, { mode: 'contain' });
- expect(result).to.have.property('mode', 'contain');
- });
+ describe('sets the mode', () => {
+ it('to contain', async () => {
+ const result = await fn(null, { mode: 'contain' });
+ expect(result).to.have.property('mode', 'contain');
+ });
- it('to cover', () => {
- const result = fn(null, { mode: 'cover' });
- expect(result).to.have.property('mode', 'cover');
- });
+ it('to cover', async () => {
+ const result = await fn(null, { mode: 'cover' });
+ expect(result).to.have.property('mode', 'cover');
+ });
- it('to stretch', () => {
- const result = fn(null, { mode: 'stretch' });
- expect(result).to.have.property('mode', 'stretch');
- });
+ it('to stretch', async () => {
+ const result = await fn(null, { mode: 'stretch' });
+ expect(result).to.have.property('mode', '100% 100%');
+ });
- it("defaults to 'contain' if not provided", () => {
- const result = fn(null);
- expect(result).to.have.property('mode', 'contain');
- });
+ it("defaults to 'contain' if not provided", async () => {
+ const result = await fn(null);
+ expect(result).to.have.property('mode', 'contain');
});
});
});
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.ts
index c3e64e48b23f..e661a15cea3a 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.ts
@@ -7,8 +7,9 @@
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import { getFunctionHelp, getFunctionErrors } from '../../../i18n';
+
import {
- elasticLogo,
+ getElasticLogo,
resolveWithMissingImage,
} from '../../../../../../src/plugins/presentation_util/common/lib';
@@ -29,10 +30,9 @@ export interface Return {
dataurl: string;
}
-export function image(): ExpressionFunctionDefinition<'image', null, Arguments, Return> {
+export function image(): ExpressionFunctionDefinition<'image', null, Arguments, Promise> {
const { help, args: argHelp } = getFunctionHelp().image;
const errors = getFunctionErrors().image;
-
return {
name: 'image',
aliases: [],
@@ -45,7 +45,7 @@ export function image(): ExpressionFunctionDefinition<'image', null, Arguments,
types: ['string', 'null'],
help: argHelp.dataurl,
aliases: ['_', 'url'],
- default: elasticLogo,
+ default: null,
},
mode: {
types: ['string'],
@@ -54,13 +54,17 @@ export function image(): ExpressionFunctionDefinition<'image', null, Arguments,
options: Object.values(ImageMode),
},
},
- fn: (input, { dataurl, mode }) => {
+ fn: async (input, { dataurl, mode }) => {
if (!mode || !Object.values(ImageMode).includes(mode)) {
throw errors.invalidImageMode();
}
+ const { elasticLogo } = await getElasticLogo();
- const modeStyle = mode === 'stretch' ? '100% 100%' : mode;
+ if (dataurl === null) {
+ dataurl = elasticLogo;
+ }
+ const modeStyle = mode === 'stretch' ? '100% 100%' : mode;
return {
type: 'image',
mode: modeStyle,
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/progress.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/progress.test.js
index 6438e2a4d19c..5f44428abf03 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/progress.test.js
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/progress.test.js
@@ -16,6 +16,7 @@ const errors = getFunctionErrors().progress;
// TODO: this test was not running and is not up to date
describe.skip('progress', () => {
const fn = functionWrapper(progress);
+
const value = 0.33;
it('returns a render as progress', () => {
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/render.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/render.test.js
index 3248af550409..1725af3522ba 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/render.test.js
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/render.test.js
@@ -8,7 +8,7 @@
import { functionWrapper } from '../../../../../../src/plugins/presentation_util/common/lib';
import { DEFAULT_ELEMENT_CSS } from '../../../common/lib/constants';
import { testTable } from './__fixtures__/test_tables';
-import { fontStyle, containerStyle } from './__fixtures__/test_styles';
+import { fontStyle, getContainerStyle } from './__fixtures__/test_styles';
import { render } from './render';
const renderTable = {
@@ -25,7 +25,12 @@ const renderTable = {
describe('render', () => {
const fn = functionWrapper(render);
- it('returns a render', () => {
+ let containerStyle;
+ beforeEach(async () => {
+ containerStyle = await getContainerStyle();
+ });
+
+ it('returns a render', async () => {
const result = fn(renderTable, {
as: 'debug',
css: '".canvasRenderEl { background-color: red; }"',
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeat_image.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeat_image.test.js
index 97f0552721cc..42569e26e426 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeat_image.test.js
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeat_image.test.js
@@ -6,8 +6,8 @@
*/
import {
- elasticLogo,
- elasticOutline,
+ getElasticLogo,
+ getElasticOutline,
functionWrapper,
} from '../../../../../../src/plugins/presentation_util/common/lib';
import { repeatImage } from './repeat_image';
@@ -15,55 +15,62 @@ import { repeatImage } from './repeat_image';
describe('repeatImage', () => {
const fn = functionWrapper(repeatImage);
- it('returns a render as repeatImage', () => {
- const result = fn(10);
+ let elasticLogo;
+ let elasticOutline;
+ beforeEach(async () => {
+ elasticLogo = await (await getElasticLogo()).elasticLogo;
+ elasticOutline = await (await getElasticOutline()).elasticOutline;
+ });
+
+ it('returns a render as repeatImage', async () => {
+ const result = await fn(10);
expect(result).toHaveProperty('type', 'render');
expect(result).toHaveProperty('as', 'repeatImage');
});
describe('args', () => {
describe('image', () => {
- it('sets the source of the repeated image', () => {
- const result = fn(10, { image: elasticLogo }).value;
+ it('sets the source of the repeated image', async () => {
+ const result = (await fn(10, { image: elasticLogo })).value;
expect(result).toHaveProperty('image', elasticLogo);
});
- it('defaults to the Elastic outline logo', () => {
- const result = fn(100000).value;
+ it('defaults to the Elastic outline logo', async () => {
+ const result = (await fn(100000)).value;
expect(result).toHaveProperty('image', elasticOutline);
});
});
describe('size', () => {
- it('sets the size of the image', () => {
- const result = fn(-5, { size: 200 }).value;
+ it('sets the size of the image', async () => {
+ const result = (await fn(-5, { size: 200 })).value;
expect(result).toHaveProperty('size', 200);
});
- it('defaults to 100', () => {
- const result = fn(-5).value;
+ it('defaults to 100', async () => {
+ const result = (await fn(-5)).value;
expect(result).toHaveProperty('size', 100);
});
});
describe('max', () => {
- it('sets the maximum number of a times the image is repeated', () => {
- const result = fn(100000, { max: 20 }).value;
+ it('sets the maximum number of a times the image is repeated', async () => {
+ const result = (await fn(100000, { max: 20 })).value;
expect(result).toHaveProperty('max', 20);
});
- it('defaults to 1000', () => {
- const result = fn(100000).value;
+ it('defaults to 1000', async () => {
+ const result = (await fn(100000)).value;
expect(result).toHaveProperty('max', 1000);
});
});
describe('emptyImage', () => {
- it('returns repeatImage object with emptyImage as undefined', () => {
- const result = fn(100000, { emptyImage: elasticLogo }).value;
+ it('returns repeatImage object with emptyImage as undefined', async () => {
+ const result = (await fn(100000, { emptyImage: elasticLogo })).value;
expect(result).toHaveProperty('emptyImage', elasticLogo);
});
- it('sets emptyImage to null', () => {
- const result = fn(100000).value;
+ it('sets emptyImage to null', async () => {
+ const result = (await fn(100000)).value;
expect(result).toHaveProperty('emptyImage', null);
});
});
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeat_image.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeat_image.ts
index 904b2478760a..751573e27183 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeat_image.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeat_image.ts
@@ -7,7 +7,7 @@
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import {
- elasticOutline,
+ getElasticOutline,
resolveWithMissingImage,
} from '../../../../../../src/plugins/presentation_util/common/lib';
import { Render } from '../../../types';
@@ -32,10 +32,9 @@ export function repeatImage(): ExpressionFunctionDefinition<
'repeatImage',
number,
Arguments,
- Render
+ Promise>
> {
const { help, args: argHelp } = getFunctionHelp().repeatImage;
-
return {
name: 'repeatImage',
aliases: [],
@@ -51,7 +50,7 @@ export function repeatImage(): ExpressionFunctionDefinition<
image: {
types: ['string', 'null'],
help: argHelp.image,
- default: elasticOutline,
+ default: null,
},
max: {
types: ['number'],
@@ -64,7 +63,12 @@ export function repeatImage(): ExpressionFunctionDefinition<
help: argHelp.size,
},
},
- fn: (count, args) => {
+ fn: async (count, args) => {
+ const { elasticOutline } = await getElasticOutline();
+ if (args.image === null) {
+ args.image = elasticOutline;
+ }
+
return {
type: 'render',
as: 'repeatImage',
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/rounddate.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/rounddate.test.js
index 0ef832d97327..987096cc0d3d 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/rounddate.test.js
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/rounddate.test.js
@@ -10,6 +10,7 @@ import { rounddate } from './rounddate';
describe('rounddate', () => {
const fn = functionWrapper(rounddate);
+
const date = new Date('2011-10-31T00:00:00.000Z').valueOf();
it('returns date in ms from date in ms or ISO8601 string', () => {
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js
index c6f592889c99..ffa1557d2b54 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js
@@ -12,6 +12,7 @@ import { switchFn } from './switch';
describe('switch', () => {
const fn = functionWrapper(switchFn);
+
const getter = (value) => () => of(value);
const mockCases = [
{
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/tail.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/tail.test.js
index 420489754d20..75744f91ccb5 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/tail.test.js
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/tail.test.js
@@ -11,6 +11,7 @@ import { tail } from './tail';
describe('tail', () => {
const fn = functionWrapper(tail);
+
const lastIndex = testTable.rows.length - 1;
it('returns a datatable with the last N rows of the context', () => {
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter.test.js
index f45ec981b1a8..38b0d1bff606 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter.test.js
+++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter.test.js
@@ -15,7 +15,7 @@ const errors = getFunctionErrors().timefilter;
let clock = null;
-beforeEach(function () {
+beforeEach(async function () {
clock = sinon.useFakeTimers();
});
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/repeat_image.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/repeat_image.stories.storyshot
index 9b97ae1fdacb..5e6b8214c3c4 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/repeat_image.stories.storyshot
+++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/repeat_image.stories.storyshot
@@ -1,5 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`Storyshots enderers/repeatImage default 1`] = `
+
+
+
+`;
+
exports[`Storyshots renderers/repeatImage default 1`] = `
{
+const Renderer = ({ elasticLogo }: { elasticLogo: string }) => {
const config = {
type: 'image' as 'image',
mode: 'cover',
@@ -19,4 +20,10 @@ storiesOf('renderers/image', module).add('default', () => {
};
return
;
-});
+};
+
+storiesOf('renderers/image', module).add(
+ 'default',
+ (_, props) =>
,
+ { decorators: [waitFor(getElasticLogo())] }
+);
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/repeat_image.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/repeat_image.stories.tsx
index ed2706389d83..0052b9139aae 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/repeat_image.stories.tsx
+++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/repeat_image.stories.tsx
@@ -8,13 +8,20 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { repeatImage } from '../repeat_image';
-import { Render } from './render';
import {
- elasticLogo,
- elasticOutline,
+ getElasticLogo,
+ getElasticOutline,
} from '../../../../../../src/plugins/presentation_util/common/lib';
+import { waitFor } from '../../../../../../src/plugins/presentation_util/public/__stories__';
+import { Render } from './render';
-storiesOf('renderers/repeatImage', module).add('default', () => {
+const Renderer = ({
+ elasticLogo,
+ elasticOutline,
+}: {
+ elasticLogo: string;
+ elasticOutline: string;
+}) => {
const config = {
count: 42,
image: elasticLogo,
@@ -24,4 +31,12 @@ storiesOf('renderers/repeatImage', module).add('default', () => {
};
return
;
-});
+};
+
+storiesOf('enderers/repeatImage', module).add(
+ 'default',
+ (_, props) => (
+
+ ),
+ { decorators: [waitFor(getElasticLogo()), waitFor(getElasticOutline())] }
+);
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/image.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/image.tsx
index 86e9daed105d..78e3ecb7a4c9 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/image.tsx
+++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/image.tsx
@@ -7,7 +7,10 @@
import ReactDOM from 'react-dom';
import React from 'react';
-import { elasticLogo, isValidUrl } from '../../../../../src/plugins/presentation_util/common/lib';
+import {
+ getElasticLogo,
+ isValidUrl,
+} from '../../../../../src/plugins/presentation_util/common/lib';
import { Return as Arguments } from '../functions/common/image';
import { RendererStrings } from '../../i18n';
import { RendererFactory } from '../../types';
@@ -19,7 +22,8 @@ export const image: RendererFactory
= () => ({
displayName: strings.getDisplayName(),
help: strings.getHelpDescription(),
reuseDomNode: true,
- render(domNode, config, handlers) {
+ render: async (domNode, config, handlers) => {
+ const { elasticLogo } = await getElasticLogo();
const dataurl = isValidUrl(config.dataurl) ? config.dataurl : elasticLogo;
const style = {
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/repeat_image.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/repeat_image.ts
index 149a88768341..b7a94c2089d8 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/repeat_image.ts
+++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/repeat_image.ts
@@ -8,7 +8,7 @@
import $ from 'jquery';
import { times } from 'lodash';
import {
- elasticOutline,
+ getElasticOutline,
isValidUrl,
} from '../../../../../src/plugins/presentation_util/common/lib';
import { RendererStrings, ErrorStrings } from '../../i18n';
@@ -23,10 +23,14 @@ export const repeatImage: RendererFactory = () => ({
displayName: strings.getDisplayName(),
help: strings.getHelpDescription(),
reuseDomNode: true,
- render(domNode, config, handlers) {
+ render: async (domNode, config, handlers) => {
+ let image = config.image;
+ if (!isValidUrl(config.image)) {
+ image = (await getElasticOutline()).elasticOutline;
+ }
const settings = {
...config,
- image: isValidUrl(config.image) ? config.image : elasticOutline,
+ image,
emptyImage: config.emptyImage || '',
};
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js
index 480d8ea364c4..4597826c031a 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js
+++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js
@@ -12,7 +12,7 @@ import { get } from 'lodash';
import { AssetPicker } from '../../../../public/components/asset_picker';
import {
encode,
- elasticOutline,
+ getElasticOutline,
isValidHttpUrl,
resolveFromArgs,
} from '../../../../../../../src/plugins/presentation_util/public';
@@ -168,13 +168,16 @@ class ImageUpload extends React.Component {
}
}
-export const imageUpload = () => ({
- name: 'imageUpload',
- displayName: strings.getDisplayName(),
- help: strings.getHelp(),
- resolveArgValue: true,
- template: templateFromReactComponent(ImageUpload),
- resolve({ args }) {
- return { dataurl: resolveFromArgs(args, elasticOutline) };
- },
-});
+export const imageUpload = () => {
+ return {
+ name: 'imageUpload',
+ displayName: strings.getDisplayName(),
+ help: strings.getHelp(),
+ resolveArgValue: true,
+ template: templateFromReactComponent(ImageUpload),
+ resolve: async ({ args }) => {
+ const { elasticOutline } = await getElasticOutline();
+ return { dataurl: resolveFromArgs(args, elasticOutline) };
+ },
+ };
+};
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/image.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/image.js
index f974667b7fad..c3855ad31e3f 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/image.js
+++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/image.js
@@ -6,38 +6,41 @@
*/
import {
- elasticLogo,
+ getElasticLogo,
resolveFromArgs,
} from '../../../../../../src/plugins/presentation_util/common/lib';
import { ViewStrings } from '../../../i18n';
const { Image: strings } = ViewStrings;
-export const image = () => ({
- name: 'image',
- displayName: strings.getDisplayName(),
- modelArgs: [],
- requiresContext: false,
- args: [
- {
- name: 'dataurl',
- argType: 'imageUpload',
- resolve({ args }) {
- return { dataurl: resolveFromArgs(args, elasticLogo) };
+export const image = () => {
+ return {
+ name: 'image',
+ displayName: strings.getDisplayName(),
+ modelArgs: [],
+ requiresContext: false,
+ args: [
+ {
+ name: 'dataurl',
+ argType: 'imageUpload',
+ resolve: async ({ args }) => {
+ const { elasticLogo } = await getElasticLogo();
+ return { dataurl: resolveFromArgs(args, elasticLogo) };
+ },
},
- },
- {
- name: 'mode',
- displayName: strings.getModeDisplayName(),
- help: strings.getModeHelp(),
- argType: 'select',
- options: {
- choices: [
- { value: 'contain', name: strings.getContainMode() },
- { value: 'cover', name: strings.getCoverMode() },
- { value: 'stretch', name: strings.getStretchMode() },
- ],
+ {
+ name: 'mode',
+ displayName: strings.getModeDisplayName(),
+ help: strings.getModeHelp(),
+ argType: 'select',
+ options: {
+ choices: [
+ { value: 'contain', name: strings.getContainMode() },
+ { value: 'cover', name: strings.getCoverMode() },
+ { value: 'stretch', name: strings.getStretchMode() },
+ ],
+ },
},
- },
- ],
-});
+ ],
+ };
+};
diff --git a/x-pack/plugins/canvas/i18n/constants.ts b/x-pack/plugins/canvas/i18n/constants.ts
index 74646e140a20..2f192fd3e2d5 100644
--- a/x-pack/plugins/canvas/i18n/constants.ts
+++ b/x-pack/plugins/canvas/i18n/constants.ts
@@ -51,3 +51,6 @@ export const TYPE_STRING = '`string`';
export const URL = 'URL';
export const UTC = 'UTC';
export const ZIP = 'ZIP';
+export const IMAGE_MODE_CONTAIN = 'contain';
+export const IMAGE_MODE_COVER = 'cover';
+export const IMAGE_MODE_STRETCH = 'stretch';
diff --git a/x-pack/plugins/canvas/i18n/functions/dict/image.ts b/x-pack/plugins/canvas/i18n/functions/dict/image.ts
index 5e643cf8de6e..b619d550f9ef 100644
--- a/x-pack/plugins/canvas/i18n/functions/dict/image.ts
+++ b/x-pack/plugins/canvas/i18n/functions/dict/image.ts
@@ -6,10 +6,16 @@
*/
import { i18n } from '@kbn/i18n';
-import { image, ImageMode } from '../../../canvas_plugin_src/functions/common/image';
+import { image } from '../../../canvas_plugin_src/functions/common/image';
import { FunctionHelp } from '../function_help';
import { FunctionFactory } from '../../../types';
-import { URL, BASE64 } from '../../constants';
+import {
+ URL,
+ BASE64,
+ IMAGE_MODE_CONTAIN,
+ IMAGE_MODE_COVER,
+ IMAGE_MODE_STRETCH,
+} from '../../constants';
export const help: FunctionHelp> = {
help: i18n.translate('xpack.canvas.functions.imageHelpText', {
@@ -35,9 +41,9 @@ export const help: FunctionHelp> = {
'{cover} fills the container with the image, cropping from the sides or bottom as needed. ' +
'{stretch} resizes the height and width of the image to 100% of the container.',
values: {
- contain: `\`"${ImageMode.CONTAIN}"\``,
- cover: `\`"${ImageMode.COVER}"\``,
- stretch: `\`"${ImageMode.STRETCH}"\``,
+ contain: `\`"${IMAGE_MODE_CONTAIN}"\``,
+ cover: `\`"${IMAGE_MODE_COVER}"\``,
+ stretch: `\`"${IMAGE_MODE_STRETCH}"\``,
},
}),
},
@@ -49,9 +55,9 @@ export const errors = {
i18n.translate('xpack.canvas.functions.image.invalidImageModeErrorMessage', {
defaultMessage: '"mode" must be "{contain}", "{cover}", or "{stretch}"',
values: {
- contain: ImageMode.CONTAIN,
- cover: ImageMode.COVER,
- stretch: ImageMode.STRETCH,
+ contain: IMAGE_MODE_CONTAIN,
+ cover: IMAGE_MODE_COVER,
+ stretch: IMAGE_MODE_STRETCH,
},
})
),
diff --git a/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/custom_element_modal.stories.tsx b/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/custom_element_modal.stories.tsx
index 93574270757f..a072579be2e8 100644
--- a/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/custom_element_modal.stories.tsx
+++ b/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/custom_element_modal.stories.tsx
@@ -9,7 +9,8 @@ import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { CustomElementModal } from '../custom_element_modal';
-import { elasticLogo } from '../../../../../../../src/plugins/presentation_util/public';
+import { waitFor } from '../../../../../../../src/plugins/presentation_util/public/__stories__';
+import { getElasticLogo } from '../../../../../../../src/plugins/presentation_util/public';
storiesOf('components/Elements/CustomElementModal', module)
.add('with title', () => (
@@ -36,11 +37,15 @@ storiesOf('components/Elements/CustomElementModal', module)
onSave={action('onSave')}
/>
))
- .add('with image', () => (
-
- ));
+ .add(
+ 'with image',
+ (_, props) => (
+
+ ),
+ { decorators: [waitFor(getElasticLogo())] }
+ );
diff --git a/x-pack/plugins/canvas/public/components/element_card/__stories__/element_card.stories.tsx b/x-pack/plugins/canvas/public/components/element_card/__stories__/element_card.stories.tsx
index 4c68f185b196..74cd86c8d17d 100644
--- a/x-pack/plugins/canvas/public/components/element_card/__stories__/element_card.stories.tsx
+++ b/x-pack/plugins/canvas/public/components/element_card/__stories__/element_card.stories.tsx
@@ -9,7 +9,8 @@ import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { ElementCard } from '../element_card';
-import { elasticLogo } from '../../../../../../../src/plugins/presentation_util/public';
+import { waitFor } from '../../../../../../../src/plugins/presentation_util/public/__stories__';
+import { getElasticLogo } from '../../../../../../../src/plugins/presentation_util/public';
storiesOf('components/Elements/ElementCard', module)
.addDecorator((story) => (
@@ -27,13 +28,17 @@ storiesOf('components/Elements/ElementCard', module)
description="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce lobortis aliquet arcu ut turpis duis."
/>
))
- .add('with image', () => (
-
- ))
+ .add(
+ 'with image',
+ (_, props) => (
+
+ ),
+ { decorators: [waitFor(getElasticLogo())] }
+ )
.add('with tags', () => (
= ({ functionRegistry }) => {
const functionDefinitions = Object.values(functionRegistry);
const copyDocs = () => {
- copy(generateFunctionReference(functionDefinitions));
+ const functionRefs = generateFunctionReference(functionDefinitions);
+ copy(functionRefs);
notifyService.success(
`Please paste updated docs into '/kibana/docs/canvas/canvas-function-reference.asciidoc' and commit your changes.`,
{ title: 'Copied function docs to clipboard' }
diff --git a/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx b/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx
index 3c68afb2bcec..f267b48028f7 100644
--- a/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx
+++ b/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx
@@ -104,7 +104,7 @@ export const RenderWithFn: FC = ({
try {
render();
firstRender.current = false;
- } catch (err) {
+ } catch (err: any) {
onError(err, { title: strings.getRenderErrorMessage(functionName) });
}
}, [domNode, functionName, onError, render, resetRenderTarget, reuseNode]);
diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/element_grid.stories.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/element_grid.stories.tsx
index 769078da972f..3f0611479c3b 100644
--- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/element_grid.stories.tsx
+++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/element_grid.stories.tsx
@@ -8,8 +8,9 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
+import { waitFor } from '../../../../../../../src/plugins/presentation_util/public/__stories__';
import { ElementGrid } from '../element_grid';
-import { testCustomElements } from './fixtures/test_elements';
+import { getTestCustomElements } from './fixtures/test_elements';
storiesOf('components/SavedElementsModal/ElementGrid', module)
.addDecorator((story) => (
@@ -21,20 +22,28 @@ storiesOf('components/SavedElementsModal/ElementGrid', module)
{story()}
))
- .add('default', () => (
-
- ))
- .add('with text filter', () => (
-
- ));
+ .add(
+ 'default',
+ (_, props) => (
+
+ ),
+ { decorators: [waitFor(getTestCustomElements())] }
+ )
+ .add(
+ 'with text filter',
+ (_, props) => (
+
+ ),
+ { decorators: [waitFor(getTestCustomElements())] }
+ );
diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/fixtures/test_elements.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/fixtures/test_elements.tsx
index ef48b9815062..854bf9c685ad 100644
--- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/fixtures/test_elements.tsx
+++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/fixtures/test_elements.tsx
@@ -5,32 +5,36 @@
* 2.0.
*/
-import { elasticLogo } from '../../../../../../../../src/plugins/presentation_util/public';
+import { getElasticLogo } from '../../../../../../../../src/plugins/presentation_util/public';
-export const testCustomElements = [
- {
- id: 'custom-element-10d625f5-1342-47c9-8f19-d174ea6b65d5',
- name: 'customElement1',
- displayName: 'Custom Element 1',
- help: 'sample description',
- image: elasticLogo,
- content: `{\"selectedNodes\":[{\"id\":\"element-3383b40a-de5d-4efb-8719-f4d8cffbfa74\",\"position\":{\"left\":142,\"top\":146,\"width\":700,\"height\":300,\"angle\":0,\"parent\":null,\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| pointseries x=\\\"project\\\" y=\\\"sum(price)\\\" color=\\\"state\\\" size=\\\"size(username)\\\"\\n| plot defaultStyle={seriesStyle points=5 fill=1}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"pointseries\",\"arguments\":{\"x\":[\"project\"],\"y\":[\"sum(price)\"],\"color\":[\"state\"],\"size\":[\"size(username)\"]}},{\"type\":\"function\",\"function\":\"plot\",\"arguments\":{\"defaultStyle\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"seriesStyle\",\"arguments\":{\"points\":[5],\"fill\":[1]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}}]}`,
- },
- {
- id: 'custom-element-b22d8d10-6116-46fb-9b46-c3f3340d3aaa',
- name: 'customElement2',
- displayName: 'Custom Element 2',
- help: 'Aenean eu justo auctor, placerat felis non, scelerisque dolor. ',
- image: elasticLogo,
- content: `{\"selectedNodes\":[{\"id\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"position\":{\"left\":250,\"top\":119,\"width\":340,\"height\":517,\"angle\":0,\"parent\":null,\"type\":\"group\"},\"expression\":\"shape fill=\\\"rgba(255,255,255,0)\\\" | render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"shape\",\"arguments\":{\"fill\":[\"rgba(255,255,255,0)\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-e2c658ee-7614-4d92-a46e-2b1a81a24485\",\"position\":{\"left\":250,\"top\":405,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\\"## Jane Doe\\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"## Jane Doe\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-3d16765e-5251-4954-8e2a-6c64ed465b73\",\"position\":{\"left\":250,\"top\":480,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\\"### Developer\\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render css=\\\".canvasRenderEl h3 {\\ncolor: #444444;\\n}\\\"\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"### Developer\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{\"css\":[\".canvasRenderEl h3 {\\ncolor: #444444;\\n}\"]}}]}},{\"id\":\"element-624675cf-46e9-4545-b86a-5409bbe53ac1\",\"position\":{\"left\":250,\"top\":555,\"width\":340,\"height\":81,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\n \\\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. \\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. \"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-c2916246-26dd-4c65-91c6-d1ad3f1791ee\",\"position\":{\"left\":293,\"top\":119,\"width\":254,\"height\":252,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"image dataurl={asset \\\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\\\"} mode=\\\"contain\\\"\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"image\",\"arguments\":{\"dataurl\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"asset\",\"arguments\":{\"_\":[\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\"]}}]}],\"mode\":[\"contain\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}}]}`,
- },
- {
- id: 'custom-element-',
- name: 'customElement3',
- displayName: 'Custom Element 3',
- help:
- 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce lobortis aliquet arcu ut turpis duis.',
- image: elasticLogo,
- content: `{\"selectedNodes\":[{\"id\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"position\":{\"left\":250,\"top\":119,\"width\":340,\"height\":517,\"angle\":0,\"parent\":null,\"type\":\"group\"},\"expression\":\"shape fill=\\\"rgba(255,255,255,0)\\\" | render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"shape\",\"arguments\":{\"fill\":[\"rgba(255,255,255,0)\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-e2c658ee-7614-4d92-a46e-2b1a81a24485\",\"position\":{\"left\":250,\"top\":405,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\\"## Jane Doe\\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"## Jane Doe\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-3d16765e-5251-4954-8e2a-6c64ed465b73\",\"position\":{\"left\":250,\"top\":480,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\\"### Developer\\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render css=\\\".canvasRenderEl h3 {\\ncolor: #444444;\\n}\\\"\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"### Developer\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{\"css\":[\".canvasRenderEl h3 {\\ncolor: #444444;\\n}\"]}}]}},{\"id\":\"element-624675cf-46e9-4545-b86a-5409bbe53ac1\",\"position\":{\"left\":250,\"top\":555,\"width\":340,\"height\":81,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\n \\\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. \\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. \"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-c2916246-26dd-4c65-91c6-d1ad3f1791ee\",\"position\":{\"left\":293,\"top\":119,\"width\":254,\"height\":252,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"image dataurl={asset \\\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\\\"} mode=\\\"contain\\\"\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"image\",\"arguments\":{\"dataurl\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"asset\",\"arguments\":{\"_\":[\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\"]}}]}],\"mode\":[\"contain\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}}]}`,
- },
-];
+export const getTestCustomElements = async () => {
+ const { elasticLogo } = await getElasticLogo();
+ const testCustomElements = [
+ {
+ id: 'custom-element-10d625f5-1342-47c9-8f19-d174ea6b65d5',
+ name: 'customElement1',
+ displayName: 'Custom Element 1',
+ help: 'sample description',
+ image: elasticLogo,
+ content: `{\"selectedNodes\":[{\"id\":\"element-3383b40a-de5d-4efb-8719-f4d8cffbfa74\",\"position\":{\"left\":142,\"top\":146,\"width\":700,\"height\":300,\"angle\":0,\"parent\":null,\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| pointseries x=\\\"project\\\" y=\\\"sum(price)\\\" color=\\\"state\\\" size=\\\"size(username)\\\"\\n| plot defaultStyle={seriesStyle points=5 fill=1}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"pointseries\",\"arguments\":{\"x\":[\"project\"],\"y\":[\"sum(price)\"],\"color\":[\"state\"],\"size\":[\"size(username)\"]}},{\"type\":\"function\",\"function\":\"plot\",\"arguments\":{\"defaultStyle\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"seriesStyle\",\"arguments\":{\"points\":[5],\"fill\":[1]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}}]}`,
+ },
+ {
+ id: 'custom-element-b22d8d10-6116-46fb-9b46-c3f3340d3aaa',
+ name: 'customElement2',
+ displayName: 'Custom Element 2',
+ help: 'Aenean eu justo auctor, placerat felis non, scelerisque dolor. ',
+ image: elasticLogo,
+ content: `{\"selectedNodes\":[{\"id\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"position\":{\"left\":250,\"top\":119,\"width\":340,\"height\":517,\"angle\":0,\"parent\":null,\"type\":\"group\"},\"expression\":\"shape fill=\\\"rgba(255,255,255,0)\\\" | render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"shape\",\"arguments\":{\"fill\":[\"rgba(255,255,255,0)\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-e2c658ee-7614-4d92-a46e-2b1a81a24485\",\"position\":{\"left\":250,\"top\":405,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\\"## Jane Doe\\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"## Jane Doe\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-3d16765e-5251-4954-8e2a-6c64ed465b73\",\"position\":{\"left\":250,\"top\":480,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\\"### Developer\\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render css=\\\".canvasRenderEl h3 {\\ncolor: #444444;\\n}\\\"\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"### Developer\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{\"css\":[\".canvasRenderEl h3 {\\ncolor: #444444;\\n}\"]}}]}},{\"id\":\"element-624675cf-46e9-4545-b86a-5409bbe53ac1\",\"position\":{\"left\":250,\"top\":555,\"width\":340,\"height\":81,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\n \\\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. \\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. \"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-c2916246-26dd-4c65-91c6-d1ad3f1791ee\",\"position\":{\"left\":293,\"top\":119,\"width\":254,\"height\":252,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"image dataurl={asset \\\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\\\"} mode=\\\"contain\\\"\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"image\",\"arguments\":{\"dataurl\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"asset\",\"arguments\":{\"_\":[\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\"]}}]}],\"mode\":[\"contain\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}}]}`,
+ },
+ {
+ id: 'custom-element-',
+ name: 'customElement3',
+ displayName: 'Custom Element 3',
+ help:
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce lobortis aliquet arcu ut turpis duis.',
+ image: elasticLogo,
+ content: `{\"selectedNodes\":[{\"id\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"position\":{\"left\":250,\"top\":119,\"width\":340,\"height\":517,\"angle\":0,\"parent\":null,\"type\":\"group\"},\"expression\":\"shape fill=\\\"rgba(255,255,255,0)\\\" | render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"shape\",\"arguments\":{\"fill\":[\"rgba(255,255,255,0)\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-e2c658ee-7614-4d92-a46e-2b1a81a24485\",\"position\":{\"left\":250,\"top\":405,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\\"## Jane Doe\\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"## Jane Doe\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-3d16765e-5251-4954-8e2a-6c64ed465b73\",\"position\":{\"left\":250,\"top\":480,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\\"### Developer\\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render css=\\\".canvasRenderEl h3 {\\ncolor: #444444;\\n}\\\"\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"### Developer\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{\"css\":[\".canvasRenderEl h3 {\\ncolor: #444444;\\n}\"]}}]}},{\"id\":\"element-624675cf-46e9-4545-b86a-5409bbe53ac1\",\"position\":{\"left\":250,\"top\":555,\"width\":340,\"height\":81,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\n \\\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. \\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. \"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-c2916246-26dd-4c65-91c6-d1ad3f1791ee\",\"position\":{\"left\":293,\"top\":119,\"width\":254,\"height\":252,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"image dataurl={asset \\\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\\\"} mode=\\\"contain\\\"\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"image\",\"arguments\":{\"dataurl\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"asset\",\"arguments\":{\"_\":[\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\"]}}]}],\"mode\":[\"contain\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}}]}`,
+ },
+ ];
+ return { testCustomElements };
+};
diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/saved_elements_modal.stories.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/saved_elements_modal.stories.tsx
index d5c9af6ded8d..086a4be14021 100644
--- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/saved_elements_modal.stories.tsx
+++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/saved_elements_modal.stories.tsx
@@ -8,8 +8,9 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
+import { waitFor } from '../../../../../../../src/plugins/presentation_util/public/__stories__';
import { SavedElementsModal } from '../saved_elements_modal.component';
-import { testCustomElements } from './fixtures/test_elements';
+import { getTestCustomElements } from './fixtures/test_elements';
import { CustomElement } from '../../../../types';
storiesOf('components/SavedElementsModal', module)
@@ -25,27 +26,35 @@ storiesOf('components/SavedElementsModal', module)
removeCustomElement={action('removeCustomElement')}
/>
))
- .add('with custom elements', () => (
-
- ))
- .add('with text filter', () => (
-
- ));
+ .add(
+ 'with custom elements',
+ (_, props) => (
+
+ ),
+ { decorators: [waitFor(getTestCustomElements())] }
+ )
+ .add(
+ 'with text filter',
+ (_, props) => (
+
+ ),
+ { decorators: [waitFor(getTestCustomElements())] }
+ );
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/custom_interval.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/custom_interval.tsx
index 284749340e44..a35b83bad3e7 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/custom_interval.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/custom_interval.tsx
@@ -75,12 +75,7 @@ export const CustomInterval = ({ gutterSize, buttonSize, onSubmit, defaultValue
-
+
{strings.getButtonLabel()}
diff --git a/x-pack/plugins/canvas/public/functions/pie.test.js b/x-pack/plugins/canvas/public/functions/pie.test.js
index 5e35cc3bf523..f1ce8786f2c6 100644
--- a/x-pack/plugins/canvas/public/functions/pie.test.js
+++ b/x-pack/plugins/canvas/public/functions/pie.test.js
@@ -30,9 +30,9 @@ describe('pie', () => {
});
describe('data', () => {
- const result = fn(testPie).value.data;
-
it('has one series per unique label', () => {
+ const result = fn(testPie).value.data;
+
const uniqueLabels = testPie.rows.reduce(
(unique, series) =>
!unique.includes(series.color) ? unique.concat([series.color]) : unique,
@@ -44,6 +44,8 @@ describe('pie', () => {
});
it('populates the data of the plot with points from the pointseries', () => {
+ const result = fn(testPie).value.data;
+
expect(result[0].data).toEqual([202]);
expect(result[1].data).toEqual([67]);
expect(result[2].data).toEqual([311]);
diff --git a/x-pack/plugins/canvas/public/functions/plot.test.js b/x-pack/plugins/canvas/public/functions/plot.test.js
index 8dd2470ea17d..a2eef889aa07 100644
--- a/x-pack/plugins/canvas/public/functions/plot.test.js
+++ b/x-pack/plugins/canvas/public/functions/plot.test.js
@@ -33,12 +33,15 @@ describe('plot', () => {
});
describe('data', () => {
- const result = fn(testPlot).value.data;
it('is sorted by the series labels', () => {
+ const result = fn(testPlot).value.data;
+
expect(result.every((val, i) => (!!i ? val.label >= result[i - 1].label : true))).toBe(true);
});
it('has one series per unique label', () => {
+ const result = fn(testPlot).value.data;
+
const uniqueLabels = testPlot.rows
.reduce(
(unique, series) =>
@@ -52,6 +55,8 @@ describe('plot', () => {
});
it('populates the data of the plot with points from the pointseries', () => {
+ const result = fn(testPlot).value.data;
+
expect(result[0].data).toEqual([
[1517842800950, 605, { size: 100, text: 605 }],
[1517929200950, 583, { size: 200, text: 583 }],
@@ -118,6 +123,7 @@ describe('plot', () => {
describe('palette', () => {
it('sets the color palette', () => {
const mockedColors = jest.fn(() => ['#FFFFFF', '#888888', '#000000']);
+
const mockedFn = functionWrapper(
plotFunctionFactory({
get: () => ({
diff --git a/x-pack/plugins/canvas/public/lib/monaco_language_def.ts b/x-pack/plugins/canvas/public/lib/monaco_language_def.ts
index f370ab7a9943..12dc385a4147 100644
--- a/x-pack/plugins/canvas/public/lib/monaco_language_def.ts
+++ b/x-pack/plugins/canvas/public/lib/monaco_language_def.ts
@@ -97,7 +97,6 @@ export const language: Language = {
export function registerLanguage(functions: ExpressionFunction[]) {
language.keywords = functions.map((fn) => fn.name);
-
monaco.languages.register({ id: LANGUAGE_ID });
monaco.languages.setMonarchTokensProvider(LANGUAGE_ID, language);
}
diff --git a/x-pack/plugins/canvas/public/plugin_api.ts b/x-pack/plugins/canvas/public/plugin_api.ts
index 8f39f2d990d0..55a7390437c2 100644
--- a/x-pack/plugins/canvas/public/plugin_api.ts
+++ b/x-pack/plugins/canvas/public/plugin_api.ts
@@ -22,7 +22,9 @@ export interface CanvasApi {
addArgumentUIs: AddToRegistry;
addDatasourceUIs: AddToRegistry;
addElements: AddToRegistry;
- addFunctions: AddSpecsToRegistry<() => AnyExpressionFunctionDefinition>;
+ addFunctions: AddSpecsToRegistry<
+ (() => AnyExpressionFunctionDefinition) | AnyExpressionFunctionDefinition
+ >;
addModelUIs: AddToRegistry;
addRenderers: AddSpecsToRegistry;
addTagUIs: AddToRegistry;
diff --git a/x-pack/plugins/canvas/public/services/legacy/expressions.ts b/x-pack/plugins/canvas/public/services/legacy/expressions.ts
index 99915cad745e..b48172172e3c 100644
--- a/x-pack/plugins/canvas/public/services/legacy/expressions.ts
+++ b/x-pack/plugins/canvas/public/services/legacy/expressions.ts
@@ -42,6 +42,7 @@ export const expressionsServiceFactory: CanvasServiceFactory
return batchedFunction({ functionName, args, context: serialize(input) });
},
});
+
expressions.registerFunction(fn);
});
})();
diff --git a/x-pack/plugins/canvas/public/services/legacy/stubs/expressions.ts b/x-pack/plugins/canvas/public/services/legacy/stubs/expressions.ts
index bd1076ab0bf8..1093bb745213 100644
--- a/x-pack/plugins/canvas/public/services/legacy/stubs/expressions.ts
+++ b/x-pack/plugins/canvas/public/services/legacy/stubs/expressions.ts
@@ -17,7 +17,9 @@ const setup = expressionsPlugin.setup(placeholder);
export const expressionsService: ExpressionsService = setup.fork();
-functionDefinitions.forEach((fn) => expressionsService.registerFunction(fn));
-renderFunctions.forEach((fn) => {
- expressionsService.registerRenderer((fn as unknown) as AnyExpressionRenderDefinition);
-});
+export function setupExpressionsService() {
+ functionDefinitions.forEach((fn) => expressionsService.registerFunction(fn));
+ renderFunctions.forEach((fn) => {
+ expressionsService.registerRenderer((fn as unknown) as AnyExpressionRenderDefinition);
+ });
+}
diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__snapshots__/settings.test.tsx.snap b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__snapshots__/settings.test.tsx.snap
index 075c0cd38675..988aba48d976 100644
--- a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__snapshots__/settings.test.tsx.snap
+++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__snapshots__/settings.test.tsx.snap
@@ -324,6 +324,7 @@ exports[` can navigate Autoplay Settings 2`] = `
- {dedent(`
- curl -X POST '${documentsApiUrl}'
- -H 'Content-Type: application/json'
- -H 'Authorization: Bearer ${apiKey}'
- -d '${DOCUMENTS_API_JSON_EXAMPLE}'
- # Returns
- # [
- # {
- # "id": "park_rocky-mountain",
- # "errors": []
- # },
- # {
- # "id": "park_saguaro",
- # "errors": []
- # }
- # ]
- `)}
+ {`\
+curl -X POST '${documentsApiUrl}' \\
+ -H 'Content-Type: application/json' \\
+ -H 'Authorization: Bearer ${apiKey}' \\
+ -d '${DOCUMENTS_API_JSON_EXAMPLE}'
+# Returns
+# [
+# {
+# "id": "park_rocky-mountain",
+# "errors": []
+# },
+# {
+# "id": "park_saguaro",
+# "errors": []
+# }
+# ]`}
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/constants.ts
index 21051e77547f..d6ee25da6e2b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/constants.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/constants.ts
@@ -7,12 +7,7 @@
import { i18n } from '@kbn/i18n';
-export const LEAVE_UNASSIGNED_FIELD = i18n.translate(
- 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.leaveUnassigned.field',
- {
- defaultMessage: 'Leave unassigned',
- }
-);
+export const LEAVE_UNASSIGNED_FIELD = '';
export const SUCCESS_MESSAGE = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.success.message',
diff --git a/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts b/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts
index 6275d6aa9dce..d7ba5f05e691 100644
--- a/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts
+++ b/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts
@@ -5,9 +5,24 @@
* 2.0.
*/
+import type { LicenseType } from '../../../licensing/common/types';
+import { LICENSE_TYPE } from '../../../licensing/server';
import { KibanaFeature } from '../';
+import { SubFeaturePrivilegeConfig } from '../../common';
+import type { FeaturePrivilegeIteratorOptions } from './feature_privilege_iterator';
import { featurePrivilegeIterator } from './feature_privilege_iterator';
+function getFeaturePrivilegeIterator(
+ feature: KibanaFeature,
+ options: Omit & { licenseType: LicenseType }
+) {
+ const { licenseType, ...otherOptions } = options;
+ const licenseHasAtLeast = (licenseTypeToCheck: LicenseType) => {
+ return LICENSE_TYPE[licenseTypeToCheck] <= LICENSE_TYPE[options.licenseType];
+ };
+ return featurePrivilegeIterator(feature, { licenseHasAtLeast, ...otherOptions });
+}
+
describe('featurePrivilegeIterator', () => {
it('handles features with no privileges', () => {
const feature = new KibanaFeature({
@@ -19,7 +34,7 @@ describe('featurePrivilegeIterator', () => {
});
const actualPrivileges = Array.from(
- featurePrivilegeIterator(feature, {
+ getFeaturePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: true,
licenseType: 'basic',
})
@@ -90,7 +105,7 @@ describe('featurePrivilegeIterator', () => {
});
const actualPrivileges = Array.from(
- featurePrivilegeIterator(feature, {
+ getFeaturePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: true,
licenseType: 'basic',
})
@@ -219,7 +234,7 @@ describe('featurePrivilegeIterator', () => {
});
const actualPrivileges = Array.from(
- featurePrivilegeIterator(feature, {
+ getFeaturePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: true,
licenseType: 'basic',
predicate: (privilegeId) => privilegeId === 'all',
@@ -357,7 +372,7 @@ describe('featurePrivilegeIterator', () => {
});
const actualPrivileges = Array.from(
- featurePrivilegeIterator(feature, {
+ getFeaturePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: false,
licenseType: 'basic',
})
@@ -519,7 +534,7 @@ describe('featurePrivilegeIterator', () => {
});
const actualPrivileges = Array.from(
- featurePrivilegeIterator(feature, {
+ getFeaturePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: true,
licenseType: 'basic',
})
@@ -682,7 +697,7 @@ describe('featurePrivilegeIterator', () => {
});
const actualPrivileges = Array.from(
- featurePrivilegeIterator(feature, {
+ getFeaturePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: true,
licenseType: 'basic',
})
@@ -852,7 +867,7 @@ describe('featurePrivilegeIterator', () => {
});
const actualPrivileges = Array.from(
- featurePrivilegeIterator(feature, {
+ getFeaturePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: true,
licenseType: 'basic',
})
@@ -1020,7 +1035,7 @@ describe('featurePrivilegeIterator', () => {
});
const actualPrivileges = Array.from(
- featurePrivilegeIterator(feature, {
+ getFeaturePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: true,
licenseType: 'basic',
})
@@ -1088,97 +1103,40 @@ describe('featurePrivilegeIterator', () => {
]);
});
- it('excludes sub feature privileges when the minimum license is not met', () => {
+ describe('excludes sub-feature privileges when the minimum license is not met', () => {
+ function createSubFeaturePrivilegeConfig(licenseType: LicenseType): SubFeaturePrivilegeConfig {
+ return {
+ // This is not a realistic sub-feature privilege config, but we only need the "api" string for our test cases
+ id: `${licenseType}-sub-feature`,
+ name: '',
+ includeIn: 'all',
+ minimumLicense: licenseType,
+ api: [`${licenseType}-api`],
+ savedObject: { all: [], read: [] },
+ ui: [],
+ };
+ }
+
const feature = new KibanaFeature({
- id: 'foo',
- name: 'foo',
+ id: 'feature',
+ name: 'feature-name',
app: [],
- category: { id: 'foo', label: 'foo' },
+ category: { id: 'category-id', label: 'category-label' },
privileges: {
- all: {
- api: ['all-api', 'read-api'],
- app: ['foo'],
- catalogue: ['foo-catalogue'],
- management: {
- section: ['foo-management'],
- },
- savedObject: {
- all: ['all-type'],
- read: ['read-type'],
- },
- alerting: {
- rule: {
- all: ['alerting-all-type'],
- },
- alert: {
- read: ['alerting-another-read-type'],
- },
- },
- cases: {
- all: ['cases-all-type'],
- read: ['cases-read-type'],
- },
- ui: ['ui-action'],
- },
- read: {
- api: ['read-api'],
- app: ['foo'],
- catalogue: ['foo-catalogue'],
- management: {
- section: ['foo-management'],
- },
- savedObject: {
- all: [],
- read: ['read-type'],
- },
- alerting: {
- rule: {
- read: ['alerting-read-type'],
- },
- alert: {
- read: ['alerting-read-type'],
- },
- },
- cases: {
- read: ['cases-read-type'],
- },
- ui: ['ui-action'],
- },
+ all: { savedObject: { all: ['obj-type'], read: [] }, ui: [] },
+ read: { savedObject: { all: [], read: ['obj-type'] }, ui: [] },
},
subFeatures: [
{
- name: 'sub feature 1',
+ name: `sub-feature-name`,
privilegeGroups: [
{
groupType: 'independent',
privileges: [
- {
- id: 'sub-feature-priv-1',
- name: 'first sub feature privilege',
- includeIn: 'all',
- minimumLicense: 'gold',
- api: ['sub-feature-api'],
- app: ['sub-app'],
- catalogue: ['sub-catalogue'],
- management: {
- section: ['other-sub-management'],
- kibana: ['sub-management'],
- },
- savedObject: {
- all: ['all-sub-type'],
- read: ['read-sub-type'],
- },
- alerting: {
- alert: {
- all: ['alerting-all-sub-type'],
- },
- },
- cases: {
- all: ['cases-all-sub-type'],
- read: ['cases-read-sub-type'],
- },
- ui: ['ui-sub-type'],
- },
+ createSubFeaturePrivilegeConfig('gold'),
+ createSubFeaturePrivilegeConfig('platinum'),
+ createSubFeaturePrivilegeConfig('enterprise'),
+ // Note: we intentionally do not include a sub-feature privilege config for the "trial" license because that should never be used
],
},
],
@@ -1186,70 +1144,64 @@ describe('featurePrivilegeIterator', () => {
],
});
- const actualPrivileges = Array.from(
- featurePrivilegeIterator(feature, {
- augmentWithSubFeaturePrivileges: true,
- licenseType: 'basic',
- })
- );
+ // Each of the test cases below is a minimal check to make sure the correct sub-feature privileges are applied -- nothing more, nothing less
+ // Note: we do not include a test case for the "basic" license, because sub-feature privileges are not enabled at that license level
- expect(actualPrivileges).toEqual([
- {
- privilegeId: 'all',
- privilege: {
- api: ['all-api', 'read-api'],
- app: ['foo'],
- catalogue: ['foo-catalogue'],
- management: {
- section: ['foo-management'],
- },
- savedObject: {
- all: ['all-type'],
- read: ['read-type'],
- },
- alerting: {
- rule: {
- all: ['alerting-all-type'],
- },
- alert: {
- read: ['alerting-another-read-type'],
- },
- },
- cases: {
- all: ['cases-all-type'],
- read: ['cases-read-type'],
- },
- ui: ['ui-action'],
- },
- },
- {
- privilegeId: 'read',
- privilege: {
- api: ['read-api'],
- app: ['foo'],
- catalogue: ['foo-catalogue'],
- management: {
- section: ['foo-management'],
- },
- savedObject: {
- all: [],
- read: ['read-type'],
- },
- alerting: {
- rule: {
- read: ['alerting-read-type'],
- },
- alert: {
- read: ['alerting-read-type'],
- },
- },
- cases: {
- read: ['cases-read-type'],
- },
- ui: ['ui-action'],
- },
- },
- ]);
+ it('with a gold license', () => {
+ const actualPrivileges = Array.from(
+ getFeaturePrivilegeIterator(feature, {
+ augmentWithSubFeaturePrivileges: true,
+ licenseType: 'gold',
+ })
+ );
+ const expectedPrivilege = expect.objectContaining({ api: ['gold-api'] });
+ expect(actualPrivileges).toEqual(
+ expect.arrayContaining([{ privilegeId: 'all', privilege: expectedPrivilege }])
+ );
+ });
+
+ it('with a platinum license', () => {
+ const actualPrivileges = Array.from(
+ getFeaturePrivilegeIterator(feature, {
+ augmentWithSubFeaturePrivileges: true,
+ licenseType: 'platinum',
+ })
+ );
+ const expectedPrivilege = expect.objectContaining({ api: ['gold-api', 'platinum-api'] });
+ expect(actualPrivileges).toEqual(
+ expect.arrayContaining([{ privilegeId: 'all', privilege: expectedPrivilege }])
+ );
+ });
+
+ it('with an enterprise license', () => {
+ const actualPrivileges = Array.from(
+ getFeaturePrivilegeIterator(feature, {
+ augmentWithSubFeaturePrivileges: true,
+ licenseType: 'enterprise',
+ })
+ );
+ const expectedPrivilege = expect.objectContaining({
+ api: ['gold-api', 'platinum-api', 'enterprise-api'],
+ });
+ expect(actualPrivileges).toEqual(
+ expect.arrayContaining([{ privilegeId: 'all', privilege: expectedPrivilege }])
+ );
+ });
+
+ it('with a trial license', () => {
+ const actualPrivileges = Array.from(
+ getFeaturePrivilegeIterator(feature, {
+ augmentWithSubFeaturePrivileges: true,
+ licenseType: 'trial',
+ })
+ );
+ const expectedPrivilege = expect.objectContaining({
+ api: ['gold-api', 'platinum-api', 'enterprise-api'],
+ });
+ expect(actualPrivileges).toEqual(
+ expect.arrayContaining([{ privilegeId: 'all', privilege: expectedPrivilege }])
+ );
+ });
});
it(`can augment primary feature privileges even if they don't specify their own`, () => {
@@ -1316,7 +1268,7 @@ describe('featurePrivilegeIterator', () => {
});
const actualPrivileges = Array.from(
- featurePrivilegeIterator(feature, {
+ getFeaturePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: true,
licenseType: 'basic',
})
@@ -1470,7 +1422,7 @@ describe('featurePrivilegeIterator', () => {
});
const actualPrivileges = Array.from(
- featurePrivilegeIterator(feature, {
+ getFeaturePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: true,
licenseType: 'basic',
})
diff --git a/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts b/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts
index 8843423ed3c4..86a232ca68d2 100644
--- a/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts
+++ b/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts
@@ -21,9 +21,10 @@ export interface FeaturePrivilegeIteratorOptions {
augmentWithSubFeaturePrivileges: boolean;
/**
- * The current license type. Controls which sub-features are returned, as they may have different license terms than the overall feature.
+ * Function that returns whether the current license is equal to or greater than the given license type.
+ * Controls which sub-features are returned, as they may have different license terms than the overall feature.
*/
- licenseType: LicenseType;
+ licenseHasAtLeast: (licenseType: LicenseType) => boolean | undefined;
/**
* Optional predicate to filter the returned set of privileges.
@@ -59,7 +60,7 @@ const featurePrivilegeIterator: FeaturePrivilegeIterator = function* featurePriv
if (options.augmentWithSubFeaturePrivileges) {
yield {
privilegeId,
- privilege: mergeWithSubFeatures(privilegeId, privilege, feature, options.licenseType),
+ privilege: mergeWithSubFeatures(privilegeId, privilege, feature, options.licenseHasAtLeast),
};
} else {
yield { privilegeId, privilege };
@@ -71,10 +72,10 @@ function mergeWithSubFeatures(
privilegeId: string,
privilege: FeatureKibanaPrivileges,
feature: KibanaFeature,
- licenseType: LicenseType
+ licenseHasAtLeast: FeaturePrivilegeIteratorOptions['licenseHasAtLeast']
) {
const mergedConfig = _.cloneDeep(privilege);
- for (const subFeaturePrivilege of subFeaturePrivilegeIterator(feature, licenseType)) {
+ for (const subFeaturePrivilege of subFeaturePrivilegeIterator(feature, licenseHasAtLeast)) {
if (subFeaturePrivilege.includeIn !== 'read' && subFeaturePrivilege.includeIn !== privilegeId) {
continue;
}
diff --git a/x-pack/plugins/features/server/feature_privilege_iterator/sub_feature_privilege_iterator.ts b/x-pack/plugins/features/server/feature_privilege_iterator/sub_feature_privilege_iterator.ts
index e4cc52bca81b..4e4364333883 100644
--- a/x-pack/plugins/features/server/feature_privilege_iterator/sub_feature_privilege_iterator.ts
+++ b/x-pack/plugins/features/server/feature_privilege_iterator/sub_feature_privilege_iterator.ts
@@ -16,17 +16,17 @@ import type { LicenseType } from '../../../licensing/server';
*/
export type SubFeaturePrivilegeIterator = (
feature: KibanaFeature,
- licenseType: LicenseType
+ licenseHasAtLeast: (licenseType: LicenseType) => boolean | undefined
) => IterableIterator;
const subFeaturePrivilegeIterator: SubFeaturePrivilegeIterator = function* subFeaturePrivilegeIterator(
feature: KibanaFeature,
- licenseType: LicenseType
+ licenseHasAtLeast: (licenseType: LicenseType) => boolean | undefined
): IterableIterator {
for (const subFeature of feature.subFeatures) {
for (const group of subFeature.privilegeGroups) {
yield* group.privileges.filter(
- (privilege) => !privilege.minimumLicense || privilege.minimumLicense <= licenseType
+ (privilege) => !privilege.minimumLicense || licenseHasAtLeast(privilege.minimumLicense)
);
}
}
diff --git a/x-pack/plugins/features/server/oss_features.test.ts b/x-pack/plugins/features/server/oss_features.test.ts
index 207abaeee947..39bb12d90ea1 100644
--- a/x-pack/plugins/features/server/oss_features.test.ts
+++ b/x-pack/plugins/features/server/oss_features.test.ts
@@ -8,7 +8,7 @@
import { buildOSSFeatures } from './oss_features';
import { featurePrivilegeIterator } from './feature_privilege_iterator';
import { KibanaFeature } from '.';
-import { LicenseType } from '../../licensing/server';
+import { LicenseType, LICENSE_TYPE } from '../../licensing/server';
describe('buildOSSFeatures', () => {
it('returns features including timelion', () => {
@@ -86,7 +86,8 @@ Array [
new KibanaFeature(featureConfig),
{
augmentWithSubFeaturePrivileges: true,
- licenseType,
+ licenseHasAtLeast: (licenseTypeToCheck: LicenseType) =>
+ LICENSE_TYPE[licenseTypeToCheck] <= LICENSE_TYPE[licenseType],
}
)) {
privileges.push(featurePrivilege);
diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json
index 9ec519365ea2..feeba6864433 100644
--- a/x-pack/plugins/fleet/common/openapi/bundled.json
+++ b/x-pack/plugins/fleet/common/openapi/bundled.json
@@ -941,6 +941,11 @@
"post": {
"summary": "Agent policy - copy one policy",
"operationId": "agent-policy-copy",
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/kbn_xsrf"
+ }
+ ],
"responses": {
"200": {
"description": "OK",
@@ -981,8 +986,7 @@
}
},
"description": ""
- },
- "description": "Copies one agent policy"
+ }
}
},
"/agent_policies/delete": {
diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml
index a5d177e9a0f1..38daf60b33e0 100644
--- a/x-pack/plugins/fleet/common/openapi/bundled.yaml
+++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml
@@ -579,6 +579,8 @@ paths:
post:
summary: Agent policy - copy one policy
operationId: agent-policy-copy
+ parameters:
+ - $ref: '#/components/parameters/kbn_xsrf'
responses:
'200':
description: OK
@@ -604,7 +606,6 @@ paths:
required:
- name
description: ''
- description: Copies one agent policy
/agent_policies/delete:
post:
summary: Agent policy - Delete
diff --git a/x-pack/plugins/fleet/common/openapi/paths/agent_policies@{agent_policy_id}@copy.yaml b/x-pack/plugins/fleet/common/openapi/paths/agent_policies@{agent_policy_id}@copy.yaml
index 4b42f8cab067..487f6e95f866 100644
--- a/x-pack/plugins/fleet/common/openapi/paths/agent_policies@{agent_policy_id}@copy.yaml
+++ b/x-pack/plugins/fleet/common/openapi/paths/agent_policies@{agent_policy_id}@copy.yaml
@@ -7,6 +7,8 @@ parameters:
post:
summary: Agent policy - copy one policy
operationId: agent-policy-copy
+ parameters:
+ - $ref: ../components/headers/kbn_xsrf.yaml
responses:
'200':
description: OK
@@ -32,4 +34,4 @@ post:
required:
- name
description: ''
- description: Copies one agent policy
+
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx
index c276e67cabbf..fde09c3dbea3 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx
@@ -163,22 +163,6 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{
defaultMessage="Description"
/>
}
- helpText={
-
- {i18n.translate(
- 'xpack.fleet.createPackagePolicy.stepConfigure.packagePolicyNamespaceHelpLearnMoreLabel',
- { defaultMessage: 'Learn more' }
- )}
-
- ),
- }}
- />
- }
labelAppend={
}
+ helpText={
+
+ {i18n.translate(
+ 'xpack.fleet.createPackagePolicy.stepConfigure.packagePolicyNamespaceHelpLearnMoreLabel',
+ { defaultMessage: 'Learn more' }
+ )}
+
+ ),
+ }}
+ />
+ }
>
);
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx
index 63372e435cfa..8ec5fd83a125 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx
@@ -223,6 +223,12 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
iconType="plusInCircle"
onClick={() => setFlyoutOpenForPolicyId(agentPolicy.id)}
data-test-subj="addAgentButton"
+ aria-label={i18n.translate(
+ 'xpack.fleet.epm.packageDetails.integrationList.addAgent',
+ {
+ defaultMessage: 'Add Agent',
+ }
+ )}
/>
)}
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.helpers.tsx
index 4677ea4b747b..79ffd28c9e78 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.helpers.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.helpers.tsx
@@ -99,6 +99,13 @@ const createActions = (testBed: TestBed) => {
component.update();
},
+ async setProcessorType(type: string) {
+ await act(async () => {
+ find('processorTypeSelector.input').simulate('change', [{ value: type }]);
+ });
+ component.update();
+ },
+
removeProcessor(processorSelector: string) {
find(`${processorSelector}.moreMenu.button`).simulate('click');
find(`${processorSelector}.moreMenu.deleteButton`).simulate('click');
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.test.tsx
index fbc46159c4e1..5b02927ab873 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.test.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.test.tsx
@@ -5,6 +5,7 @@
* 2.0.
*/
+import { act } from 'react-dom/test-utils';
import { setup, SetupResult } from './pipeline_processors_editor.helpers';
import { Pipeline } from '../../../../../common/types';
@@ -120,6 +121,37 @@ describe('Pipeline Editor', () => {
});
});
+ it('allows to edit an existing processor and change its type', async () => {
+ const { actions, exists, component, find } = testBed;
+
+ // Open one of the existing processors
+ actions.openProcessorEditor('processors>2');
+ expect(exists('editProcessorForm')).toBeTruthy();
+
+ // Change its type to `append` and set the missing required fields
+ await actions.setProcessorType('append');
+ await act(async () => {
+ find('appendValueField.input').simulate('change', [{ label: 'some_value' }]);
+ });
+ component.update();
+
+ await actions.submitProcessorForm();
+
+ const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1];
+ const {
+ processors: { 2: editedProcessor },
+ } = onUpdateResult.getData();
+
+ expect(editedProcessor.append).toEqual({
+ if: undefined,
+ tag: undefined,
+ description: undefined,
+ ignore_failure: undefined,
+ field: 'test',
+ value: ['some_value'],
+ });
+ });
+
it('removes a processor', () => {
const { actions } = testBed;
// processor>0 denotes the first processor in the top-level processors array.
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/test_pipeline.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/test_pipeline.helpers.tsx
index c980f84910fa..88e5c62a5b1d 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/test_pipeline.helpers.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/test_pipeline.helpers.tsx
@@ -102,6 +102,13 @@ const createActions = (testBed: TestBed) => {
component.update();
},
+ async clickProcessorConfigurationTab() {
+ await act(async () => {
+ find('configurationTab').simulate('click');
+ });
+ component.update();
+ },
+
async clickProcessorOutputTab() {
await act(async () => {
find('outputTab').simulate('click');
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/test_pipeline.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/test_pipeline.test.tsx
index 91384e36c839..607978512e20 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/test_pipeline.test.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/test_pipeline.test.tsx
@@ -356,6 +356,21 @@ describe('Test pipeline', () => {
expect(statusIconLabel).toEqual('Success');
});
+ describe('Configuration tab', () => {
+ it('should not clear up form when clicking configuration tab', async () => {
+ const { actions, find, exists } = testBed;
+
+ // Click processor to open manage flyout
+ await actions.clickProcessor('processors>0');
+ // Verify flyout opened
+ expect(exists('editProcessorForm')).toBe(true);
+ // Click the "Configuration" tab
+ await actions.clickProcessorConfigurationTab();
+ // Verify type selector has not changed
+ expect(find('processorTypeSelector.input').text()).toBe('Set');
+ });
+ });
+
describe('Output tab', () => {
beforeEach(async () => {
const { actions } = testBed;
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/edit_processor_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/edit_processor_form.tsx
index c63842f28f7e..c80b9b136339 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/edit_processor_form.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/edit_processor_form.tsx
@@ -196,6 +196,11 @@ export const EditProcessorForm: FunctionComponent = ({
{tabs.map((tab) => (
{
+ // No need to do anything if user clicks the already active tab
+ if (tab.id === activeTab) {
+ return;
+ }
+
if (tab.id === 'output') {
await handleSubmit(false);
} else {
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/context/processors_context.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/context/processors_context.tsx
index ddf996de7805..6233b220ae77 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/context/processors_context.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/context/processors_context.tsx
@@ -158,18 +158,26 @@ export const PipelineProcessorsContextProvider: FunctionComponent = ({
'internal_networks_field',
];
+ // If the processor type is changed while editing, we need to ignore unkownOptions as they
+ // will contain the fields from the previous processor resulting in the wrong request.
+ const hasProcessorTypeChanged = mode.arg.processor.type !== processorTypeAndOptions.type;
// The processor that we are updating may have options configured the UI does not know about
- const unknownOptions = omit(mode.arg.processor.options, knownOptionNames);
+ const unknownOptions = hasProcessorTypeChanged
+ ? {}
+ : omit(mode.arg.processor.options, knownOptionNames);
// In order to keep the options we don't get back from our UI, we merge the known and unknown options
const updatedProcessorOptions = {
...processorTypeAndOptions.options,
...unknownOptions,
};
+
processorsDispatch({
type: 'updateProcessor',
payload: {
processor: {
...mode.arg.processor,
+ // Always prefer the newly selected processor type, as it might change during editing
+ type: processorTypeAndOptions.type,
options: updatedProcessorOptions,
},
selector: mode.arg.selector,
diff --git a/x-pack/plugins/maps/server/sample_data/flights_saved_objects.js b/x-pack/plugins/maps/server/sample_data/flights_saved_objects.js
index a1fbeacfcbfd..4e9915623d7c 100644
--- a/x-pack/plugins/maps/server/sample_data/flights_saved_objects.js
+++ b/x-pack/plugins/maps/server/sample_data/flights_saved_objects.js
@@ -24,7 +24,7 @@ const layerList = [
{
id: 'jzppx',
label: 'Flights',
- minZoom: 9,
+ minZoom: 8,
maxZoom: 24,
alpha: 1,
sourceDescriptor: {
@@ -45,26 +45,41 @@ const layerList = [
'AvgTicketPrice',
'FlightDelay',
],
+ applyGlobalQuery: true,
+ scalingType: 'MVT',
+ sortField: 'timestamp',
indexPatternRefName: 'layer_1_source_index_pattern',
},
visible: true,
style: {
type: 'VECTOR',
properties: {
+ icon: {
+ type: 'STATIC',
+ options: {
+ value: 'marker',
+ },
+ },
fillColor: {
type: 'DYNAMIC',
options: {
field: {
- name: 'FlightTimeMin',
+ name: 'FlightDelayMin',
origin: 'source',
},
- color: 'Greens',
+ color: 'Yellow to Red',
+ fieldMetaOptions: {
+ isEnabled: false,
+ sigma: 3,
+ },
+ type: 'ORDINAL',
+ useCustomColorRamp: false,
},
},
lineColor: {
type: 'STATIC',
options: {
- color: '#FFFFFF',
+ color: '#000',
},
},
lineWidth: {
@@ -74,25 +89,61 @@ const layerList = [
},
},
iconSize: {
- type: 'DYNAMIC',
+ type: 'STATIC',
options: {
- field: {
- name: 'DistanceMiles',
- origin: 'source',
- },
- minSize: 1,
- maxSize: 32,
+ size: 6,
+ },
+ },
+ iconOrientation: {
+ type: 'STATIC',
+ options: {
+ orientation: 0,
+ },
+ },
+ labelText: {
+ type: 'STATIC',
+ options: {
+ value: '',
+ },
+ },
+ labelColor: {
+ type: 'STATIC',
+ options: {
+ color: '#000000',
+ },
+ },
+ labelSize: {
+ type: 'STATIC',
+ options: {
+ size: 14,
+ },
+ },
+ labelBorderColor: {
+ type: 'STATIC',
+ options: {
+ color: '#FFFFFF',
+ },
+ },
+ symbolizeAs: {
+ options: {
+ value: 'circle',
+ },
+ },
+ labelBorderSize: {
+ options: {
+ size: 'SMALL',
},
},
},
+ isTimeAware: true,
},
- type: 'VECTOR',
+ type: 'TILED_VECTOR',
},
{
id: 'y4jsz',
label: 'Flight Origin Location',
minZoom: 0,
- maxZoom: 9,
+ maxZoom: 8,
alpha: 1,
sourceDescriptor: {
type: 'ES_GEO_GRID',
@@ -106,25 +157,37 @@ const layerList = [
label: 'flight count',
},
{
- type: 'avg',
- field: 'FlightTimeMin',
- label: 'minimum flight time',
+ type: 'sum',
+ field: 'FlightDelayMin',
},
],
+ applyGlobalQuery: true,
indexPatternRefName: 'layer_2_source_index_pattern',
},
visible: true,
style: {
type: 'VECTOR',
properties: {
+ icon: {
+ type: 'STATIC',
+ options: {
+ value: 'marker',
+ },
+ },
fillColor: {
type: 'DYNAMIC',
options: {
+ color: 'Yellow to Red',
+ fieldMetaOptions: {
+ isEnabled: false,
+ sigma: 3,
+ },
+ type: 'ORDINAL',
+ useCustomColorRamp: false,
field: {
- name: 'doc_count',
+ name: 'sum_of_FlightDelayMin',
origin: 'source',
},
- color: 'Blues',
},
},
lineColor: {
@@ -143,80 +206,59 @@ const layerList = [
type: 'DYNAMIC',
options: {
field: {
- name: 'avg_of_FlightTimeMin',
origin: 'source',
+ name: 'doc_count',
},
- minSize: 1,
+ minSize: 4,
maxSize: 32,
+ fieldMetaOptions: {
+ isEnabled: false,
+ sigma: 3,
+ },
},
},
- },
- },
- type: 'VECTOR',
- },
- {
- id: 'x8xpo',
- label: 'Flight Destination Location',
- minZoom: 0,
- maxZoom: 9,
- alpha: 1,
- sourceDescriptor: {
- type: 'ES_GEO_GRID',
- resolution: 'COARSE',
- id: '60a7346a-8c5f-4c03-b7d1-e8b36e847551',
- geoField: 'DestLocation',
- requestType: 'point',
- metrics: [
- {
- type: 'count',
- label: 'flight count',
+ iconOrientation: {
+ type: 'STATIC',
+ options: {
+ orientation: 0,
+ },
},
- {
- type: 'avg',
- field: 'FlightDelayMin',
- label: 'average delay',
+ labelText: {
+ type: 'STATIC',
+ options: {
+ value: '',
+ },
},
- ],
- indexPatternRefName: 'layer_3_source_index_pattern',
- },
- visible: true,
- style: {
- type: 'VECTOR',
- properties: {
- fillColor: {
- type: 'DYNAMIC',
+ labelColor: {
+ type: 'STATIC',
options: {
- field: {
- name: 'doc_count',
- origin: 'source',
- },
- color: 'Reds',
+ color: '#000000',
},
},
- lineColor: {
+ labelSize: {
type: 'STATIC',
options: {
- color: '#af0303',
+ size: 14,
},
},
- lineWidth: {
+ labelBorderColor: {
type: 'STATIC',
options: {
- size: 1,
+ color: '#FFFFFF',
},
},
- iconSize: {
- type: 'DYNAMIC',
+ symbolizeAs: {
options: {
- field: {
- name: 'avg_of_FlightDelayMin',
- origin: 'source',
- },
- minSize: 1,
- maxSize: 32,
+ value: 'circle',
+ },
+ },
+ labelBorderSize: {
+ options: {
+ size: 'SMALL',
},
},
},
+ isTimeAware: true,
},
type: 'VECTOR',
},
@@ -227,45 +269,34 @@ export const getFlightsSavedObjects = () => {
{
id: '5dd88580-1906-11e9-919b-ffe5949a18d2',
type: 'map',
- updated_at: '2019-01-15T20:44:54.767Z',
- version: 2,
+ updated_at: '2021-07-07T02:20:04.294Z',
+ version: '3',
+ attributes: {
+ title: i18n.translate('xpack.maps.sampleData.flightsSpec.mapsTitle', {
+ defaultMessage: '[Flights] Origin Time Delayed',
+ }),
+ description: '',
+ layerListJSON: JSON.stringify(layerList),
+ mapStateJSON:
+ '{"zoom":4.28,"center":{"lon":-112.44472,"lat":34.65823},"timeFilters":{"from":"now-7d","to":"now"},"refreshConfig":{"isPaused":true,"interval":0},"query":{"query":"","language":"kuery"},"filters":[],"settings":{"autoFitToDataBounds":false,"backgroundColor":"#ffffff","disableInteractive":false,"disableTooltipControl":false,"hideToolbarOverlay":false,"hideLayerControl":false,"hideViewControl":false,"initialLocation":"LAST_SAVED_LOCATION","fixedLocation":{"lat":0,"lon":0,"zoom":2},"browserLocation":{"zoom":2},"maxZoom":24,"minZoom":0,"showScaleControl":false,"showSpatialFilters":true,"showTimesliderToggleButton":true,"spatialFiltersAlpa":0.3,"spatialFiltersFillColor":"#DA8B45","spatialFiltersLineColor":"#DA8B45"}}',
+ title: '[Flights] Origin Time Delayed',
+ uiStateJSON: '{"isLayerTOCOpen":true,"openTOCDetails":[]}',
+ },
+ migrationVersion: {
+ map: '7.14.0',
+ },
references: [
{
+ id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
name: 'layer_1_source_index_pattern',
type: 'index-pattern',
- id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
},
{
- name: 'layer_2_source_index_pattern',
- type: 'index-pattern',
id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
- },
- {
- name: 'layer_3_source_index_pattern',
+ name: 'layer_2_source_index_pattern',
type: 'index-pattern',
- id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
},
],
- migrationVersion: {
- map: '7.4.0',
- },
- attributes: {
- title: i18n.translate('xpack.maps.sampleData.flightaSpec.mapsTitle', {
- defaultMessage: '[Flights] Origin and Destination Flight Time',
- }),
- description: '',
- mapStateJSON:
- '{"zoom":3.14,"center":{"lon":-89.58746,"lat":38.38637},"timeFilters":{"from":"now-7d","to":"now"},"refreshConfig":{"isPaused":true,"interval":0},"query":{"query":"","language":"kuery"}}',
- layerListJSON: JSON.stringify(layerList),
- uiStateJSON: '{"isDarkMode":false}',
- bounds: {
- type: 'envelope',
- coordinates: [
- [-139.83779, 56.64828],
- [-39.33713, 14.04811],
- ],
- },
- },
},
];
};
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/metric_selection_summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/metric_selection_summary.tsx
index 02566474512e..bb4b83f639be 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/metric_selection_summary.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/metric_selection_summary.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { FC, useContext, useEffect, useState } from 'react';
+import React, { FC, useContext, useEffect, useState, useMemo } from 'react';
import { EuiSpacer } from '@elastic/eui';
import { JobCreatorContext } from '../../../job_creator_context';
import { RareJobCreator } from '../../../../../common/job_creator';
@@ -17,14 +17,15 @@ import { RARE_DETECTOR_TYPE } from './rare_view';
import { DetectorDescription } from './detector_description';
const DTR_IDX = 0;
-interface Props {
- rareDetectorType: RARE_DETECTOR_TYPE;
-}
-export const RareDetectorsSummary: FC = ({ rareDetectorType }) => {
- const { jobCreator: jc, chartLoader, resultsLoader, chartInterval } = useContext(
- JobCreatorContext
- );
+export const RareDetectorsSummary: FC = () => {
+ const {
+ jobCreator: jc,
+ chartLoader,
+ resultsLoader,
+ chartInterval,
+ jobCreatorUpdated,
+ } = useContext(JobCreatorContext);
const jobCreator = jc as RareJobCreator;
const [loadingData, setLoadingData] = useState(false);
@@ -32,6 +33,20 @@ export const RareDetectorsSummary: FC = ({ rareDetectorType }) => {
const [eventRateChartData, setEventRateChartData] = useState([]);
const [jobIsRunning, setJobIsRunning] = useState(false);
+ const rareDetectorType = useMemo(() => {
+ if (jobCreator.rareField !== null) {
+ if (jobCreator.populationField === null) {
+ return RARE_DETECTOR_TYPE.RARE;
+ } else {
+ return jobCreator.frequentlyRare
+ ? RARE_DETECTOR_TYPE.FREQ_RARE_POPULATION
+ : RARE_DETECTOR_TYPE.RARE_POPULATION;
+ }
+ } else {
+ return RARE_DETECTOR_TYPE.RARE;
+ }
+ }, [jobCreatorUpdated]);
+
function setResultsWrapper(results: Results) {
const anomalies = results.anomalies[DTR_IDX];
if (anomalies !== undefined) {
@@ -48,6 +63,7 @@ export const RareDetectorsSummary: FC = ({ rareDetectorType }) => {
const resultsSubscription = resultsLoader.subscribeToResults(setResultsWrapper);
jobCreator.subscribeToProgress(watchProgress);
loadChart();
+
return () => {
resultsSubscription.unsubscribe();
};
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/rare_view.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/rare_view.tsx
index d67cac8d0fc5..4d0ed3b58973 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/rare_view.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_view/rare_view.tsx
@@ -35,7 +35,7 @@ export const RareView: FC = ({ isActive, setCanProceed }) => {
}, [rareFieldValid, settingsValid]);
return isActive === false ? (
-
+
) : (
<>
+
+#### Defined in
+
+[rule_registry/server/alert_data_client/alerts_client.ts:66](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L66)
## Methods
@@ -101,7 +112,7 @@ ___
#### Defined in
-[rule_registry/server/alert_data_client/alerts_client.ts:79](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L79)
+[rule_registry/server/alert_data_client/alerts_client.ts:87](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L87)
___
@@ -121,7 +132,7 @@ ___
#### Defined in
-[rule_registry/server/alert_data_client/alerts_client.ts:115](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L115)
+[rule_registry/server/alert_data_client/alerts_client.ts:134](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L134)
___
@@ -142,7 +153,7 @@ ___
#### Defined in
-[rule_registry/server/alert_data_client/alerts_client.ts:68](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L68)
+[rule_registry/server/alert_data_client/alerts_client.ts:76](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L76)
___
@@ -162,7 +173,7 @@ ___
#### Defined in
-[rule_registry/server/alert_data_client/alerts_client.ts:219](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L219)
+[rule_registry/server/alert_data_client/alerts_client.ts:238](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L238)
___
@@ -188,4 +199,4 @@ ___
#### Defined in
-[rule_registry/server/alert_data_client/alerts_client.ts:160](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L160)
+[rule_registry/server/alert_data_client/alerts_client.ts:179](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L179)
diff --git a/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/constructoroptions.md b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/constructoroptions.md
index 051a5affc037..23c5b94ad0d5 100644
--- a/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/constructoroptions.md
+++ b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/constructoroptions.md
@@ -19,7 +19,7 @@
#### Defined in
-[rule_registry/server/alert_data_client/alerts_client.ts:34](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L34)
+[rule_registry/server/alert_data_client/alerts_client.ts:40](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L40)
___
@@ -29,7 +29,7 @@ ___
#### Defined in
-[rule_registry/server/alert_data_client/alerts_client.ts:33](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L33)
+[rule_registry/server/alert_data_client/alerts_client.ts:39](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L39)
___
@@ -39,7 +39,7 @@ ___
#### Defined in
-[rule_registry/server/alert_data_client/alerts_client.ts:35](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L35)
+[rule_registry/server/alert_data_client/alerts_client.ts:41](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L41)
___
@@ -49,4 +49,4 @@ ___
#### Defined in
-[rule_registry/server/alert_data_client/alerts_client.ts:32](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L32)
+[rule_registry/server/alert_data_client/alerts_client.ts:38](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L38)
diff --git a/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/updateoptions.md b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/updateoptions.md
index 10e793155c19..a8bff815c250 100644
--- a/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/updateoptions.md
+++ b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/updateoptions.md
@@ -25,7 +25,7 @@
#### Defined in
-[rule_registry/server/alert_data_client/alerts_client.ts:41](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L41)
+[rule_registry/server/alert_data_client/alerts_client.ts:47](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L47)
___
@@ -35,7 +35,7 @@ ___
#### Defined in
-[rule_registry/server/alert_data_client/alerts_client.ts:39](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L39)
+[rule_registry/server/alert_data_client/alerts_client.ts:45](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L45)
___
@@ -45,7 +45,7 @@ ___
#### Defined in
-[rule_registry/server/alert_data_client/alerts_client.ts:42](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L42)
+[rule_registry/server/alert_data_client/alerts_client.ts:48](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L48)
___
@@ -55,4 +55,4 @@ ___
#### Defined in
-[rule_registry/server/alert_data_client/alerts_client.ts:40](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L40)
+[rule_registry/server/alert_data_client/alerts_client.ts:46](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L46)
diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts
index 553c5ce4472a..e5b89cb86acf 100644
--- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts
+++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts
@@ -6,6 +6,7 @@
*/
import { PublicMethodsOf } from '@kbn/utility-types';
import { decodeVersion, encodeHitVersion } from '@kbn/securitysolution-es-utils';
+
import { AlertTypeParams } from '../../../alerting/server';
import {
ReadOperations,
@@ -16,7 +17,12 @@ import {
import { Logger, ElasticsearchClient } from '../../../../../src/core/server';
import { alertAuditEvent, AlertAuditAction } from './audit_events';
import { AuditLogger } from '../../../security/server';
-import { ALERT_STATUS, OWNER, RULE_ID } from '../../common/technical_rule_data_field_names';
+import {
+ ALERT_STATUS,
+ OWNER,
+ RULE_ID,
+ SPACE_IDS,
+} from '../../common/technical_rule_data_field_names';
import { ParsedTechnicalFields } from '../../common/parse_technical_fields';
import { mapConsumerToIndexName, validFeatureIds, isValidFeatureId } from '../utils/rbac';
@@ -57,12 +63,14 @@ export class AlertsClient {
private readonly auditLogger?: AuditLogger;
private readonly authorization: PublicMethodsOf;
private readonly esClient: ElasticsearchClient;
+ private readonly spaceId: Promise;
constructor({ auditLogger, authorization, logger, esClient }: ConstructorOptions) {
this.logger = logger;
this.authorization = authorization;
this.esClient = esClient;
this.auditLogger = auditLogger;
+ this.spaceId = this.authorization.getSpaceId();
}
public async getAlertsIndex(
@@ -81,13 +89,24 @@ export class AlertsClient {
index,
}: GetAlertParams): Promise<(AlertType & { _version: string | undefined }) | null | undefined> {
try {
+ const alertSpaceId = await this.spaceId;
+ if (alertSpaceId == null) {
+ this.logger.error('Failed to acquire spaceId from authorization client');
+ return;
+ }
const result = await this.esClient.search({
// Context: Originally thought of always just searching `.alerts-*` but that could
// result in a big performance hit. If the client already knows which index the alert
// belongs to, passing in the index will speed things up
index: index ?? '.alerts-*',
ignore_unavailable: true,
- body: { query: { term: { _id: id } } },
+ body: {
+ query: {
+ bool: {
+ filter: [{ term: { _id: id } }, { term: { [SPACE_IDS]: alertSpaceId } }],
+ },
+ },
+ },
seq_no_primary_term: true,
});
diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts
index 897c17a82b98..bf5375c55c04 100644
--- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts
+++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts
@@ -27,6 +27,7 @@ const alertsClientParams: jest.Mocked = {
beforeEach(() => {
jest.resetAllMocks();
+ alertingAuthMock.getSpaceId.mockImplementation(() => Promise.resolve('test_default_space_id'));
});
describe('get()', () => {
@@ -60,6 +61,7 @@ describe('get()', () => {
message: 'hello world 1',
'kibana.rac.alert.owner': 'apm',
'kibana.rac.alert.status': 'open',
+ 'kibana.rac.alert.space_ids': ['test_default_space_id'],
},
},
],
@@ -72,6 +74,9 @@ describe('get()', () => {
Object {
"_version": "WzM2MiwyXQ==",
"kibana.rac.alert.owner": "apm",
+ "kibana.rac.alert.space_ids": Array [
+ "test_default_space_id",
+ ],
"kibana.rac.alert.status": "open",
"message": "hello world 1",
"rule.id": "apm.error_rate",
@@ -83,8 +88,19 @@ describe('get()', () => {
Object {
"body": Object {
"query": Object {
- "term": Object {
- "_id": "1",
+ "bool": Object {
+ "filter": Array [
+ Object {
+ "term": Object {
+ "_id": "1",
+ },
+ },
+ Object {
+ "term": Object {
+ "kibana.space_ids": "test_default_space_id",
+ },
+ },
+ ],
},
},
},
@@ -126,6 +142,7 @@ describe('get()', () => {
message: 'hello world 1',
'kibana.rac.alert.owner': 'apm',
'kibana.rac.alert.status': 'open',
+ 'kibana.rac.alert.space_ids': ['test_default_space_id'],
},
},
],
@@ -187,6 +204,7 @@ describe('get()', () => {
message: 'hello world 1',
'kibana.rac.alert.owner': 'apm',
'kibana.rac.alert.status': 'open',
+ 'kibana.rac.alert.space_ids': ['test_default_space_id'],
},
},
],
@@ -210,6 +228,9 @@ describe('get()', () => {
Object {
"_version": "WzM2MiwyXQ==",
"kibana.rac.alert.owner": "apm",
+ "kibana.rac.alert.space_ids": Array [
+ "test_default_space_id",
+ ],
"kibana.rac.alert.status": "open",
"message": "hello world 1",
"rule.id": "apm.error_rate",
diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts
index 6fc387fe54b3..fd3e0f00a097 100644
--- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts
+++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts
@@ -27,6 +27,7 @@ const alertsClientParams: jest.Mocked = {
beforeEach(() => {
jest.resetAllMocks();
+ alertingAuthMock.getSpaceId.mockImplementation(() => Promise.resolve('test_default_space_id'));
});
describe('update()', () => {
@@ -57,6 +58,7 @@ describe('update()', () => {
message: 'hello world 1',
'kibana.rac.alert.owner': 'apm',
'kibana.rac.alert.status': 'open',
+ 'kibana.rac.alert.space_ids': ['test_default_space_id'],
},
},
],
@@ -142,6 +144,7 @@ describe('update()', () => {
message: 'hello world 1',
'kibana.rac.alert.owner': 'apm',
'kibana.rac.alert.status': 'open',
+ 'kibana.rac.alert.space_ids': ['test_default_space_id'],
},
},
],
@@ -234,6 +237,7 @@ describe('update()', () => {
message: 'hello world 1',
'kibana.rac.alert.owner': 'apm',
'kibana.rac.alert.status': 'open',
+ 'kibana.rac.alert.space_ids': ['test_default_space_id'],
},
},
],
@@ -293,6 +297,7 @@ describe('update()', () => {
message: 'hello world 1',
'kibana.rac.alert.owner': 'apm',
'kibana.rac.alert.status': 'open',
+ 'kibana.rac.alert.space_ids': ['test_default_space_id'],
},
},
],
diff --git a/x-pack/plugins/rule_registry/server/index.ts b/x-pack/plugins/rule_registry/server/index.ts
index 19ea85b056be..6b0765e71cba 100644
--- a/x-pack/plugins/rule_registry/server/index.ts
+++ b/x-pack/plugins/rule_registry/server/index.ts
@@ -14,13 +14,17 @@ export type { RacRequestHandlerContext, RacApiRequestHandlerContext } from './ty
export { RuleDataClient } from './rule_data_client';
export { IRuleDataClient } from './rule_data_client/types';
export { getRuleExecutorData, RuleExecutorData } from './utils/get_rule_executor_data';
-export { createLifecycleRuleTypeFactory } from './utils/create_lifecycle_rule_type_factory';
+export {
+ createLifecycleRuleTypeFactory,
+ LifecycleAlertService,
+} from './utils/create_lifecycle_rule_type_factory';
export {
LifecycleRuleExecutor,
LifecycleAlertServices,
createLifecycleExecutor,
} from './utils/create_lifecycle_executor';
export { createPersistenceRuleTypeFactory } from './utils/create_persistence_rule_type_factory';
+export type { AlertTypeWithExecutor } from './types';
export const plugin = (initContext: PluginInitializerContext) =>
new RuleRegistryPlugin(initContext);
diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/detections_role.json b/x-pack/plugins/rule_registry/server/scripts/observer/detections_role.json
index dd3d3f96e3a3..ee4a1d84f0b7 100644
--- a/x-pack/plugins/rule_registry/server/scripts/observer/detections_role.json
+++ b/x-pack/plugins/rule_registry/server/scripts/observer/detections_role.json
@@ -6,13 +6,7 @@
"kibana": [
{
"feature": {
- "ml": ["read"],
- "monitoring": ["all"],
- "apm": ["minimal_read", "alerts_all"],
- "ruleRegistry": ["all"],
- "actions": ["read"],
- "builtInAlerts": ["all"],
- "alerting": ["all"]
+ "apm": ["read", "alerts_all"]
},
"spaces": ["*"]
}
diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts
index 06c2cc8ff005..8df343fb16d4 100644
--- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts
+++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts
@@ -31,6 +31,7 @@ import {
OWNER,
RULE_UUID,
TIMESTAMP,
+ SPACE_IDS,
} from '../../common/technical_rule_data_field_names';
import { RuleDataClient } from '../rule_data_client';
import { AlertExecutorOptionsWithExtraServices } from '../types';
@@ -124,6 +125,7 @@ export const createLifecycleExecutor = (logger: Logger, ruleDataClient: RuleData
rule,
services: { alertInstanceFactory },
state: previousState,
+ spaceId,
} = options;
const ruleExecutorData = getRuleData(options);
@@ -258,6 +260,15 @@ export const createLifecycleExecutor = (logger: Logger, ruleDataClient: RuleData
event[ALERT_START] = started;
event[ALERT_UUID] = alertUuid;
+ // not sure why typescript needs the non-null assertion here
+ // we already assert the value is not undefined with the ternary
+ // still getting an error with the ternary.. strange.
+
+ event[SPACE_IDS] =
+ event[SPACE_IDS] == null
+ ? [spaceId]
+ : [spaceId, ...event[SPACE_IDS]!.filter((sid) => sid !== spaceId)];
+
if (isNew) {
event[EVENT_ACTION] = 'open';
}
diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts
index 3e7fbbe5cbc5..e26e5b00435f 100644
--- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts
+++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts
@@ -198,6 +198,9 @@ describe('createLifecycleRuleTypeFactory', () => {
"kibana.rac.alert.producer": "producer",
"kibana.rac.alert.start": "2021-06-16T09:01:00.000Z",
"kibana.rac.alert.status": "open",
+ "kibana.space_ids": Array [
+ "spaceId",
+ ],
"rule.category": "ruleTypeName",
"rule.id": "ruleTypeId",
"rule.name": "name",
@@ -217,6 +220,9 @@ describe('createLifecycleRuleTypeFactory', () => {
"kibana.rac.alert.producer": "producer",
"kibana.rac.alert.start": "2021-06-16T09:01:00.000Z",
"kibana.rac.alert.status": "open",
+ "kibana.space_ids": Array [
+ "spaceId",
+ ],
"rule.category": "ruleTypeName",
"rule.id": "ruleTypeId",
"rule.name": "name",
@@ -236,6 +242,9 @@ describe('createLifecycleRuleTypeFactory', () => {
"kibana.rac.alert.producer": "producer",
"kibana.rac.alert.start": "2021-06-16T09:01:00.000Z",
"kibana.rac.alert.status": "open",
+ "kibana.space_ids": Array [
+ "spaceId",
+ ],
"rule.category": "ruleTypeName",
"rule.id": "ruleTypeId",
"rule.name": "name",
@@ -255,6 +264,9 @@ describe('createLifecycleRuleTypeFactory', () => {
"kibana.rac.alert.producer": "producer",
"kibana.rac.alert.start": "2021-06-16T09:01:00.000Z",
"kibana.rac.alert.status": "open",
+ "kibana.space_ids": Array [
+ "spaceId",
+ ],
"rule.category": "ruleTypeName",
"rule.id": "ruleTypeId",
"rule.name": "name",
diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts
index cf1be1bd3201..783077a1f68a 100644
--- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts
+++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts
@@ -16,10 +16,13 @@ import { AlertInstance } from '../../../alerting/server';
import { AlertTypeWithExecutor } from '../types';
import { createLifecycleExecutor } from './create_lifecycle_executor';
-export type LifecycleAlertService> = (alert: {
+export type LifecycleAlertService<
+ TAlertInstanceContext extends Record,
+ TActionGroupIds extends string = string
+> = (alert: {
id: string;
fields: Record;
-}) => AlertInstance;
+}) => AlertInstance;
export const createLifecycleRuleTypeFactory = ({
logger,
diff --git a/x-pack/plugins/rule_registry/server/utils/rbac.ts b/x-pack/plugins/rule_registry/server/utils/rbac.ts
index e07c4394be2f..172201400606 100644
--- a/x-pack/plugins/rule_registry/server/utils/rbac.ts
+++ b/x-pack/plugins/rule_registry/server/utils/rbac.ts
@@ -19,6 +19,7 @@ export const mapConsumerToIndexName = {
infrastructure: '.alerts-observability.metrics',
observability: '.alerts-observability',
siem: ['.alerts-security.alerts', '.siem-signals'],
+ synthetics: '.alerts-observability-synthetics',
};
export type ValidFeatureId = keyof typeof mapConsumerToIndexName;
diff --git a/x-pack/plugins/security/common/licensing/index.mock.ts b/x-pack/plugins/security/common/licensing/index.mock.ts
index 319faa1a5b34..bd85df3dea63 100644
--- a/x-pack/plugins/security/common/licensing/index.mock.ts
+++ b/x-pack/plugins/security/common/licensing/index.mock.ts
@@ -7,15 +7,27 @@
import { of } from 'rxjs';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { LICENSE_TYPE } from '../../../licensing/server';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import type { LicenseType } from '../../../licensing/server';
import type { SecurityLicenseFeatures } from './license_features';
import type { SecurityLicense } from './license_service';
export const licenseMock = {
- create: (features?: Partial): jest.Mocked => ({
+ create: (
+ features: Partial = {},
+ licenseType: LicenseType = 'basic' // default to basic if this is not specified
+ ): jest.Mocked => ({
isLicenseAvailable: jest.fn().mockReturnValue(true),
isEnabled: jest.fn().mockReturnValue(true),
- getType: jest.fn().mockReturnValue('basic'),
- getFeatures: jest.fn(),
+ getFeatures: jest.fn().mockReturnValue(features),
+ hasAtLeast: jest
+ .fn()
+ .mockImplementation(
+ (licenseTypeToCheck: LicenseType) =>
+ LICENSE_TYPE[licenseTypeToCheck] <= LICENSE_TYPE[licenseType]
+ ),
features$: features ? of(features as SecurityLicenseFeatures) : of(),
}),
};
diff --git a/x-pack/plugins/security/common/licensing/license_service.ts b/x-pack/plugins/security/common/licensing/license_service.ts
index 7bebf65b4820..51093428e84a 100644
--- a/x-pack/plugins/security/common/licensing/license_service.ts
+++ b/x-pack/plugins/security/common/licensing/license_service.ts
@@ -14,8 +14,8 @@ import type { SecurityLicenseFeatures } from './license_features';
export interface SecurityLicense {
isLicenseAvailable(): boolean;
isEnabled(): boolean;
- getType(): LicenseType | undefined;
getFeatures(): SecurityLicenseFeatures;
+ hasAtLeast(licenseType: LicenseType): boolean | undefined;
features$: Observable;
}
@@ -39,7 +39,7 @@ export class SecurityLicenseService {
isEnabled: () => this.isSecurityEnabledFromRawLicense(rawLicense),
- getType: () => rawLicense?.type,
+ hasAtLeast: (licenseType: LicenseType) => rawLicense?.hasAtLeast(licenseType),
getFeatures: () => this.calculateFeaturesFromRawLicense(rawLicense),
diff --git a/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts
index de829ea003b0..cd72085c9a16 100644
--- a/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts
+++ b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts
@@ -8,6 +8,8 @@
import type { KibanaFeature } from '../../../../../features/public';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { featuresPluginMock } from '../../../../../features/server/mocks';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import type { LicenseType } from '../../../../../licensing/server';
import type { SecurityLicenseFeatures } from '../../../../common/licensing';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { Actions } from '../../../../server/authorization';
@@ -25,6 +27,7 @@ export const createRawKibanaPrivileges = (
const licensingService = {
getFeatures: () => ({ allowSubFeaturePrivileges } as SecurityLicenseFeatures),
getType: () => 'basic' as const,
+ hasAtLeast: (licenseType: LicenseType) => licenseType === 'basic',
};
return privilegesFactory(
diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap
index 5413bad80187..9db96be27faa 100644
--- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap
+++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap
@@ -183,7 +183,7 @@ exports[`it renders without crashing 1`] = `
"_subscribe": [Function],
},
"getFeatures": [MockFunction],
- "getType": [MockFunction],
+ "hasAtLeast": [MockFunction],
"isEnabled": [MockFunction],
"isLicenseAvailable": [MockFunction],
}
diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/index_privileges.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/index_privileges.test.tsx.snap
index 6dc3165cbea1..12824eb9193d 100644
--- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/index_privileges.test.tsx.snap
+++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/index_privileges.test.tsx.snap
@@ -24,7 +24,7 @@ exports[`it renders without crashing 1`] = `
"_subscribe": [Function],
},
"getFeatures": [MockFunction],
- "getType": [MockFunction],
+ "hasAtLeast": [MockFunction],
"isEnabled": [MockFunction],
"isLicenseAvailable": [MockFunction],
}
diff --git a/x-pack/plugins/security/public/plugin.test.tsx b/x-pack/plugins/security/public/plugin.test.tsx
index c4c551e4bb5b..bcc69bbfbd85 100644
--- a/x-pack/plugins/security/public/plugin.test.tsx
+++ b/x-pack/plugins/security/public/plugin.test.tsx
@@ -46,8 +46,8 @@ describe('Security Plugin', () => {
license: {
isLicenseAvailable: expect.any(Function),
isEnabled: expect.any(Function),
- getType: expect.any(Function),
getFeatures: expect.any(Function),
+ hasAtLeast: expect.any(Function),
features$: expect.any(Observable),
},
});
@@ -74,8 +74,8 @@ describe('Security Plugin', () => {
license: {
isLicenseAvailable: expect.any(Function),
isEnabled: expect.any(Function),
- getType: expect.any(Function),
getFeatures: expect.any(Function),
+ hasAtLeast: expect.any(Function),
features$: expect.any(Observable),
},
management: managementSetupMock,
diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts
index dfe6bef1e00e..5264e74861be 100644
--- a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts
+++ b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts
@@ -7,11 +7,19 @@
import { KibanaFeature } from '../../../../features/server';
import { featuresPluginMock } from '../../../../features/server/mocks';
+import { licenseMock } from '../../../common/licensing/index.mock';
import { Actions } from '../actions';
import { privilegesFactory } from './privileges';
const actions = new Actions('1.0.0-zeta1');
+const mockLicenseServiceBasic = licenseMock.create({ allowSubFeaturePrivileges: false }, 'basic');
+const mockLicenseServiceGold = licenseMock.create({ allowSubFeaturePrivileges: true }, 'gold');
+const mockLicenseServicePlatinum = licenseMock.create(
+ { allowSubFeaturePrivileges: true },
+ 'platinum'
+);
+
describe('features', () => {
test('actions defined at the feature do not cascade to the privileges', () => {
const features: KibanaFeature[] = [
@@ -43,14 +51,10 @@ describe('features', () => {
}),
];
- const mockFeaturesService = featuresPluginMock.createSetup();
- mockFeaturesService.getKibanaFeatures.mockReturnValue(features);
+ const mockFeaturesPlugin = featuresPluginMock.createSetup();
+ mockFeaturesPlugin.getKibanaFeatures.mockReturnValue(features);
- const mockLicenseService = {
- getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
- getType: jest.fn().mockReturnValue('basic'),
- };
- const privileges = privilegesFactory(actions, mockFeaturesService, mockLicenseService);
+ const privileges = privilegesFactory(actions, mockFeaturesPlugin, mockLicenseServiceBasic);
const actual = privileges.get();
expect(actual).toHaveProperty('features.foo-feature', {
@@ -87,11 +91,7 @@ describe('features', () => {
const mockFeaturesPlugin = featuresPluginMock.createSetup();
mockFeaturesPlugin.getKibanaFeatures.mockReturnValue(features);
- const mockLicenseService = {
- getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
- getType: jest.fn().mockReturnValue('basic'),
- };
- const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
+ const privileges = privilegesFactory(actions, mockFeaturesPlugin, mockLicenseServiceBasic);
const expectedAllPrivileges = [
actions.login,
@@ -191,11 +191,7 @@ describe('features', () => {
const mockFeaturesPlugin = featuresPluginMock.createSetup();
mockFeaturesPlugin.getKibanaFeatures.mockReturnValue(features);
- const mockLicenseService = {
- getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
- getType: jest.fn().mockReturnValue('basic'),
- };
- const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
+ const privileges = privilegesFactory(actions, mockFeaturesPlugin, mockLicenseServiceBasic);
const actual = privileges.get();
expect(actual).not.toHaveProperty('features.foo');
@@ -268,15 +264,7 @@ describe('features', () => {
const mockFeaturesPlugin = featuresPluginMock.createSetup();
mockFeaturesPlugin.getKibanaFeatures.mockReturnValue(features);
- const mockLicenseService = {
- getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
- getType: jest.fn().mockReturnValue('basic'),
- };
- const privileges = privilegesFactory(
- actions,
- mockFeaturesPlugin as any,
- mockLicenseService
- );
+ const privileges = privilegesFactory(actions, mockFeaturesPlugin, mockLicenseServiceBasic);
const actual = privileges.get();
expect(actual).toHaveProperty(`${group}.all`, [
@@ -412,15 +400,7 @@ describe('features', () => {
const mockFeaturesPlugin = featuresPluginMock.createSetup();
mockFeaturesPlugin.getKibanaFeatures.mockReturnValue(features);
- const mockLicenseService = {
- getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
- getType: jest.fn().mockReturnValue('basic'),
- };
- const privileges = privilegesFactory(
- actions,
- mockFeaturesPlugin as any,
- mockLicenseService
- );
+ const privileges = privilegesFactory(actions, mockFeaturesPlugin, mockLicenseServiceBasic);
const actual = privileges.get();
expect(actual).toHaveProperty(`${group}.read`, [
@@ -500,15 +480,7 @@ describe('features', () => {
const mockFeaturesPlugin = featuresPluginMock.createSetup();
mockFeaturesPlugin.getKibanaFeatures.mockReturnValue(features);
- const mockLicenseService = {
- getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
- getType: jest.fn().mockReturnValue('basic'),
- };
- const privileges = privilegesFactory(
- actions,
- mockFeaturesPlugin as any,
- mockLicenseService
- );
+ const privileges = privilegesFactory(actions, mockFeaturesPlugin, mockLicenseServiceBasic);
const actual = privileges.get();
expect(actual).toHaveProperty(`${group}.all`, [
@@ -574,15 +546,7 @@ describe('features', () => {
const mockFeaturesPlugin = featuresPluginMock.createSetup();
mockFeaturesPlugin.getKibanaFeatures.mockReturnValue(features);
- const mockLicenseService = {
- getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
- getType: jest.fn().mockReturnValue('basic'),
- };
- const privileges = privilegesFactory(
- actions,
- mockFeaturesPlugin as any,
- mockLicenseService
- );
+ const privileges = privilegesFactory(actions, mockFeaturesPlugin, mockLicenseServiceBasic);
const actual = privileges.get();
expect(actual).toHaveProperty(`${group}.all`, [
@@ -649,15 +613,7 @@ describe('features', () => {
const mockFeaturesPlugin = featuresPluginMock.createSetup();
mockFeaturesPlugin.getKibanaFeatures.mockReturnValue(features);
- const mockLicenseService = {
- getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
- getType: jest.fn().mockReturnValue('basic'),
- };
- const privileges = privilegesFactory(
- actions,
- mockFeaturesPlugin as any,
- mockLicenseService
- );
+ const privileges = privilegesFactory(actions, mockFeaturesPlugin, mockLicenseServiceBasic);
const actual = privileges.get();
expect(actual).toHaveProperty(`${group}.all`, [
@@ -718,11 +674,7 @@ describe('reserved', () => {
const mockFeaturesPlugin = featuresPluginMock.createSetup();
mockFeaturesPlugin.getKibanaFeatures.mockReturnValue(features);
- const mockLicenseService = {
- getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
- getType: jest.fn().mockReturnValue('basic'),
- };
- const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
+ const privileges = privilegesFactory(actions, mockFeaturesPlugin, mockLicenseServiceBasic);
const actual = privileges.get();
expect(actual).toHaveProperty('reserved.foo', [actions.version]);
@@ -756,11 +708,7 @@ describe('reserved', () => {
const mockFeaturesPlugin = featuresPluginMock.createSetup();
mockFeaturesPlugin.getKibanaFeatures.mockReturnValue(features);
- const mockLicenseService = {
- getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
- getType: jest.fn().mockReturnValue('basic'),
- };
- const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
+ const privileges = privilegesFactory(actions, mockFeaturesPlugin, mockLicenseServiceBasic);
const actual = privileges.get();
expect(actual).toHaveProperty('reserved.foo', [
@@ -830,11 +778,7 @@ describe('reserved', () => {
const mockFeaturesPlugin = featuresPluginMock.createSetup();
mockFeaturesPlugin.getKibanaFeatures.mockReturnValue(features);
- const mockLicenseService = {
- getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
- getType: jest.fn().mockReturnValue('basic'),
- };
- const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
+ const privileges = privilegesFactory(actions, mockFeaturesPlugin, mockLicenseServiceBasic);
const actual = privileges.get();
expect(actual).not.toHaveProperty('reserved.foo');
@@ -893,11 +837,7 @@ describe('subFeatures', () => {
const mockFeaturesPlugin = featuresPluginMock.createSetup();
mockFeaturesPlugin.getKibanaFeatures.mockReturnValue(features);
- const mockLicenseService = {
- getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
- getType: jest.fn().mockReturnValue('basic'),
- };
- const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
+ const privileges = privilegesFactory(actions, mockFeaturesPlugin, mockLicenseServiceGold);
const actual = privileges.get();
expect(actual.features).toHaveProperty(`foo.subFeaturePriv1`, [
@@ -1027,11 +967,7 @@ describe('subFeatures', () => {
const mockFeaturesPlugin = featuresPluginMock.createSetup();
mockFeaturesPlugin.getKibanaFeatures.mockReturnValue(features);
- const mockLicenseService = {
- getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
- getType: jest.fn().mockReturnValue('basic'),
- };
- const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
+ const privileges = privilegesFactory(actions, mockFeaturesPlugin, mockLicenseServiceGold);
const actual = privileges.get();
expect(actual.features).toHaveProperty(`foo.subFeaturePriv1`, [
@@ -1264,11 +1200,7 @@ describe('subFeatures', () => {
const mockFeaturesPlugin = featuresPluginMock.createSetup();
mockFeaturesPlugin.getKibanaFeatures.mockReturnValue(features);
- const mockLicenseService = {
- getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
- getType: jest.fn().mockReturnValue('basic'),
- };
- const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
+ const privileges = privilegesFactory(actions, mockFeaturesPlugin, mockLicenseServiceGold);
const actual = privileges.get();
expect(actual.features).toHaveProperty(`foo.subFeaturePriv1`, [
@@ -1424,11 +1356,7 @@ describe('subFeatures', () => {
const mockFeaturesPlugin = featuresPluginMock.createSetup();
mockFeaturesPlugin.getKibanaFeatures.mockReturnValue(features);
- const mockLicenseService = {
- getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
- getType: jest.fn().mockReturnValue('basic'),
- };
- const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
+ const privileges = privilegesFactory(actions, mockFeaturesPlugin, mockLicenseServiceGold);
const actual = privileges.get();
expect(actual.features).toHaveProperty(`foo.subFeaturePriv1`, [
@@ -1610,11 +1538,7 @@ describe('subFeatures', () => {
const mockFeaturesPlugin = featuresPluginMock.createSetup();
mockFeaturesPlugin.getKibanaFeatures.mockReturnValue(features);
- const mockLicenseService = {
- getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
- getType: jest.fn().mockReturnValue('basic'),
- };
- const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
+ const privileges = privilegesFactory(actions, mockFeaturesPlugin, mockLicenseServiceGold);
const actual = privileges.get();
expect(actual.features).toHaveProperty(`foo.subFeaturePriv1`, [
@@ -1753,11 +1677,7 @@ describe('subFeatures', () => {
const mockFeaturesPlugin = featuresPluginMock.createSetup();
mockFeaturesPlugin.getKibanaFeatures.mockReturnValue(features);
- const mockLicenseService = {
- getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: false }),
- getType: jest.fn().mockReturnValue('basic'),
- };
- const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
+ const privileges = privilegesFactory(actions, mockFeaturesPlugin, mockLicenseServiceBasic);
const actual = privileges.get();
expect(actual.features).not.toHaveProperty(`foo.subFeaturePriv1`);
@@ -1979,11 +1899,7 @@ describe('subFeatures', () => {
const mockFeaturesPlugin = featuresPluginMock.createSetup();
mockFeaturesPlugin.getKibanaFeatures.mockReturnValue(features);
- const mockLicenseService = {
- getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
- getType: jest.fn().mockReturnValue('gold'),
- };
- const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
+ const privileges = privilegesFactory(actions, mockFeaturesPlugin, mockLicenseServiceGold);
const actual = privileges.get();
expect(actual.features).toHaveProperty(`foo.subFeaturePriv1`);
@@ -2214,11 +2130,7 @@ describe('subFeatures', () => {
const mockFeaturesPlugin = featuresPluginMock.createSetup();
mockFeaturesPlugin.getKibanaFeatures.mockReturnValue(features);
- const mockLicenseService = {
- getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
- getType: jest.fn().mockReturnValue('platinum'),
- };
- const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService);
+ const privileges = privilegesFactory(actions, mockFeaturesPlugin, mockLicenseServicePlatinum);
const actual = privileges.get();
expect(actual.features).toHaveProperty(`foo.subFeaturePriv1`);
diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.ts
index 9f50fd0fb1d5..c38a5c9a44f5 100644
--- a/x-pack/plugins/security/server/authorization/privileges/privileges.ts
+++ b/x-pack/plugins/security/server/authorization/privileges/privileges.ts
@@ -23,7 +23,7 @@ export interface PrivilegesService {
export function privilegesFactory(
actions: Actions,
featuresService: FeaturesPluginSetup,
- licenseService: Pick
+ licenseService: Pick
) {
const featurePrivilegeBuilder = featurePrivilegeBuilderFactory(actions);
@@ -31,7 +31,7 @@ export function privilegesFactory(
get() {
const features = featuresService.getKibanaFeatures();
const { allowSubFeaturePrivileges } = licenseService.getFeatures();
- const licenseType = licenseService.getType()!;
+ const { hasAtLeast: licenseHasAtLeast } = licenseService;
const basePrivilegeFeatures = features.filter(
(feature) => !feature.excludeFromBasePrivileges
);
@@ -42,7 +42,7 @@ export function privilegesFactory(
basePrivilegeFeatures.forEach((feature) => {
for (const { privilegeId, privilege } of featuresService.featurePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: true,
- licenseType,
+ licenseHasAtLeast,
predicate: (pId, featurePrivilege) => !featurePrivilege.excludeFromBasePrivileges,
})) {
const privilegeActions = featurePrivilegeBuilder.getActions(privilege, feature);
@@ -61,7 +61,7 @@ export function privilegesFactory(
featurePrivileges[feature.id] = {};
for (const featurePrivilege of featuresService.featurePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: true,
- licenseType,
+ licenseHasAtLeast,
})) {
featurePrivileges[feature.id][featurePrivilege.privilegeId] = [
actions.login,
@@ -73,7 +73,7 @@ export function privilegesFactory(
if (allowSubFeaturePrivileges && feature.subFeatures?.length > 0) {
for (const featurePrivilege of featuresService.featurePrivilegeIterator(feature, {
augmentWithSubFeaturePrivileges: false,
- licenseType,
+ licenseHasAtLeast,
})) {
featurePrivileges[feature.id][`minimal_${featurePrivilege.privilegeId}`] = [
actions.login,
@@ -84,7 +84,7 @@ export function privilegesFactory(
for (const subFeaturePrivilege of featuresService.subFeaturePrivilegeIterator(
feature,
- licenseType
+ licenseHasAtLeast
)) {
featurePrivileges[feature.id][subFeaturePrivilege.id] = [
actions.login,
diff --git a/x-pack/plugins/security/server/config_deprecations.test.ts b/x-pack/plugins/security/server/config_deprecations.test.ts
index d2c75fd2331b..bd8b21358e3e 100644
--- a/x-pack/plugins/security/server/config_deprecations.test.ts
+++ b/x-pack/plugins/security/server/config_deprecations.test.ts
@@ -165,6 +165,68 @@ describe('Config Deprecations', () => {
`);
});
+ it('warns when using the legacy audit logger', () => {
+ const config = {
+ xpack: {
+ security: {
+ audit: {
+ enabled: true,
+ },
+ },
+ },
+ };
+ const { messages, migrated } = applyConfigDeprecations(cloneDeep(config));
+ expect(migrated.xpack.security.audit.appender).not.toBeDefined();
+ expect(messages).toMatchInlineSnapshot(`
+ Array [
+ "The legacy audit logger is deprecated in favor of the new ECS-compliant audit logger.",
+ ]
+ `);
+ });
+
+ it('does not warn when using the ECS audit logger', () => {
+ const config = {
+ xpack: {
+ security: {
+ audit: {
+ enabled: true,
+ appender: {
+ type: 'file',
+ fileName: './audit.log',
+ },
+ },
+ },
+ },
+ };
+ const { messages, migrated } = applyConfigDeprecations(cloneDeep(config));
+ expect(migrated).toEqual(config);
+ expect(messages).toHaveLength(0);
+ });
+
+ it('does not warn about using the legacy logger when using the ECS audit logger, even when using the deprecated ECS appender config', () => {
+ const config = {
+ xpack: {
+ security: {
+ audit: {
+ enabled: true,
+ appender: {
+ type: 'file',
+ path: './audit.log',
+ },
+ },
+ },
+ },
+ };
+ const { messages, migrated } = applyConfigDeprecations(cloneDeep(config));
+ expect(migrated.xpack.security.audit.appender.path).not.toBeDefined();
+ expect(migrated.xpack.security.audit.appender.fileName).toEqual('./audit.log');
+ expect(messages).toMatchInlineSnapshot(`
+ Array [
+ "\\"xpack.security.audit.appender.path\\" is deprecated and has been replaced by \\"xpack.security.audit.appender.fileName\\"",
+ ]
+ `);
+ });
+
it(`warns that 'authorization.legacyFallback.enabled' is unused`, () => {
const config = {
xpack: {
diff --git a/x-pack/plugins/security/server/config_deprecations.ts b/x-pack/plugins/security/server/config_deprecations.ts
index 7b659ec1c350..808a192f8320 100644
--- a/x-pack/plugins/security/server/config_deprecations.ts
+++ b/x-pack/plugins/security/server/config_deprecations.ts
@@ -21,6 +21,23 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({
unused('authorization.legacyFallback.enabled'),
unused('authc.saml.maxRedirectURLSize'),
+ // Deprecation warning for the legacy audit logger.
+ (settings, fromPath, addDeprecation) => {
+ const auditLoggingEnabled = settings?.xpack?.security?.audit?.enabled ?? false;
+ const legacyAuditLoggerEnabled = !settings?.xpack?.security?.audit?.appender;
+ if (auditLoggingEnabled && legacyAuditLoggerEnabled) {
+ addDeprecation({
+ message: `The legacy audit logger is deprecated in favor of the new ECS-compliant audit logger.`,
+ documentationUrl:
+ 'https://www.elastic.co/guide/en/kibana/current/security-settings-kb.html#audit-logging-settings',
+ correctiveActions: {
+ manualSteps: [
+ `Declare an audit logger "appender" via "xpack.security.audit.appender" to enable the ECS audit logger.`,
+ ],
+ },
+ });
+ }
+ },
// Deprecation warning for the old array-based format of `xpack.security.authc.providers`.
(settings, fromPath, addDeprecation) => {
if (Array.isArray(settings?.xpack?.security?.authc?.providers)) {
diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts
index 2d17e75527c6..7df717ddefef 100644
--- a/x-pack/plugins/security/server/plugin.test.ts
+++ b/x-pack/plugins/security/server/plugin.test.ts
@@ -119,7 +119,7 @@ describe('Security Plugin', () => {
},
},
"getFeatures": [Function],
- "getType": [Function],
+ "hasAtLeast": [Function],
"isEnabled": [Function],
"isLicenseAvailable": [Function],
},
diff --git a/x-pack/plugins/security_solution/cypress/README.md b/x-pack/plugins/security_solution/cypress/README.md
index 1b486ca3a5fc..1bc4a47d6d9c 100644
--- a/x-pack/plugins/security_solution/cypress/README.md
+++ b/x-pack/plugins/security_solution/cypress/README.md
@@ -27,6 +27,77 @@ This is the configuration used by CI. It uses the FTR to spawn both a Kibana ins
This configuration runs cypress tests against an arbitrary host.
**WARNING**: When using your own instances you need to take into account that if you already have data on it, the tests may fail, as well as, they can put your instances in an undesired state, since our tests uses es_archive to populate data.
+#### integration-test (CI)
+
+This configuration is driven by [elastic/integration-test](https://github.com/elastic/integration-test) which, as part of a bigger set of tests, provisions one VM with two instances configured in CCS mode and runs the [CCS Cypress test specs](./ccs_integration).
+
+The two clusters are named `admin` and `data` and are reachable as follows:
+
+| | Elasticsearch | Kibana |
+|-------|------------------------|------------------------|
+| admin | https://localhost:9200 | https://localhost:5601 |
+| data | https://localhost:9210 | https://localhost:5602 |
+
+### Working with integration-test
+
+#### Initial setup and prerequisites
+
+The entry point is [integration-test/jenkins_test.sh](https://github.com/elastic/integration-test/blob/master/jenkins_test.sh), it essentially prepares the VMs and there runs tests. Some snapshots (`phase1` and `phase2`) are taken along the way so that it's possible to short cut the VM preparation when iterating over tests for development or debugging.
+
+The VMs are managed with Vagrant using the VirtualBox provider therefore you need to install both these tools. The host OS can be either Windows, Linux or MacOS.
+
+`jenkins_test.sh` assumes that a `kibana` folder is present alongside the `integration-test` where it's executed from. The `kibana` folder is used only for loading the test suites though, the actual packages for the VMs preparation are downloaded from elastic.co according to the `BUILD` environment variable or the branch which `jenkins_test.sh` is invoked from. It's your responsibility to checkout the matching branches in `kibana` and `integration-test` as needed.
+
+Read [integration-test#readme](https://github.com/elastic/integration-test#readme) for further details.
+
+#### Use cases
+
+There is no way to just set up the test environment without also executing tests at least once. On the other hand it's time consuming to go throught the whole CI procedure to just iterate over the tests therefore the following instructions support the two use cases:
+
+* reproduce e2e the CI execution locally, ie. for debugging a CI failure
+* use the CI script to easily setup the environment for tests development/debugging
+
+The missing use case, application TDD, requires a different solution that runs from the checked out repositories instead of the pre-built packages and it's yet to be developed.
+
+#### Run the CI flow
+
+This is the CI flow narrowed down to the execution of CCS Cypress tests:
+
+```shell
+cd integration-test
+VMS=ubuntu16_tar_ccs_cypress ./jenkins_test.sh
+```
+
+It destroys and rebuilds the VM. There installs, provisions and starts the stack according to the configuration in [integration-test/provision/ubuntu16_tar_ccs_cypress.sh](https://github.com/elastic/integration-test/blob/master/provision/ubuntu16_tar_ccs_cypress.sh).
+
+The tests are executed using the FTR runner `SecuritySolutionCypressCcsTestRunner` defined in [x-pack/test/security_solution_cypress/runner.ts](../../../test/security_solution_cypress/runner.ts) as configured in [x-pack/test/security_solution_cypress/ccs_config.ts](../../../test/security_solution_cypress/ccs_config.ts).
+
+#### Re-run the tests
+
+After the first run it's possible to restore the VM at `phase2`, right before tests were executed, and run them again:
+
+```shell
+cd integration-test
+MODE=retest ./jenkins_test.sh
+```
+
+It remembers which VM the first round was executed on, you don't need to specify `VMS` any more.
+
+In case your tests are cleaning after themselves and therefore result idempotent, you can skip the restoration to `phase2` and directly run the Cypress command line. See [CCS Custom Target + Headless](#ccs-custom-target--headless) further below for details but ensure you'll define the `CYPRESS_*` following the correspondence:
+
+| Cypress command line | [integration-test/provision/ubuntu16_tar_ccs_cypress.sh](https://github.com/elastic/integration-test/blob/master/provision/ubuntu16_tar_ccs_cypress.sh) |
+|--------------------------------|----------------------------------|
+| CYPRESS_BASE_URL | TEST_KIBANA_URL |
+| CYPRESS_ELASTICSEARCH_URL | TEST_ES_URL |
+| CYPRESS_CCS_KIBANA_URL | TEST_KIBANA_URLDATA |
+| CYPRESS_CCS_ELASTICSEARCH_URL | TEST_ES_URLDATA |
+| CYPRESS_CCS_REMOTE_NAME | TEST_CCS_REMOTE_NAME |
+| CYPRESS_ELASTICSEARCH_USERNAME | ELASTICSEARCH_USERNAME |
+| CYPRESS_ELASTICSEARCH_PASSWORD | ELASTICSEARCH_PASSWORD |
+| TEST_CA_CERT_PATH | integration-test/certs/ca/ca.crt |
+
+Note: `TEST_CA_CERT_PATH` above is truly without `CYPRESS_` prefix.
+
### Test Execution: Examples
#### FTR + Headless (Chrome)
@@ -149,7 +220,7 @@ Appending `--browser firefox` to the `yarn cypress:run:ccs` command above will r
### ccs_integration/
-Contains the specs that are executed in a Cross Cluster Search configuration, typically during integration tests.
+Contains the specs that are executed in a Cross Cluster Search configuration.
### integration/
diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json
index b5650e0f7688..990756f3da70 100644
--- a/x-pack/plugins/security_solution/kibana.json
+++ b/x-pack/plugins/security_solution/kibana.json
@@ -32,7 +32,8 @@
"usageCollection",
"lists",
"home",
- "telemetry"
+ "telemetry",
+ "telemetryManagementSection"
],
"server": true,
"ui": true,
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/no_enrichments_panel.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/no_enrichments_panel.test.tsx
index 819c666bd726..6e077f068a0d 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/no_enrichments_panel.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/no_enrichments_panel.test.tsx
@@ -16,21 +16,30 @@ jest.mock('../../../lib/kibana');
describe('NoEnrichmentsPanelView', () => {
it('renders a qualified container', () => {
const wrapper = mount(
-
+
);
expect(wrapper.find('[data-test-subj="no-enrichments-panel"]').exists()).toEqual(true);
});
it('renders nothing when all enrichments are present', () => {
const wrapper = mount(
-
+
);
expect(wrapper.find('[data-test-subj="no-enrichments-panel"]').exists()).toEqual(false);
});
it('renders expected text when no enrichments are present', () => {
const wrapper = mount(
-
+
);
expect(wrapper.find('[data-test-subj="no-enrichments-panel"]').hostNodes().text()).toContain(
i18n.NO_ENRICHMENTS_FOUND_TITLE
@@ -39,7 +48,10 @@ describe('NoEnrichmentsPanelView', () => {
it('renders expected text when existing enrichments are absent', () => {
const wrapper = mount(
-
+
);
expect(wrapper.find('[data-test-subj="no-enrichments-panel"]').hostNodes().text()).toContain(
i18n.NO_INDICATOR_ENRICHMENTS_TITLE
@@ -48,7 +60,10 @@ describe('NoEnrichmentsPanelView', () => {
it('renders expected text when investigation enrichments are absent', () => {
const wrapper = mount(
-
+
);
expect(wrapper.find('[data-test-subj="no-enrichments-panel"]').hostNodes().text()).toContain(
i18n.NO_INVESTIGATION_ENRICHMENTS_TITLE
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/no_enrichments_panel.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/no_enrichments_panel.tsx
index a26e74d12f40..1c419372aea3 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/no_enrichments_panel.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/no_enrichments_panel.tsx
@@ -35,9 +35,9 @@ const NoEnrichmentsPanelView: React.FC<{
NoEnrichmentsPanelView.displayName = 'NoEnrichmentsPanelView';
export const NoEnrichmentsPanel: React.FC<{
- existingEnrichmentsCount: number;
- investigationEnrichmentsCount: number;
-}> = ({ existingEnrichmentsCount, investigationEnrichmentsCount }) => {
+ isIndicatorMatchesPresent: boolean;
+ isInvestigationTimeEnrichmentsPresent: boolean;
+}> = ({ isIndicatorMatchesPresent, isInvestigationTimeEnrichmentsPresent }) => {
const threatIntelDocsUrl = `${
useKibana().services.docLinks.links.filebeat.base
}/filebeat-module-threatintel.html`;
@@ -50,7 +50,7 @@ export const NoEnrichmentsPanel: React.FC<{
>
);
- if (existingEnrichmentsCount === 0 && investigationEnrichmentsCount === 0) {
+ if (!isIndicatorMatchesPresent && !isInvestigationTimeEnrichmentsPresent) {
return (
{i18n.NO_ENRICHMENTS_FOUND_TITLE}
}
@@ -61,7 +61,7 @@ export const NoEnrichmentsPanel: React.FC<{
}
/>
);
- } else if (existingEnrichmentsCount === 0) {
+ } else if (!isIndicatorMatchesPresent) {
return (
<>
@@ -75,7 +75,7 @@ export const NoEnrichmentsPanel: React.FC<{
/>
>
);
- } else if (investigationEnrichmentsCount === 0) {
+ } else if (!isInvestigationTimeEnrichmentsPresent) {
return (
<>
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx
index 7074212dcdb4..668c6ffb723a 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx
@@ -113,15 +113,14 @@ const EventDetailsComponent: React.FC = ({
loading: enrichmentsLoading,
result: enrichmentsResponse,
} = useInvestigationTimeEnrichment(eventFields);
- const investigationEnrichments = useMemo(() => enrichmentsResponse?.enrichments ?? [], [
- enrichmentsResponse?.enrichments,
- ]);
+
const allEnrichments = useMemo(() => {
if (enrichmentsLoading || !enrichmentsResponse?.enrichments) {
return existingEnrichments;
}
return filterDuplicateEnrichments([...existingEnrichments, ...enrichmentsResponse.enrichments]);
}, [enrichmentsLoading, enrichmentsResponse, existingEnrichments]);
+
const enrichmentCount = allEnrichments.length;
const summaryTab: EventViewTab | undefined = useMemo(
@@ -184,21 +183,16 @@ const EventDetailsComponent: React.FC = ({
<>
existingEnrichments.length
+ }
+ isIndicatorMatchesPresent={existingEnrichments.length > 0}
/>
>
),
}
: undefined,
- [
- allEnrichments,
- enrichmentCount,
- enrichmentsLoading,
- existingEnrichments.length,
- investigationEnrichments.length,
- isAlert,
- ]
+ [allEnrichments, enrichmentCount, enrichmentsLoading, existingEnrichments.length, isAlert]
);
const tableTab = useMemo(
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx
index 32eb4baad505..bf6a94c53b47 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx
@@ -26,8 +26,9 @@ import {
getProcessCodeSignature,
retrieveAlertOsTypes,
filterIndexPatterns,
+ getCodeSignatureValue,
} from './helpers';
-import { AlertData } from './types';
+import { AlertData, Flattened } from './types';
import {
ListOperatorTypeEnum as OperatorTypeEnum,
EntriesArray,
@@ -41,6 +42,7 @@ import { getCommentsArrayMock } from '../../../../../lists/common/schemas/types/
import { fields } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
import { ENTRIES, OLD_DATE_RELATIVE_TO_DATE_NOW } from '../../../../../lists/common/constants.mock';
import { IFieldType, IIndexPattern } from 'src/plugins/data/common';
+import { CodeSignature } from '../../../../common/ecs/file';
jest.mock('uuid', () => ({
v4: jest.fn().mockReturnValue('123'),
@@ -340,6 +342,17 @@ describe('Exception helpers', () => {
});
});
+ describe('#getCodeSignatureValue', () => {
+ test('it should return empty string if code_signature nested value are undefined', () => {
+ // Using the unsafe casting because with our types this shouldn't be possible but there have been issues with old data having undefined values in these fields
+ const payload = ([{ trusted: undefined, subject_name: undefined }] as unknown) as Flattened<
+ CodeSignature[]
+ >;
+ const result = getCodeSignatureValue(payload);
+ expect(result).toEqual([{ trusted: '', subjectName: '' }]);
+ });
+ });
+
describe('#entryHasNonEcsType', () => {
const mockEcsIndexPattern = {
title: 'testIndex',
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx
index 3c8652637a99..f8260062f697 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx
@@ -325,8 +325,8 @@ export const getCodeSignatureValue = (
if (Array.isArray(codeSignature) && codeSignature.length > 0) {
return codeSignature.map((signature) => {
return {
- subjectName: signature.subject_name ?? '',
- trusted: signature.trusted.toString() ?? '',
+ subjectName: signature?.subject_name ?? '',
+ trusted: signature?.trusted?.toString() ?? '',
};
});
} else {
diff --git a/x-pack/plugins/security_solution/public/common/components/hover_actions/actions/show_top_n.tsx b/x-pack/plugins/security_solution/public/common/components/hover_actions/actions/show_top_n.tsx
new file mode 100644
index 000000000000..fba9dd734600
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/hover_actions/actions/show_top_n.tsx
@@ -0,0 +1,85 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { StatefulTopN } from '../../top_n';
+import { TimelineId } from '../../../../../common/types/timeline';
+import { SourcererScopeName } from '../../../store/sourcerer/model';
+import { useSourcererScope } from '../../../containers/sourcerer';
+import { TooltipWithKeyboardShortcut } from '../../accessibility';
+import { getAdditionalScreenReaderOnlyContext } from '../utils';
+import { SHOW_TOP_N_KEYBOARD_SHORTCUT } from '../keyboard_shortcut_constants';
+
+const SHOW_TOP = (fieldName: string) =>
+ i18n.translate('xpack.securitySolution.hoverActions.showTopTooltip', {
+ values: { fieldName },
+ defaultMessage: `Show top {fieldName}`,
+ });
+
+interface Props {
+ field: string;
+ onClick: () => void;
+ onFilterAdded?: () => void;
+ ownFocus: boolean;
+ showTopN: boolean;
+ timelineId?: string | null;
+ value?: string[] | string | null;
+}
+
+export const ShowTopNButton: React.FC = React.memo(
+ ({ field, onClick, onFilterAdded, ownFocus, showTopN, timelineId, value }) => {
+ const activeScope: SourcererScopeName =
+ timelineId === TimelineId.active
+ ? SourcererScopeName.timeline
+ : timelineId != null &&
+ [TimelineId.detectionsPage, TimelineId.detectionsRulesDetailsPage].includes(
+ timelineId as TimelineId
+ )
+ ? SourcererScopeName.detections
+ : SourcererScopeName.default;
+ const { browserFields, indexPattern } = useSourcererScope(activeScope);
+
+ return showTopN ? (
+
+ ) : (
+
+ }
+ >
+
+
+ );
+ }
+);
+
+ShowTopNButton.displayName = 'ShowTopNButton';
diff --git a/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx b/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx
new file mode 100644
index 000000000000..bd5d78fd4e85
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx
@@ -0,0 +1,349 @@
+/*
+ * 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 { EuiFocusTrap, EuiScreenReaderOnly } from '@elastic/eui';
+import React, { useCallback, useEffect, useRef, useMemo } from 'react';
+import { DraggableId } from 'react-beautiful-dnd';
+import styled from 'styled-components';
+import { i18n } from '@kbn/i18n';
+import { getAllFieldsByName } from '../../containers/source';
+import { COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME } from '../../lib/clipboard/clipboard';
+import { useKibana } from '../../lib/kibana';
+import { allowTopN } from './utils';
+import { useDeepEqualSelector } from '../../hooks/use_selector';
+import { ColumnHeaderOptions, TimelineId } from '../../../../common/types/timeline';
+import { SourcererScopeName } from '../../store/sourcerer/model';
+import { useSourcererScope } from '../../containers/sourcerer';
+import { timelineSelectors } from '../../../timelines/store/timeline';
+import { stopPropagationAndPreventDefault } from '../../../../../timelines/public';
+import { ShowTopNButton } from './actions/show_top_n';
+import { SHOW_TOP_N_KEYBOARD_SHORTCUT } from './keyboard_shortcut_constants';
+
+export const YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS = (fieldName: string) =>
+ i18n.translate(
+ 'xpack.securitySolution.dragAndDrop.youAreInADialogContainingOptionsScreenReaderOnly',
+ {
+ values: { fieldName },
+ defaultMessage: `You are in a dialog, containing options for field {fieldName}. Press tab to navigate options. Press escape to exit.`,
+ }
+ );
+
+export const AdditionalContent = styled.div`
+ padding: 2px;
+`;
+
+AdditionalContent.displayName = 'AdditionalContent';
+
+const StyledHoverActionsContainer = styled.div<{ $showTopN: boolean }>`
+ padding: ${(props) => (props.$showTopN ? 'none' : props.theme.eui.paddingSizes.s)};
+
+ &:focus-within {
+ .timelines__hoverActionButton,
+ .securitySolution__hoverActionButton {
+ opacity: 1;
+ }
+ }
+
+ &:hover {
+ .timelines__hoverActionButton,
+ .securitySolution__hoverActionButton {
+ opacity: 1;
+ }
+ }
+
+ .timelines__hoverActionButton,
+ .securitySolution__hoverActionButton {
+ // TODO: Using this logic from discover
+ /* @include euiBreakpoint('m', 'l', 'xl') {
+ opacity: 0;
+ } */
+ opacity: 0;
+
+ &:focus {
+ opacity: 1;
+ }
+ }
+`;
+
+interface Props {
+ additionalContent?: React.ReactNode;
+ dataType?: string;
+ draggableId?: DraggableId;
+ field: string;
+ isObjectArray: boolean;
+ goGetTimelineId?: (args: boolean) => void;
+ onFilterAdded?: () => void;
+ ownFocus: boolean;
+ showTopN: boolean;
+ timelineId?: string | null;
+ toggleColumn?: (column: ColumnHeaderOptions) => void;
+ toggleTopN: () => void;
+ value?: string[] | string | null;
+}
+
+/** Returns a value for the `disabled` prop of `EuiFocusTrap` */
+const isFocusTrapDisabled = ({
+ ownFocus,
+ showTopN,
+}: {
+ ownFocus: boolean;
+ showTopN: boolean;
+}): boolean => {
+ if (showTopN) {
+ return false; // we *always* want to trap focus when showing Top N
+ }
+
+ return !ownFocus;
+};
+
+export const HoverActions: React.FC = React.memo(
+ ({
+ additionalContent = null,
+ dataType,
+ draggableId,
+ field,
+ goGetTimelineId,
+ isObjectArray,
+ onFilterAdded,
+ ownFocus,
+ showTopN,
+ timelineId,
+ toggleColumn,
+ toggleTopN,
+ value,
+ }) => {
+ const kibana = useKibana();
+ const { timelines } = kibana.services;
+ // Common actions used by the alert table and alert flyout
+ const {
+ addToTimeline: {
+ AddToTimelineButton,
+ keyboardShortcut: addToTimelineKeyboardShortcut,
+ useGetHandleStartDragToTimeline,
+ },
+ columnToggle: {
+ ColumnToggleButton,
+ columnToggleFn,
+ keyboardShortcut: columnToggleKeyboardShortcut,
+ },
+ copy: { CopyButton, keyboardShortcut: copyKeyboardShortcut },
+ filterForValue: {
+ FilterForValueButton,
+ filterForValueFn,
+ keyboardShortcut: filterForValueKeyboardShortcut,
+ },
+ filterOutValue: {
+ FilterOutValueButton,
+ filterOutValueFn,
+ keyboardShortcut: filterOutValueKeyboardShortcut,
+ },
+ } = timelines.getHoverActions();
+
+ const filterManagerBackup = useMemo(() => kibana.services.data.query.filterManager, [
+ kibana.services.data.query.filterManager,
+ ]);
+ const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
+ const { filterManager: activeFilterMananager } = useDeepEqualSelector((state) =>
+ getManageTimeline(state, timelineId ?? '')
+ );
+ const filterManager = useMemo(
+ () => (timelineId === TimelineId.active ? activeFilterMananager : filterManagerBackup),
+ [timelineId, activeFilterMananager, filterManagerBackup]
+ );
+
+ // Regarding data from useManageTimeline:
+ // * `indexToAdd`, which enables the alerts index to be appended to
+ // the `indexPattern` returned by `useWithSource`, may only be populated when
+ // this component is rendered in the context of the active timeline. This
+ // behavior enables the 'All events' view by appending the alerts index
+ // to the index pattern.
+ const activeScope: SourcererScopeName =
+ timelineId === TimelineId.active
+ ? SourcererScopeName.timeline
+ : timelineId != null &&
+ [TimelineId.detectionsPage, TimelineId.detectionsRulesDetailsPage].includes(
+ timelineId as TimelineId
+ )
+ ? SourcererScopeName.detections
+ : SourcererScopeName.default;
+ const { browserFields } = useSourcererScope(activeScope);
+
+ const handleStartDragToTimeline = useGetHandleStartDragToTimeline({ draggableId, field });
+
+ const handleFilterForValue = useCallback(
+ () => filterForValueFn({ field, value, filterManager, onFilterAdded }),
+ [filterForValueFn, field, value, filterManager, onFilterAdded]
+ );
+
+ const handleFilterOutValue = useCallback(
+ () => filterOutValueFn({ field, value, filterManager, onFilterAdded }),
+ [filterOutValueFn, field, value, filterManager, onFilterAdded]
+ );
+
+ const handleToggleColumn = useCallback(
+ () => (toggleColumn ? columnToggleFn({ toggleColumn, field }) : null),
+ [columnToggleFn, field, toggleColumn]
+ );
+
+ const isInit = useRef(true);
+ const defaultFocusedButtonRef = useRef(null);
+ const panelRef = useRef(null);
+
+ useEffect(() => {
+ if (isInit.current && goGetTimelineId != null && timelineId == null) {
+ isInit.current = false;
+ goGetTimelineId(true);
+ }
+ }, [goGetTimelineId, timelineId]);
+
+ useEffect(() => {
+ if (ownFocus) {
+ setTimeout(() => {
+ defaultFocusedButtonRef.current?.focus();
+ }, 0);
+ }
+ }, [ownFocus]);
+
+ const onKeyDown = useCallback(
+ (keyboardEvent: React.KeyboardEvent) => {
+ if (!ownFocus) {
+ return;
+ }
+ switch (keyboardEvent.key) {
+ case addToTimelineKeyboardShortcut:
+ stopPropagationAndPreventDefault(keyboardEvent);
+ handleStartDragToTimeline();
+ break;
+ case columnToggleKeyboardShortcut:
+ stopPropagationAndPreventDefault(keyboardEvent);
+ handleToggleColumn();
+ break;
+ case copyKeyboardShortcut:
+ stopPropagationAndPreventDefault(keyboardEvent);
+ const copyToClipboardButton = panelRef.current?.querySelector(
+ `.${COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME}`
+ );
+ if (copyToClipboardButton != null) {
+ copyToClipboardButton.click();
+ }
+ break;
+ case filterForValueKeyboardShortcut:
+ stopPropagationAndPreventDefault(keyboardEvent);
+ handleFilterForValue();
+ break;
+ case filterOutValueKeyboardShortcut:
+ stopPropagationAndPreventDefault(keyboardEvent);
+ handleFilterOutValue();
+ break;
+ case SHOW_TOP_N_KEYBOARD_SHORTCUT:
+ stopPropagationAndPreventDefault(keyboardEvent);
+ toggleTopN();
+ break;
+ case 'Enter':
+ break;
+ case 'Escape':
+ stopPropagationAndPreventDefault(keyboardEvent);
+ break;
+ default:
+ break;
+ }
+ },
+
+ [
+ addToTimelineKeyboardShortcut,
+ columnToggleKeyboardShortcut,
+ copyKeyboardShortcut,
+ filterForValueKeyboardShortcut,
+ filterOutValueKeyboardShortcut,
+ handleFilterForValue,
+ handleFilterOutValue,
+ handleStartDragToTimeline,
+ handleToggleColumn,
+ ownFocus,
+ toggleTopN,
+ ]
+ );
+
+ const showFilters = !showTopN && value != null;
+
+ return (
+
+
+
+ {YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS(field)}
+
+
+ {additionalContent != null && {additionalContent}}
+
+ {showFilters && (
+ <>
+
+
+ >
+ )}
+ {toggleColumn && (
+
+ )}
+
+ {showFilters && draggableId != null && (
+
+ )}
+ {allowTopN({
+ browserField: getAllFieldsByName(browserFields)[field],
+ fieldName: field,
+ }) && (
+
+ )}
+ {!showTopN && (
+
+ )}
+
+
+ );
+ }
+);
+
+HoverActions.displayName = 'HoverActions';
diff --git a/x-pack/plugins/security_solution/public/common/components/hover_actions/keyboard_shortcut_constants.ts b/x-pack/plugins/security_solution/public/common/components/hover_actions/keyboard_shortcut_constants.ts
new file mode 100644
index 000000000000..cef4c896e39f
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/hover_actions/keyboard_shortcut_constants.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export const SHOW_TOP_N_KEYBOARD_SHORTCUT = 't';
diff --git a/x-pack/plugins/security_solution/public/common/components/hover_actions/utils.ts b/x-pack/plugins/security_solution/public/common/components/hover_actions/utils.ts
new file mode 100644
index 000000000000..9a6b0838c668
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/hover_actions/utils.ts
@@ -0,0 +1,117 @@
+/*
+ * 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 { BrowserField } from '../../containers/source';
+
+export const getAdditionalScreenReaderOnlyContext = ({
+ field,
+ value,
+}: {
+ field: string;
+ value?: string[] | string | null;
+}): string => {
+ if (value == null) {
+ return field;
+ }
+
+ return Array.isArray(value) ? `${field} ${value.join(' ')}` : `${field} ${value}`;
+};
+
+export const allowTopN = ({
+ browserField,
+ fieldName,
+}: {
+ browserField: Partial | undefined;
+ fieldName: string;
+}): boolean => {
+ const isAggregatable = browserField?.aggregatable ?? false;
+ const fieldType = browserField?.type ?? '';
+ const isAllowedType = [
+ 'boolean',
+ 'geo-point',
+ 'geo-shape',
+ 'ip',
+ 'keyword',
+ 'number',
+ 'numeric',
+ 'string',
+ ].includes(fieldType);
+
+ // TODO: remove this explicit allowlist when the ECS documentation includes alerts
+ const isAllowlistedNonBrowserField = [
+ 'signal.ancestors.depth',
+ 'signal.ancestors.id',
+ 'signal.ancestors.rule',
+ 'signal.ancestors.type',
+ 'signal.original_event.action',
+ 'signal.original_event.category',
+ 'signal.original_event.code',
+ 'signal.original_event.created',
+ 'signal.original_event.dataset',
+ 'signal.original_event.duration',
+ 'signal.original_event.end',
+ 'signal.original_event.hash',
+ 'signal.original_event.id',
+ 'signal.original_event.kind',
+ 'signal.original_event.module',
+ 'signal.original_event.original',
+ 'signal.original_event.outcome',
+ 'signal.original_event.provider',
+ 'signal.original_event.risk_score',
+ 'signal.original_event.risk_score_norm',
+ 'signal.original_event.sequence',
+ 'signal.original_event.severity',
+ 'signal.original_event.start',
+ 'signal.original_event.timezone',
+ 'signal.original_event.type',
+ 'signal.original_time',
+ 'signal.parent.depth',
+ 'signal.parent.id',
+ 'signal.parent.index',
+ 'signal.parent.rule',
+ 'signal.parent.type',
+ 'signal.rule.created_by',
+ 'signal.rule.description',
+ 'signal.rule.enabled',
+ 'signal.rule.false_positives',
+ 'signal.rule.filters',
+ 'signal.rule.from',
+ 'signal.rule.id',
+ 'signal.rule.immutable',
+ 'signal.rule.index',
+ 'signal.rule.interval',
+ 'signal.rule.language',
+ 'signal.rule.max_signals',
+ 'signal.rule.name',
+ 'signal.rule.note',
+ 'signal.rule.output_index',
+ 'signal.rule.query',
+ 'signal.rule.references',
+ 'signal.rule.risk_score',
+ 'signal.rule.rule_id',
+ 'signal.rule.saved_id',
+ 'signal.rule.severity',
+ 'signal.rule.size',
+ 'signal.rule.tags',
+ 'signal.rule.threat',
+ 'signal.rule.threat.tactic.id',
+ 'signal.rule.threat.tactic.name',
+ 'signal.rule.threat.tactic.reference',
+ 'signal.rule.threat.technique.id',
+ 'signal.rule.threat.technique.name',
+ 'signal.rule.threat.technique.reference',
+ 'signal.rule.timeline_id',
+ 'signal.rule.timeline_title',
+ 'signal.rule.to',
+ 'signal.rule.type',
+ 'signal.rule.updated_by',
+ 'signal.rule.version',
+ 'signal.status',
+ ].includes(fieldName);
+
+ return isAllowlistedNonBrowserField || (isAggregatable && isAllowedType);
+};
diff --git a/x-pack/plugins/security_solution/public/common/lib/clipboard/clipboard.tsx b/x-pack/plugins/security_solution/public/common/lib/clipboard/clipboard.tsx
index 678a0586f3c0..d213cb7ab873 100644
--- a/x-pack/plugins/security_solution/public/common/lib/clipboard/clipboard.tsx
+++ b/x-pack/plugins/security_solution/public/common/lib/clipboard/clipboard.tsx
@@ -6,6 +6,7 @@
*/
import { EuiButtonIcon } from '@elastic/eui';
+import classNames from 'classnames';
import copy from 'copy-to-clipboard';
import React from 'react';
@@ -25,12 +26,20 @@ export type OnCopy = ({
interface Props {
children?: JSX.Element;
content: string | number;
+ isHoverAction?: boolean;
onCopy?: OnCopy;
titleSummary?: string;
toastLifeTimeMs?: number;
}
-export const Clipboard = ({ children, content, onCopy, titleSummary, toastLifeTimeMs }: Props) => {
+export const Clipboard = ({
+ children,
+ content,
+ isHoverAction,
+ onCopy,
+ titleSummary,
+ toastLifeTimeMs,
+}: Props) => {
const { addSuccess } = useAppToasts();
const onClick = (event: React.MouseEvent) => {
event.preventDefault();
@@ -47,11 +56,15 @@ export const Clipboard = ({ children, content, onCopy, titleSummary, toastLifeTi
}
};
+ const className = classNames(COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME, {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ securitySolution__hoverActionButton: isHoverAction,
+ });
+
return (
(({ keyboardShortcut = '', text, titleSummary }) => (
+}>(({ isHoverAction, keyboardShortcut = '', text, titleSummary }) => (
}
>
-
+
));
diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts
index 5d6744de9dbe..4dbcd515db4c 100644
--- a/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts
@@ -27,9 +27,14 @@ export const track: TrackFn = (type, event, count) => {
};
export const initTelemetry = (
- { usageCollection }: Pick,
+ {
+ usageCollection,
+ telemetryManagementSection,
+ }: Pick,
appId: string
) => {
+ telemetryManagementSection?.toggleSecuritySolutionExample(true);
+
_track = usageCollection?.reportUiCounter?.bind(null, appId) ?? noop;
};
diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx
index da5216be7db3..38c352c43b0d 100644
--- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx
@@ -41,7 +41,12 @@ export const CtiDisabledModuleComponent = () => {
);
return (
-
+
);
};
diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.test.tsx
index 9714c28cc58c..ffd0c8e69e76 100644
--- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.test.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.test.tsx
@@ -144,4 +144,32 @@ describe('ThreatIntelPanelView', () => {
`Showing: ${mockThreatIntelPanelViewProps.totalEventCount} indicators`
);
});
+
+ it('renders inspect button by default', () => {
+ const wrapper = mount(
+
+
+
+
+
+
+
+ );
+
+ expect(wrapper.exists('[data-test-subj="inspect-icon-button"]')).toBe(true);
+ });
+
+ it('does not render inspect button if isInspectEnabled is false', () => {
+ const wrapper = mount(
+
+
+
+
+
+
+
+ );
+
+ expect(wrapper.exists('[data-test-subj="inspect-icon-button"]')).toBe(false);
+ });
});
diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx
index 2add03788eea..6bd7bef20fcb 100644
--- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx
@@ -55,6 +55,7 @@ const RightSideLink = styled(EuiLink)`
interface ThreatIntelPanelViewProps {
buttonHref?: string;
isDashboardPluginDisabled?: boolean;
+ isInspectEnabled?: boolean;
listItems: CtiListItem[];
splitPanel?: JSX.Element;
totalEventCount: number;
@@ -77,6 +78,7 @@ const panelTitle = (
export const ThreatIntelPanelView: React.FC = ({
buttonHref = '',
isDashboardPluginDisabled,
+ isInspectEnabled = true,
listItems,
splitPanel,
totalEventCount,
@@ -142,7 +144,11 @@ export const ThreatIntelPanelView: React.FC = ({
-
+
<>{button}>
{splitPanel}
diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx
index a7cb9ee68891..137fef164150 100644
--- a/x-pack/plugins/security_solution/public/plugin.tsx
+++ b/x-pack/plugins/security_solution/public/plugin.tsx
@@ -91,6 +91,7 @@ export class Plugin implements IPlugin void;
+
+interface Props {
+ children?: JSX.Element;
+ content: string | number;
+ isHoverAction?: boolean;
+ onCopy?: OnCopy;
+ titleSummary?: string;
+ toastLifeTimeMs?: number;
+}
+
+export const Clipboard = ({
+ children,
+ content,
+ isHoverAction,
+ onCopy,
+ titleSummary,
+ toastLifeTimeMs,
+}: Props) => {
+ const { addSuccess } = useAppToasts();
+ const onClick = (event: React.MouseEvent) => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const isSuccess = copy(`${content}`, { debug: true });
+
+ if (onCopy != null) {
+ onCopy({ content, isSuccess });
+ }
+
+ if (isSuccess) {
+ addSuccess(`${i18n.COPIED} ${titleSummary} ${i18n.TO_THE_CLIPBOARD}`, { toastLifeTimeMs });
+ }
+ };
+
+ const className = classNames(COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME, {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ securitySolution__hoverActionButton: isHoverAction,
+ });
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/x-pack/plugins/timelines/public/components/clipboard/translations.ts b/x-pack/plugins/timelines/public/components/clipboard/translations.ts
new file mode 100644
index 000000000000..a92c9656f3cf
--- /dev/null
+++ b/x-pack/plugins/timelines/public/components/clipboard/translations.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const COPY = i18n.translate('xpack.timelines.clipboard.copy', {
+ defaultMessage: 'Copy',
+});
+
+export const COPIED = i18n.translate('xpack.timelines.clipboard.copied', {
+ defaultMessage: 'Copied',
+});
+
+export const TO_THE_CLIPBOARD = i18n.translate('xpack.timelines.clipboard.to.the.clipboard', {
+ defaultMessage: 'to the clipboard',
+});
+
+export const COPY_TO_THE_CLIPBOARD = i18n.translate(
+ 'xpack.timelines.clipboard.copy.to.the.clipboard',
+ {
+ defaultMessage: 'Copy to the clipboard',
+ }
+);
diff --git a/x-pack/plugins/timelines/public/components/clipboard/with_copy_to_clipboard.tsx b/x-pack/plugins/timelines/public/components/clipboard/with_copy_to_clipboard.tsx
new file mode 100644
index 000000000000..a62f52c27cf7
--- /dev/null
+++ b/x-pack/plugins/timelines/public/components/clipboard/with_copy_to_clipboard.tsx
@@ -0,0 +1,58 @@
+/*
+ * 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 { EuiToolTip } from '@elastic/eui';
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { TooltipWithKeyboardShortcut } from '../tooltip_with_keyboard_shortcut';
+import { Clipboard } from '.';
+
+export const COPY_TO_CLIPBOARD = i18n.translate('xpack.timelines.copyToClipboardTooltip', {
+ defaultMessage: 'Copy to Clipboard',
+});
+
+/**
+ * Renders `children` with an adjacent icon that when clicked, copies `text` to
+ * the clipboard and displays a confirmation toast
+ */
+export const WithCopyToClipboard = React.memo<{
+ isHoverAction?: boolean;
+ keyboardShortcut?: string;
+ showTooltip?: boolean;
+ text: string;
+ titleSummary?: string;
+}>(({ isHoverAction, keyboardShortcut = '', showTooltip = false, text, titleSummary }) => {
+ return showTooltip ? (
+
+ }
+ >
+
+
+ ) : (
+
+ );
+});
+
+WithCopyToClipboard.displayName = 'WithCopyToClipboard';
diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx
new file mode 100644
index 000000000000..eb9c95f0998c
--- /dev/null
+++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx
@@ -0,0 +1,82 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useCallback } from 'react';
+import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { DraggableId } from 'react-beautiful-dnd';
+import { TooltipWithKeyboardShortcut } from '../../tooltip_with_keyboard_shortcut';
+import { getAdditionalScreenReaderOnlyContext } from '../utils';
+import { useAddToTimeline } from '../../../hooks/use_add_to_timeline';
+import { HoverActionComponentProps } from './types';
+
+const ADD_TO_TIMELINE = i18n.translate('xpack.timelines.hoverActions.addToTimeline', {
+ defaultMessage: 'Add to timeline investigation',
+});
+
+export const ADD_TO_TIMELINE_KEYBOARD_SHORTCUT = 'a';
+
+export interface UseGetHandleStartDragToTimelineArgs {
+ field: string;
+ draggableId: DraggableId | undefined;
+}
+
+export const useGetHandleStartDragToTimeline = ({
+ field,
+ draggableId,
+}: UseGetHandleStartDragToTimelineArgs): (() => void) => {
+ const { startDragToTimeline } = useAddToTimeline({
+ draggableId,
+ fieldName: field,
+ });
+
+ const handleStartDragToTimeline = useCallback(() => {
+ startDragToTimeline();
+ }, [startDragToTimeline]);
+
+ return handleStartDragToTimeline;
+};
+
+export const AddToTimelineButton: React.FC = React.memo(
+ ({ field, onClick, ownFocus, showTooltip = false, value }) => {
+ return showTooltip ? (
+
+ }
+ >
+
+
+ ) : (
+
+ );
+ }
+);
+
+AddToTimelineButton.displayName = 'AddToTimelineButton';
diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/column_toggle.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/column_toggle.tsx
new file mode 100644
index 000000000000..52d8fb439526
--- /dev/null
+++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/column_toggle.tsx
@@ -0,0 +1,97 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { TooltipWithKeyboardShortcut } from '../../tooltip_with_keyboard_shortcut';
+import { getAdditionalScreenReaderOnlyContext } from '../utils';
+import { defaultColumnHeaderType } from '../../t_grid/body/column_headers/default_headers';
+import { DEFAULT_COLUMN_MIN_WIDTH } from '../../t_grid/body/constants';
+import { ColumnHeaderOptions } from '../../../../common/types/timeline';
+import { HoverActionComponentProps } from './types';
+
+export const COLUMN_TOGGLE = (field: string) =>
+ i18n.translate('xpack.timelines.hoverActions.columnToggleLabel', {
+ values: { field },
+ defaultMessage: 'Toggle {field} column in table',
+ });
+
+export const NESTED_COLUMN = (field: string) =>
+ i18n.translate('xpack.timelines.hoverActions.nestedColumnToggleLabel', {
+ values: { field },
+ defaultMessage:
+ 'The {field} field is an object, and is broken down into nested fields which can be added as columns',
+ });
+
+export const COLUMN_TOGGLE_KEYBOARD_SHORTCUT = 'i';
+
+export interface ColumnToggleFnArgs {
+ toggleColumn: (column: ColumnHeaderOptions) => void;
+ field: string;
+}
+
+export const columnToggleFn = ({ toggleColumn, field }: ColumnToggleFnArgs): void => {
+ return toggleColumn({
+ columnHeaderType: defaultColumnHeaderType,
+ id: field,
+ initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
+ });
+};
+
+export interface ColumnToggleProps extends HoverActionComponentProps {
+ isDisabled: boolean;
+ isObjectArray: boolean;
+}
+
+export const ColumnToggleButton: React.FC = React.memo(
+ ({ field, isDisabled, isObjectArray, onClick, ownFocus, showTooltip = false, value }) => {
+ const label = isObjectArray ? NESTED_COLUMN(field) : COLUMN_TOGGLE(field);
+
+ return showTooltip ? (
+
+ }
+ >
+
+
+ ) : (
+
+ );
+ }
+);
+
+ColumnToggleButton.displayName = 'ColumnToggleButton';
diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx
new file mode 100644
index 000000000000..33cc71e12c46
--- /dev/null
+++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { WithCopyToClipboard } from '../../clipboard/with_copy_to_clipboard';
+import { HoverActionComponentProps } from './types';
+
+export const FIELD = i18n.translate('xpack.timelines.hoverActions.fieldLabel', {
+ defaultMessage: 'Field',
+});
+
+export const COPY_TO_CLIPBOARD_KEYBOARD_SHORTCUT = 'c';
+
+export type CopyProps = Omit & {
+ isHoverAction?: boolean;
+};
+
+export const CopyButton: React.FC = React.memo(
+ ({ field, isHoverAction, ownFocus, value }) => {
+ return (
+
+ );
+ }
+);
+
+CopyButton.displayName = 'CopyButton';
diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_for_value.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_for_value.tsx
new file mode 100644
index 000000000000..f62d29c8b648
--- /dev/null
+++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_for_value.tsx
@@ -0,0 +1,81 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiButtonIcon, EuiButtonIconPropsForButton, EuiToolTip } from '@elastic/eui';
+import { TooltipWithKeyboardShortcut } from '../../tooltip_with_keyboard_shortcut';
+import { createFilter, getAdditionalScreenReaderOnlyContext } from '../utils';
+import { HoverActionComponentProps, FilterValueFnArgs } from './types';
+
+export const FILTER_FOR_VALUE = i18n.translate('xpack.timelines.hoverActions.filterForValue', {
+ defaultMessage: 'Filter for value',
+});
+export const FILTER_FOR_VALUE_KEYBOARD_SHORTCUT = 'f';
+
+export const filterForValueFn = ({
+ field,
+ value,
+ filterManager,
+ onFilterAdded,
+}: FilterValueFnArgs): void => {
+ const filter = value?.length === 0 ? createFilter(field, undefined) : createFilter(field, value);
+ const activeFilterManager = filterManager;
+
+ if (activeFilterManager != null) {
+ activeFilterManager.addFilters(filter);
+ if (onFilterAdded != null) {
+ onFilterAdded();
+ }
+ }
+};
+
+export interface FilterForValueProps extends HoverActionComponentProps {
+ defaultFocusedButtonRef: EuiButtonIconPropsForButton['buttonRef'];
+}
+
+export const FilterForValueButton: React.FC = React.memo(
+ ({ defaultFocusedButtonRef, field, onClick, ownFocus, showTooltip = false, value }) => {
+ return showTooltip ? (
+
+ }
+ >
+
+
+ ) : (
+
+ );
+ }
+);
+
+FilterForValueButton.displayName = 'FilterForValueButton';
diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_out_value.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_out_value.tsx
new file mode 100644
index 000000000000..a0888ac7c6e1
--- /dev/null
+++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_out_value.tsx
@@ -0,0 +1,77 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
+import { TooltipWithKeyboardShortcut } from '../../tooltip_with_keyboard_shortcut';
+import { createFilter, getAdditionalScreenReaderOnlyContext } from '../utils';
+import { HoverActionComponentProps, FilterValueFnArgs } from './types';
+
+export const FILTER_OUT_VALUE = i18n.translate('xpack.timelines.hoverActions.filterOutValue', {
+ defaultMessage: 'Filter out value',
+});
+
+export const FILTER_OUT_VALUE_KEYBOARD_SHORTCUT = 'o';
+
+export const filterOutValueFn = ({
+ field,
+ value,
+ filterManager,
+ onFilterAdded,
+}: FilterValueFnArgs) => {
+ const filter =
+ value?.length === 0 ? createFilter(field, null, false) : createFilter(field, value, true);
+ const activeFilterManager = filterManager;
+
+ if (activeFilterManager != null) {
+ activeFilterManager.addFilters(filter);
+ if (onFilterAdded != null) {
+ onFilterAdded();
+ }
+ }
+};
+
+export const FilterOutValueButton: React.FC = React.memo(
+ ({ field, onClick, ownFocus, showTooltip = false, value }) => {
+ return showTooltip ? (
+
+ }
+ >
+
+
+ ) : (
+
+ );
+ }
+);
+
+FilterOutValueButton.displayName = 'FilterOutValueButton';
diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/types.ts b/x-pack/plugins/timelines/public/components/hover_actions/actions/types.ts
new file mode 100644
index 000000000000..4999638e0fe8
--- /dev/null
+++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/types.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { FilterManager } from '../../../../../../../src/plugins/data/public';
+
+export interface FilterValueFnArgs {
+ field: string;
+ value: string[] | string | null | undefined;
+ filterManager: FilterManager | undefined;
+ onFilterAdded: (() => void) | undefined;
+}
+
+export interface HoverActionComponentProps {
+ field: string;
+ onClick?: () => void;
+ ownFocus: boolean;
+ showTooltip?: boolean;
+ value?: string[] | string | null;
+}
diff --git a/x-pack/plugins/timelines/public/components/hover_actions/index.tsx b/x-pack/plugins/timelines/public/components/hover_actions/index.tsx
new file mode 100644
index 000000000000..2329134d8562
--- /dev/null
+++ b/x-pack/plugins/timelines/public/components/hover_actions/index.tsx
@@ -0,0 +1,89 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React from 'react';
+import {
+ AddToTimelineButton,
+ ADD_TO_TIMELINE_KEYBOARD_SHORTCUT,
+ UseGetHandleStartDragToTimelineArgs,
+ useGetHandleStartDragToTimeline,
+} from './actions/add_to_timeline';
+import {
+ ColumnToggleButton,
+ columnToggleFn,
+ ColumnToggleFnArgs,
+ ColumnToggleProps,
+ COLUMN_TOGGLE_KEYBOARD_SHORTCUT,
+} from './actions/column_toggle';
+import { CopyButton, CopyProps, COPY_TO_CLIPBOARD_KEYBOARD_SHORTCUT } from './actions/copy';
+import {
+ FilterForValueButton,
+ filterForValueFn,
+ FilterForValueProps,
+ FILTER_FOR_VALUE_KEYBOARD_SHORTCUT,
+} from './actions/filter_for_value';
+import {
+ FilterOutValueButton,
+ filterOutValueFn,
+ FILTER_OUT_VALUE_KEYBOARD_SHORTCUT,
+} from './actions/filter_out_value';
+import { HoverActionComponentProps, FilterValueFnArgs } from './actions/types';
+
+export interface HoverActionsConfig {
+ addToTimeline: {
+ AddToTimelineButton: React.FC;
+ keyboardShortcut: string;
+ useGetHandleStartDragToTimeline: (args: UseGetHandleStartDragToTimelineArgs) => () => void;
+ };
+ columnToggle: {
+ ColumnToggleButton: React.FC;
+ columnToggleFn: (args: ColumnToggleFnArgs) => void;
+ keyboardShortcut: string;
+ };
+ copy: {
+ CopyButton: React.FC;
+ keyboardShortcut: string;
+ };
+ filterForValue: {
+ FilterForValueButton: React.FC;
+ filterForValueFn: (args: FilterValueFnArgs) => void;
+ keyboardShortcut: string;
+ };
+ filterOutValue: {
+ FilterOutValueButton: React.FC;
+ filterOutValueFn: (args: FilterValueFnArgs) => void;
+ keyboardShortcut: string;
+ };
+}
+
+export const addToTimeline = {
+ AddToTimelineButton,
+ keyboardShortcut: ADD_TO_TIMELINE_KEYBOARD_SHORTCUT,
+ useGetHandleStartDragToTimeline,
+};
+
+export const columnToggle = {
+ ColumnToggleButton,
+ columnToggleFn,
+ keyboardShortcut: COLUMN_TOGGLE_KEYBOARD_SHORTCUT,
+};
+
+export const copy = {
+ CopyButton,
+ keyboardShortcut: COPY_TO_CLIPBOARD_KEYBOARD_SHORTCUT,
+};
+
+export const filterForValue = {
+ FilterForValueButton,
+ filterForValueFn,
+ keyboardShortcut: FILTER_FOR_VALUE_KEYBOARD_SHORTCUT,
+};
+
+export const filterOutValue = {
+ FilterOutValueButton,
+ filterOutValueFn,
+ keyboardShortcut: FILTER_OUT_VALUE_KEYBOARD_SHORTCUT,
+};
diff --git a/x-pack/plugins/timelines/public/components/hover_actions/utils.ts b/x-pack/plugins/timelines/public/components/hover_actions/utils.ts
new file mode 100644
index 000000000000..f34506eaa795
--- /dev/null
+++ b/x-pack/plugins/timelines/public/components/hover_actions/utils.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 { Filter } from '../../../../../../src/plugins/data/public';
+
+export const getAdditionalScreenReaderOnlyContext = ({
+ field,
+ value,
+}: {
+ field: string;
+ value?: string[] | string | null;
+}): string => {
+ if (value == null) {
+ return field;
+ }
+
+ return Array.isArray(value) ? `${field} ${value.join(' ')}` : `${field} ${value}`;
+};
+
+export const createFilter = (
+ key: string,
+ value: string[] | string | null | undefined,
+ negate: boolean = false
+): Filter => {
+ const queryValue = value != null ? (Array.isArray(value) ? value[0] : value) : null;
+ return queryValue != null
+ ? {
+ meta: {
+ alias: null,
+ negate,
+ disabled: false,
+ type: 'phrase',
+ key,
+ value: queryValue,
+ params: {
+ query: queryValue,
+ },
+ },
+ query: {
+ match: {
+ [key]: {
+ query: queryValue,
+ type: 'phrase',
+ },
+ },
+ },
+ }
+ : ({
+ exists: {
+ field: key,
+ },
+ meta: {
+ alias: null,
+ disabled: false,
+ key,
+ negate: value === undefined,
+ type: 'exists',
+ value: 'exists',
+ },
+ } as Filter);
+};
diff --git a/x-pack/plugins/timelines/public/components/tooltip_with_keyboard_shortcut/index.tsx b/x-pack/plugins/timelines/public/components/tooltip_with_keyboard_shortcut/index.tsx
new file mode 100644
index 000000000000..fdf1f5275c17
--- /dev/null
+++ b/x-pack/plugins/timelines/public/components/tooltip_with_keyboard_shortcut/index.tsx
@@ -0,0 +1,49 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiText, EuiScreenReaderOnly } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+
+export const PRESS = i18n.translate(
+ 'xpack.timelines.hoverActions.tooltipWithKeyboardShortcut.pressTooltipLabel',
+ {
+ defaultMessage: 'Press',
+ }
+);
+export interface TooltipWithKeyboardShortcutProps {
+ additionalScreenReaderOnlyContext?: string;
+ content: React.ReactNode;
+ shortcut: string;
+ showShortcut: boolean;
+}
+
+const TooltipWithKeyboardShortcutComponent = ({
+ additionalScreenReaderOnlyContext = '',
+ content,
+ shortcut,
+ showShortcut,
+}: TooltipWithKeyboardShortcutProps) => (
+ <>
+ {content}
+ {additionalScreenReaderOnlyContext !== '' && (
+
+ {additionalScreenReaderOnlyContext}
+
+ )}
+ {showShortcut && (
+
+ {PRESS}
+ {'\u00a0'}
+ {shortcut}
+
+ )}
+ >
+);
+
+export const TooltipWithKeyboardShortcut = React.memo(TooltipWithKeyboardShortcutComponent);
+TooltipWithKeyboardShortcut.displayName = 'TooltipWithKeyboardShortcut';
diff --git a/x-pack/plugins/timelines/public/plugin.ts b/x-pack/plugins/timelines/public/plugin.ts
index c9534d4312e7..38fdc6149839 100644
--- a/x-pack/plugins/timelines/public/plugin.ts
+++ b/x-pack/plugins/timelines/public/plugin.ts
@@ -21,7 +21,7 @@ import type { LastUpdatedAtProps, LoadingPanelProps } from './components';
import { tGridReducer } from './store/t_grid/reducer';
import { useDraggableKeyboardWrapper } from './components/drag_and_drop/draggable_keyboard_wrapper_hook';
import { useAddToTimeline, useAddToTimelineSensor } from './hooks/use_add_to_timeline';
-
+import * as hoverActions from './components/hover_actions';
export class TimelinesPlugin implements Plugin {
constructor(private readonly initializerContext: PluginInitializerContext) {}
private _store: Store | undefined;
@@ -35,6 +35,9 @@ export class TimelinesPlugin implements Plugin {
return {} as TimelinesUIStart;
}
return {
+ getHoverActions: () => {
+ return hoverActions;
+ },
getTGrid: (props: TGridProps) => {
return getTGridLazy(props, {
store: this._store,
diff --git a/x-pack/plugins/timelines/public/types.ts b/x-pack/plugins/timelines/public/types.ts
index ffef1ee35c83..b5a4b54cf5eb 100644
--- a/x-pack/plugins/timelines/public/types.ts
+++ b/x-pack/plugins/timelines/public/types.ts
@@ -4,6 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+
import { ReactElement } from 'react';
import type { SensorAPI } from 'react-beautiful-dnd';
import { Store } from 'redux';
@@ -16,8 +17,10 @@ import type {
import type { TGridIntegratedProps } from './components/t_grid/integrated';
import type { TGridStandaloneProps } from './components/t_grid/standalone';
import type { UseAddToTimelineProps, UseAddToTimeline } from './hooks/use_add_to_timeline';
+import { HoverActionsConfig } from './components/hover_actions/index';
export * from './store/t_grid';
export interface TimelinesUIStart {
+ getHoverActions: () => HoverActionsConfig;
getTGrid: (
props: GetTGridProps
) => ReactElement>;
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 1cdd712ea53b..13cb175ac908 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -1951,26 +1951,14 @@
"home.sampleData.ecommerceSpec.totalRevenueTitle": "[e コマース] 合計収益",
"home.sampleData.ecommerceSpecDescription": "e コマースの注文をトラッキングするサンプルデータ、ビジュアライゼーション、ダッシュボードです。",
"home.sampleData.ecommerceSpecTitle": "サンプル e コマース注文",
- "home.sampleData.flightsSpec.airlineCarrierTitle": "[フライト] 航空会社",
"home.sampleData.flightsSpec.airportConnectionsTitle": "[フライト] 空港乗り継ぎ (空港にカーソルを合わせてください) ",
- "home.sampleData.flightsSpec.averageTicketPriceTitle": "[フライト] 平均運賃",
- "home.sampleData.flightsSpec.controlsTitle": "[フライト] コントロール",
"home.sampleData.flightsSpec.delayBucketsTitle": "[フライト] 遅延バケット",
"home.sampleData.flightsSpec.delaysAndCancellationsTitle": "[フライト] 遅延・欠航",
- "home.sampleData.flightsSpec.delayTypeTitle": "[フライト] 遅延タイプ",
"home.sampleData.flightsSpec.departuresCountMapTitle": "[フライト] 出発カウントマップ",
"home.sampleData.flightsSpec.destinationWeatherTitle": "[フライト] 目的地の天候",
- "home.sampleData.flightsSpec.flightCancellationsTitle": "[フライト] フライト欠航",
- "home.sampleData.flightsSpec.flightCountAndAverageTicketPriceTitle": "[フライト] カウントと平均運賃",
- "home.sampleData.flightsSpec.flightDelaysTitle": "[フライト] フライトの遅延",
"home.sampleData.flightsSpec.flightLogTitle": "[フライト] 飛行記録",
"home.sampleData.flightsSpec.globalFlightDashboardDescription": "ES-Air、Logstash Airways、Kibana Airlines、JetBeats のサンプル飛行データを分析します",
"home.sampleData.flightsSpec.globalFlightDashboardTitle": "[フライト] グローバルフライトダッシュボード",
- "home.sampleData.flightsSpec.markdownInstructionsTitle": "[フライト] マークダウンの指示",
- "home.sampleData.flightsSpec.originCountryTitle": "[Flights] 出発国と到着国の比較",
- "home.sampleData.flightsSpec.totalFlightCancellationsTitle": "[フライト] フライト欠航合計",
- "home.sampleData.flightsSpec.totalFlightDelaysTitle": "[フライト] フライト遅延合計",
- "home.sampleData.flightsSpec.totalFlightsTitle": "[フライト] フライト合計",
"home.sampleData.flightsSpecDescription": "飛行ルートを監視するサンプルデータ、ビジュアライゼーション、ダッシュボードです。",
"home.sampleData.flightsSpecTitle": "サンプル飛行データ",
"home.sampleData.logsSpec.fileTypeScatterPlotTitle": "[ログ] ファイルタイプ散布図",
@@ -3572,7 +3560,9 @@
"telemetry.provideUsageStatisticsAriaName": "使用統計を提供",
"telemetry.provideUsageStatisticsTitle": "使用統計を提供",
"telemetry.readOurUsageDataPrivacyStatementLinkText": "プライバシーポリシー",
+ "telemetry.securityData": "Endpoint Security データ",
"telemetry.seeExampleOfClusterData": "収集する {clusterData} の例をご覧ください。",
+ "telemetry.seeExampleOfClusterDataAndEndpointSecuity": "当社が収集する{clusterData}および{endpointSecurityData}の例をご覧ください。",
"telemetry.telemetryBannerDescription": "Elastic Stackの改善にご協力ください使用状況データの収集は現在無効です。使用状況データの収集を有効にすると、製品とサービスを管理して改善することができます。詳細については、{privacyStatementLink}をご覧ください。",
"telemetry.telemetryConfigAndLinkDescription": "使用状況データの収集を有効にすると、製品とサービスを管理して改善することができます。詳細については、{privacyStatementLink}をご覧ください。",
"telemetry.telemetryConfigDescription": "基本的な機能の利用状況に関する統計情報を提供して、Elastic Stack の改善にご協力ください。このデータは Elastic 社外と共有されません。",
@@ -8051,7 +8041,6 @@
"xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.featuredResults.title": "強調された結果",
"xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.go.button": "Go",
"xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.lastUpdated.heading": "最終更新",
- "xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.leaveUnassigned.field": "割り当てなし",
"xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.preview.title": "プレビュー",
"xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.reset.button": "リセット",
"xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.resultDetail.label": "結果詳細",
@@ -13286,7 +13275,6 @@
"xpack.maps.observability.uniqueCountMetricName": "ユニークカウント",
"xpack.maps.sampleData.ecommerceSpec.mapsTitle": "[e コマース] 国別の注文",
"xpack.maps.sampleData.flightaSpec.logsTitle": "[ログ ] 合計リクエスト数とバイト数",
- "xpack.maps.sampleData.flightaSpec.mapsTitle": "[フライト] 出発地と目的地の飛行時間",
"xpack.maps.sampleDataLinkLabel": "マップ",
"xpack.maps.security.desc": "セキュリティレイヤー",
"xpack.maps.security.disabledDesc": "セキュリティインデックスパターンが見つかりません。セキュリティを開始するには、[セキュリティ]>[概要]に移動します。",
@@ -19841,13 +19829,9 @@
"xpack.securitySolution.documentationLinks.detectionsRequirements.text": "検出の前提条件と要件",
"xpack.securitySolution.documentationLinks.mlJobCompatibility.text": "MLジョブの互換性",
"xpack.securitySolution.documentationLinks.solutionRequirements.text": "Elasticセキュリティシステム要件",
- "xpack.securitySolution.dragAndDrop.addToTimeline": "タイムライン調査に追加",
"xpack.securitySolution.dragAndDrop.closeButtonLabel": "閉じる",
"xpack.securitySolution.dragAndDrop.copyToClipboardTooltip": "クリップボードにコピー",
"xpack.securitySolution.dragAndDrop.draggableKeyboardInstructionsNotDraggingScreenReaderOnly": "オプションは Enter キーを押します。ドラッグを開始するには、スペースを押します。",
- "xpack.securitySolution.dragAndDrop.fieldLabel": "フィールド",
- "xpack.securitySolution.dragAndDrop.filterForValueHoverAction": "値でフィルター",
- "xpack.securitySolution.dragAndDrop.filterOutValueHoverAction": "値を除外",
"xpack.securitySolution.dragAndDrop.youAreInADialogContainingOptionsScreenReaderOnly": "フィールド {fieldName} のオプションを含む、ダイアログを表示しています。Tab を押すと、オプションを操作します。Escape を押すと、終了します。",
"xpack.securitySolution.draggables.field.categoryLabel": "カテゴリー",
"xpack.securitySolution.draggables.field.fieldLabel": "フィールド",
@@ -20138,7 +20122,6 @@
"xpack.securitySolution.eventDetails.nestedColumnCheckboxAriaLabel": "{field}フィールドはオブジェクトであり、列として追加できるネストされたフィールドに分解されます",
"xpack.securitySolution.eventDetails.table": "表",
"xpack.securitySolution.eventDetails.value": "値",
- "xpack.securitySolution.eventDetails.viewColumnCheckboxAriaLabel": "{field} 列を表示",
"xpack.securitySolution.eventRenderers.alertsDescription": "マルウェアまたはランサムウェアが防御、検出されたときにアラートが表示されます。",
"xpack.securitySolution.eventRenderers.alertsName": "アラート",
"xpack.securitySolution.eventRenderers.auditdDescriptionPart1": "監査イベントは、Linux Audit Framework からセキュリティ関連ログを通知します。",
@@ -20673,7 +20656,6 @@
"xpack.securitySolution.overview.pageTitle": "セキュリティ",
"xpack.securitySolution.overview.recentCasesSidebarTitle": "最近のケース",
"xpack.securitySolution.overview.recentTimelinesSidebarTitle": "最近のタイムライン",
- "xpack.securitySolution.overview.showTopTooltip": "上位の{fieldName}を表示",
"xpack.securitySolution.overview.signalCountTitle": "検出アラート傾向",
"xpack.securitySolution.overview.startedText": "セキュリティ情報およびイベント管理 (SIEM) へようこそ。はじめに{docs}や{data}をご参照ください。今後の機能に関する情報やチュートリアルは、{siemSolution}ページをご覧ください。",
"xpack.securitySolution.overview.startedText.dataLinkText": "投入データ",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 94af55cc0f3b..6e7ab7a33a0f 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -1963,26 +1963,14 @@
"home.sampleData.ecommerceSpec.totalRevenueTitle": "[电子商务] 总收入",
"home.sampleData.ecommerceSpecDescription": "用于追踪电子商务订单的样例数据、可视化和仪表板。",
"home.sampleData.ecommerceSpecTitle": "样例电子商务订单",
- "home.sampleData.flightsSpec.airlineCarrierTitle": "[航班] 航空公司",
"home.sampleData.flightsSpec.airportConnectionsTitle": "[航班] 机场航线 (将鼠标悬停在机场上) ",
- "home.sampleData.flightsSpec.averageTicketPriceTitle": "[航班] 平均票价",
- "home.sampleData.flightsSpec.controlsTitle": "[航班] 控制",
"home.sampleData.flightsSpec.delayBucketsTitle": "[航班] 延误存储桶",
"home.sampleData.flightsSpec.delaysAndCancellationsTitle": "[航班] 延误与取消",
- "home.sampleData.flightsSpec.delayTypeTitle": "[航班] 延误类型",
"home.sampleData.flightsSpec.departuresCountMapTitle": "[航班] 离港计数地图",
"home.sampleData.flightsSpec.destinationWeatherTitle": "[航班] 到达地天气",
- "home.sampleData.flightsSpec.flightCancellationsTitle": "[航班] 航班取消",
- "home.sampleData.flightsSpec.flightCountAndAverageTicketPriceTitle": "[航班] 航班计数和平均票价",
- "home.sampleData.flightsSpec.flightDelaysTitle": "[航班] 航班延误",
"home.sampleData.flightsSpec.flightLogTitle": "[航班] 飞行日志",
"home.sampleData.flightsSpec.globalFlightDashboardDescription": "分析 ES-Air、Logstash Airways、Kibana Airlines 和 JetBeats 的模拟航班数据",
"home.sampleData.flightsSpec.globalFlightDashboardTitle": "[航班] 全球航班仪表板",
- "home.sampleData.flightsSpec.markdownInstructionsTitle": "[航班] Markdown 说明",
- "home.sampleData.flightsSpec.originCountryTitle": "[航班] 始发国/地区与到达国/地区",
- "home.sampleData.flightsSpec.totalFlightCancellationsTitle": "[航班] 航班取消总数",
- "home.sampleData.flightsSpec.totalFlightDelaysTitle": "[航班] 航班延误总数",
- "home.sampleData.flightsSpec.totalFlightsTitle": "[航班] 航班总数",
"home.sampleData.flightsSpecDescription": "用于监测航班路线的样例数据、可视化和仪表板。",
"home.sampleData.flightsSpecTitle": "样例航班数据",
"home.sampleData.logsSpec.fileTypeScatterPlotTitle": "[日志] 文件类型散点图",
@@ -3598,7 +3586,9 @@
"telemetry.provideUsageStatisticsAriaName": "提供使用情况统计",
"telemetry.provideUsageStatisticsTitle": "提供使用情况统计",
"telemetry.readOurUsageDataPrivacyStatementLinkText": "隐私声明",
+ "telemetry.securityData": "终端安全数据",
"telemetry.seeExampleOfClusterData": "查看我们收集的{clusterData}的示例。",
+ "telemetry.seeExampleOfClusterDataAndEndpointSecuity": "查看我们收集的{clusterData}和 {endpointSecurityData}示例。",
"telemetry.telemetryBannerDescription": "想帮助我们改进 Elastic Stack?数据使用情况收集当前已禁用。启用使用情况数据收集可帮助我们管理并改善产品和服务。有关更多详情,请参阅我们的{privacyStatementLink}。",
"telemetry.telemetryConfigAndLinkDescription": "启用使用情况数据收集可帮助我们管理并改善产品和服务。有关更多详情,请参阅我们的{privacyStatementLink}。",
"telemetry.telemetryConfigDescription": "通过提供基本功能的使用情况统计信息,来帮助我们改进 Elastic Stack。我们不会在 Elastic 之外共享此数据。",
@@ -8119,7 +8109,6 @@
"xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.featuredResults.title": "精选结果",
"xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.go.button": "执行",
"xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.lastUpdated.heading": "上次更新时间",
- "xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.leaveUnassigned.field": "不分配",
"xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.preview.title": "预览",
"xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.reset.button": "重置",
"xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.resultDetail.label": "结果详情",
@@ -13464,7 +13453,6 @@
"xpack.maps.observability.uniqueCountMetricName": "唯一计数",
"xpack.maps.sampleData.ecommerceSpec.mapsTitle": "[电子商务] 订单 (按国家/地区) ",
"xpack.maps.sampleData.flightaSpec.logsTitle": "[日志] 请求和字节总数",
- "xpack.maps.sampleData.flightaSpec.mapsTitle": "[航班] 始发地和到达地航班时间",
"xpack.maps.sampleDataLinkLabel": "地图",
"xpack.maps.security.desc": "安全层",
"xpack.maps.security.disabledDesc": "找不到安全索引模式。要开始使用“安全性”,请前往“安全性”>“概览”。",
@@ -20139,13 +20127,9 @@
"xpack.securitySolution.documentationLinks.detectionsRequirements.text": "检测先决条件和要求",
"xpack.securitySolution.documentationLinks.mlJobCompatibility.text": "ML 作业兼容性",
"xpack.securitySolution.documentationLinks.solutionRequirements.text": "Elastic Security 系统要求",
- "xpack.securitySolution.dragAndDrop.addToTimeline": "添加到时间线调查",
"xpack.securitySolution.dragAndDrop.closeButtonLabel": "关闭",
"xpack.securitySolution.dragAndDrop.copyToClipboardTooltip": "复制到剪贴板",
"xpack.securitySolution.dragAndDrop.draggableKeyboardInstructionsNotDraggingScreenReaderOnly": "按 enter 键可显示选项,或按空格键开始拖动。",
- "xpack.securitySolution.dragAndDrop.fieldLabel": "字段",
- "xpack.securitySolution.dragAndDrop.filterForValueHoverAction": "筛留值",
- "xpack.securitySolution.dragAndDrop.filterOutValueHoverAction": "筛除值",
"xpack.securitySolution.dragAndDrop.youAreInADialogContainingOptionsScreenReaderOnly": "您在对话框中,其中包含 {fieldName} 字段的选项。按 tab 键导航选项。按 escape 退出。",
"xpack.securitySolution.draggables.field.categoryLabel": "类别",
"xpack.securitySolution.draggables.field.fieldLabel": "字段",
@@ -20439,7 +20423,6 @@
"xpack.securitySolution.eventDetails.nestedColumnCheckboxAriaLabel": "{field} 字段是对象,并分解为可以添加为列的嵌套字段",
"xpack.securitySolution.eventDetails.table": "表",
"xpack.securitySolution.eventDetails.value": "值",
- "xpack.securitySolution.eventDetails.viewColumnCheckboxAriaLabel": "查看 {field} 列",
"xpack.securitySolution.eventRenderers.alertsDescription": "阻止或检测到恶意软件或勒索软件时,显示告警",
"xpack.securitySolution.eventRenderers.alertsName": "告警",
"xpack.securitySolution.eventRenderers.auditdDescriptionPart1": "审计事件传送 Linux 审计框架的安全相关日志。",
@@ -21003,7 +20986,6 @@
"xpack.securitySolution.overview.pageTitle": "安全",
"xpack.securitySolution.overview.recentCasesSidebarTitle": "最近案例",
"xpack.securitySolution.overview.recentTimelinesSidebarTitle": "最近的时间线",
- "xpack.securitySolution.overview.showTopTooltip": "显示排名靠前的{fieldName}",
"xpack.securitySolution.overview.signalCountTitle": "检测告警趋势",
"xpack.securitySolution.overview.startedText": "欢迎使用安全信息和事件管理 (SIEM)。首先阅读我们的{docs}或{data}。要了解即将推出的功能和教程,请访问我们的 {siemSolution}页面。",
"xpack.securitySolution.overview.startedText.dataLinkText": "正在采集数据",
diff --git a/x-pack/plugins/uptime/common/rules/uptime_rule_field_map.ts b/x-pack/plugins/uptime/common/rules/uptime_rule_field_map.ts
new file mode 100644
index 000000000000..2a4e6ddcf58f
--- /dev/null
+++ b/x-pack/plugins/uptime/common/rules/uptime_rule_field_map.ts
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export const uptimeRuleFieldMap = {
+ // common fields
+ 'monitor.id': {
+ type: 'keyword',
+ },
+ 'url.full': {
+ type: 'keyword',
+ },
+ 'observer.geo.name': {
+ type: 'keyword',
+ },
+ reason: {
+ type: 'text',
+ },
+ // monitor status alert fields
+ 'error.message': {
+ type: 'text',
+ },
+ 'agent.name': {
+ type: 'keyword',
+ },
+ 'monitor.name': {
+ type: 'keyword',
+ },
+ 'monitor.type': {
+ type: 'keyword',
+ },
+ // tls alert fields
+ 'tls.server.x509.issuer.common_name': {
+ type: 'keyword',
+ },
+ 'tls.server.x509.subject.common_name': {
+ type: 'keyword',
+ },
+ 'tls.server.x509.not_after': {
+ type: 'date',
+ },
+ 'tls.server.x509.not_before': {
+ type: 'date',
+ },
+ 'tls.server.hash.sha256': {
+ type: 'keyword',
+ },
+ // anomaly alert fields
+ 'anomaly.start': {
+ type: 'date',
+ },
+ 'anomaly.bucket_span.minutes': {
+ type: 'keyword',
+ },
+} as const;
+
+export type UptimeRuleFieldMap = typeof uptimeRuleFieldMap;
diff --git a/x-pack/plugins/uptime/common/translations.ts b/x-pack/plugins/uptime/common/translations.ts
index 353c7fb5325f..5211438b8ec4 100644
--- a/x-pack/plugins/uptime/common/translations.ts
+++ b/x-pack/plugins/uptime/common/translations.ts
@@ -37,3 +37,78 @@ export const MonitorStatusTranslations = {
defaultMessage: 'Alert when a monitor is down or an availability threshold is breached.',
}),
};
+
+export const TlsTranslations = {
+ defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.legacy.defaultActionMessage', {
+ defaultMessage: `Detected TLS certificate {commonName} from issuer {issuer} is {status}. Certificate {summary}
+`,
+ values: {
+ commonName: '{{state.commonName}}',
+ issuer: '{{state.issuer}}',
+ summary: '{{state.summary}}',
+ status: '{{state.status}}',
+ },
+ }),
+ name: i18n.translate('xpack.uptime.alerts.tls.legacy.clientName', {
+ defaultMessage: 'Uptime TLS (Legacy)',
+ }),
+ description: i18n.translate('xpack.uptime.alerts.tls.legacy.description', {
+ defaultMessage:
+ 'Alert when the TLS certificate of an Uptime monitor is about to expire. This alert will be deprecated in a future version.',
+ }),
+};
+
+export const TlsTranslationsLegacy = {
+ defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.defaultActionMessage', {
+ defaultMessage: `Detected {count} TLS certificates expiring or becoming too old.
+{expiringConditionalOpen}
+Expiring cert count: {expiringCount}
+Expiring Certificates: {expiringCommonNameAndDate}
+{expiringConditionalClose}
+{agingConditionalOpen}
+Aging cert count: {agingCount}
+Aging Certificates: {agingCommonNameAndDate}
+{agingConditionalClose}
+`,
+ values: {
+ count: '{{state.count}}',
+ expiringCount: '{{state.expiringCount}}',
+ expiringCommonNameAndDate: '{{state.expiringCommonNameAndDate}}',
+ expiringConditionalOpen: '{{#state.hasExpired}}',
+ expiringConditionalClose: '{{/state.hasExpired}}',
+ agingCount: '{{state.agingCount}}',
+ agingCommonNameAndDate: '{{state.agingCommonNameAndDate}}',
+ agingConditionalOpen: '{{#state.hasAging}}',
+ agingConditionalClose: '{{/state.hasAging}}',
+ },
+ }),
+ name: i18n.translate('xpack.uptime.alerts.tls.clientName', {
+ defaultMessage: 'Uptime TLS',
+ }),
+ description: i18n.translate('xpack.uptime.alerts.tls.description', {
+ defaultMessage: 'Alert when the TLS certificate of an Uptime monitor is about to expire.',
+ }),
+};
+
+export const DurationAnomalyTranslations = {
+ defaultActionMessage: i18n.translate('xpack.uptime.alerts.durationAnomaly.defaultActionMessage', {
+ defaultMessage: `Abnormal ({severity} level) response time detected on {monitor} with url {monitorUrl} at {anomalyStartTimestamp}. Anomaly severity score is {severityScore}.
+Response times as high as {slowestAnomalyResponse} have been detected from location {observerLocation}. Expected response time is {expectedResponseTime}.`,
+ values: {
+ severity: '{{state.severity}}',
+ anomalyStartTimestamp: '{{state.anomalyStartTimestamp}}',
+ monitor: '{{state.monitor}}',
+ monitorUrl: '{{{state.monitorUrl}}}',
+ slowestAnomalyResponse: '{{state.slowestAnomalyResponse}}',
+ expectedResponseTime: '{{state.expectedResponseTime}}',
+ severityScore: '{{state.severityScore}}',
+ observerLocation: '{{state.observerLocation}}',
+ },
+ }),
+ name: i18n.translate('xpack.uptime.alerts.durationAnomaly.clientName', {
+ defaultMessage: 'Uptime Duration Anomaly',
+ }),
+ description: i18n.translate('xpack.uptime.alerts.durationAnomaly.description', {
+ defaultMessage: 'Alert when the Uptime monitor duration is anaomalous.',
+ }),
+};
diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json
index 97d154843231..e7fcb4607a8e 100644
--- a/x-pack/plugins/uptime/kibana.json
+++ b/x-pack/plugins/uptime/kibana.json
@@ -1,8 +1,16 @@
{
- "configPath": ["xpack", "uptime"],
+ "configPath": [
+ "xpack",
+ "uptime"
+ ],
"id": "uptime",
"kibanaVersion": "kibana",
- "optionalPlugins": ["data", "home", "ml", "fleet"],
+ "optionalPlugins": [
+ "data",
+ "home",
+ "ml",
+ "fleet"
+ ],
"requiredPlugins": [
"alerting",
"embeddable",
@@ -10,15 +18,24 @@
"licensing",
"triggersActionsUi",
"usageCollection",
+ "ruleRegistry",
"observability"
],
"server": true,
"ui": true,
"version": "8.0.0",
- "requiredBundles": ["observability", "kibanaReact", "kibanaUtils", "home", "data", "ml", "fleet"],
+ "requiredBundles": [
+ "observability",
+ "kibanaReact",
+ "kibanaUtils",
+ "home",
+ "data",
+ "ml",
+ "fleet"
+ ],
"owner": {
"name": "Uptime",
"githubTeam": "uptime"
},
"description": "This plugin visualizes data from Synthetics and Heartbeat, and integrates with other Observability solutions."
-}
+}
\ No newline at end of file
diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts
index 869ecda3d29c..45067c6018cc 100644
--- a/x-pack/plugins/uptime/public/apps/plugin.ts
+++ b/x-pack/plugins/uptime/public/apps/plugin.ts
@@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
import {
CoreSetup,
CoreStart,
@@ -29,7 +28,7 @@ import {
DataPublicPluginSetup,
DataPublicPluginStart,
} from '../../../../../src/plugins/data/public';
-import { alertTypeInitializers } from '../lib/alert_types';
+import { alertTypeInitializers, legacyAlertTypeInitializers } from '../lib/alert_types';
import { FleetStart } from '../../../fleet/public';
import {
FetchDataParams,
@@ -141,6 +140,36 @@ export class UptimePlugin
)
);
+ const { observabilityRuleTypeRegistry } = plugins.observability;
+
+ core.getStartServices().then(([coreStart, clientPluginsStart]) => {
+ alertTypeInitializers.forEach((init) => {
+ const alertInitializer = init({
+ core: coreStart,
+ plugins: clientPluginsStart,
+ });
+ if (
+ clientPluginsStart.triggersActionsUi &&
+ !clientPluginsStart.triggersActionsUi.alertTypeRegistry.has(alertInitializer.id)
+ ) {
+ observabilityRuleTypeRegistry.register(alertInitializer);
+ }
+ });
+
+ legacyAlertTypeInitializers.forEach((init) => {
+ const alertInitializer = init({
+ core: coreStart,
+ plugins: clientPluginsStart,
+ });
+ if (
+ clientPluginsStart.triggersActionsUi &&
+ !clientPluginsStart.triggersActionsUi.alertTypeRegistry.has(alertInitializer.id)
+ ) {
+ plugins.triggersActionsUi.alertTypeRegistry.register(alertInitializer);
+ }
+ });
+ });
+
core.application.register({
id: PLUGIN.ID,
euiIconType: 'logoObservability',
@@ -171,26 +200,12 @@ export class UptimePlugin
const [coreStart, corePlugins] = await core.getStartServices();
const { renderApp } = await import('./render_app');
-
return renderApp(coreStart, plugins, corePlugins, params);
},
});
}
public start(start: CoreStart, plugins: ClientPluginsStart): void {
- alertTypeInitializers.forEach((init) => {
- const alertInitializer = init({
- core: start,
- plugins,
- });
- if (
- plugins.triggersActionsUi &&
- !plugins.triggersActionsUi.alertTypeRegistry.has(alertInitializer.id)
- ) {
- plugins.triggersActionsUi.alertTypeRegistry.register(alertInitializer);
- }
- });
-
if (plugins.fleet) {
const { registerExtension } = plugins.fleet;
diff --git a/x-pack/plugins/uptime/public/lib/alert_types/common.ts b/x-pack/plugins/uptime/public/lib/alert_types/common.ts
new file mode 100644
index 000000000000..09b02150957d
--- /dev/null
+++ b/x-pack/plugins/uptime/public/lib/alert_types/common.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 { stringify } from 'querystring';
+
+export const format = ({
+ pathname,
+ query,
+}: {
+ pathname: string;
+ query: Record;
+}): string => {
+ return `${pathname}?${stringify(query)}`;
+};
+
+export const getMonitorRouteFromMonitorId = ({
+ monitorId,
+ dateRangeStart,
+ dateRangeEnd,
+ filters = {},
+}: {
+ monitorId: string;
+ dateRangeStart: string;
+ dateRangeEnd: string;
+ filters?: Record;
+}) =>
+ format({
+ pathname: `/app/uptime/monitor/${btoa(monitorId)}`,
+ query: {
+ dateRangeEnd,
+ dateRangeStart,
+ ...(Object.keys(filters).length
+ ? { filters: JSON.stringify(Object.keys(filters).map((key) => [key, filters[key]])) }
+ : {}),
+ },
+ });
diff --git a/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx b/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx
index a80e38ac622a..f14c1a4a9fdd 100644
--- a/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx
+++ b/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx
@@ -6,18 +6,23 @@
*/
import React from 'react';
-import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
+import moment from 'moment';
+
import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts';
-import { DurationAnomalyTranslations } from './translations';
+import { DurationAnomalyTranslations } from '../../../common/translations';
import { AlertTypeInitializer } from '.';
+import { getMonitorRouteFromMonitorId } from './common';
+
+import { ObservabilityRuleTypeModel } from '../../../../observability/public';
+
const { defaultActionMessage, description } = DurationAnomalyTranslations;
const DurationAnomalyAlert = React.lazy(() => import('./lazy_wrapper/duration_anomaly'));
export const initDurationAnomalyAlertType: AlertTypeInitializer = ({
core,
plugins,
-}): AlertTypeModel => ({
+}): ObservabilityRuleTypeModel => ({
id: CLIENT_ALERT_TYPES.DURATION_ANOMALY,
iconClass: 'uptimeApp',
documentationUrl(docLinks) {
@@ -30,4 +35,13 @@ export const initDurationAnomalyAlertType: AlertTypeInitializer = ({
validate: () => ({ errors: {} }),
defaultActionMessage,
requiresAppContext: true,
+ format: ({ fields }) => ({
+ reason: fields.reason,
+ link: getMonitorRouteFromMonitorId({
+ monitorId: fields['monitor.id']!,
+ dateRangeEnd:
+ fields['kibana.rac.alert.status'] === 'open' ? 'now' : fields['kibana.rac.alert.end']!,
+ dateRangeStart: moment(new Date(fields['anomaly.start']!)).subtract('5', 'm').toISOString(),
+ }),
+ }),
});
diff --git a/x-pack/plugins/uptime/public/lib/alert_types/index.ts b/x-pack/plugins/uptime/public/lib/alert_types/index.ts
index 406b730fa1e6..9dc67340a043 100644
--- a/x-pack/plugins/uptime/public/lib/alert_types/index.ts
+++ b/x-pack/plugins/uptime/public/lib/alert_types/index.ts
@@ -6,6 +6,7 @@
*/
import { CoreStart } from 'kibana/public';
+import { ObservabilityRuleTypeModel } from '../../../../observability/public';
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
import { initMonitorStatusAlertType } from './monitor_status';
import { initTlsAlertType } from './tls';
@@ -13,14 +14,17 @@ import { initTlsLegacyAlertType } from './tls_legacy';
import { ClientPluginsStart } from '../../apps/plugin';
import { initDurationAnomalyAlertType } from './duration_anomaly';
-export type AlertTypeInitializer = (dependenies: {
+export type AlertTypeInitializer = (dependenies: {
core: CoreStart;
plugins: ClientPluginsStart;
-}) => AlertTypeModel;
+}) => TAlertTypeModel;
export const alertTypeInitializers: AlertTypeInitializer[] = [
initMonitorStatusAlertType,
initTlsAlertType,
- initTlsLegacyAlertType,
initDurationAnomalyAlertType,
];
+
+export const legacyAlertTypeInitializers: Array> = [
+ initTlsLegacyAlertType,
+];
diff --git a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.test.ts b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.test.ts
index fc19d4c60e17..16c20cf7666e 100644
--- a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.test.ts
+++ b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.test.ts
@@ -206,6 +206,7 @@ describe('monitor status alert type', () => {
"defaultActionMessage": "Monitor {{state.monitorName}} with url {{{state.monitorUrl}}} is {{state.statusMessage}} from {{state.observerLocation}}. The latest error message is {{{state.latestErrorMessage}}}",
"description": "Alert when a monitor is down or an availability threshold is breached.",
"documentationUrl": [Function],
+ "format": [Function],
"iconClass": "uptimeApp",
"id": "xpack.uptime.alerts.monitorStatus",
"requiresAppContext": false,
diff --git a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx
index 50db7d9b5b5a..a87ba4aedbb2 100644
--- a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx
+++ b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx
@@ -6,12 +6,18 @@
*/
import React from 'react';
-import { AlertTypeModel, ValidationResult } from '../../../../triggers_actions_ui/public';
-import { AlertTypeInitializer } from '.';
+import moment from 'moment';
+
+import { ObservabilityRuleTypeModel } from '../../../../observability/public';
+import { ValidationResult } from '../../../../triggers_actions_ui/public';
import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts';
import { MonitorStatusTranslations } from '../../../common/translations';
+import { getMonitorRouteFromMonitorId } from './common';
+
+import { AlertTypeInitializer } from '.';
+
const { defaultActionMessage, description } = MonitorStatusTranslations;
const MonitorStatusAlert = React.lazy(() => import('./lazy_wrapper/monitor_status'));
@@ -21,7 +27,7 @@ let validateFunc: (alertParams: any) => ValidationResult;
export const initMonitorStatusAlertType: AlertTypeInitializer = ({
core,
plugins,
-}): AlertTypeModel => ({
+}): ObservabilityRuleTypeModel => ({
id: CLIENT_ALERT_TYPES.MONITOR_STATUS,
description,
iconClass: 'uptimeApp',
@@ -44,4 +50,18 @@ export const initMonitorStatusAlertType: AlertTypeInitializer = ({
},
defaultActionMessage,
requiresAppContext: false,
+ format: ({ fields }) => ({
+ reason: fields.reason,
+ link: getMonitorRouteFromMonitorId({
+ monitorId: fields['monitor.id']!,
+ dateRangeEnd:
+ fields['kibana.rac.alert.status'] === 'open' ? 'now' : fields['kibana.rac.alert.end']!,
+ dateRangeStart: moment(new Date(fields['kibana.rac.alert.start']!))
+ .subtract('5', 'm')
+ .toISOString(),
+ filters: {
+ 'observer.geo.name': [fields['observer.geo.name'][0]],
+ },
+ }),
+ }),
});
diff --git a/x-pack/plugins/uptime/public/lib/alert_types/tls.tsx b/x-pack/plugins/uptime/public/lib/alert_types/tls.tsx
index c3bcfc46646d..6632a0c04396 100644
--- a/x-pack/plugins/uptime/public/lib/alert_types/tls.tsx
+++ b/x-pack/plugins/uptime/public/lib/alert_types/tls.tsx
@@ -6,14 +6,19 @@
*/
import React from 'react';
-import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
+import { ObservabilityRuleTypeModel } from '../../../../observability/public';
import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts';
-import { TlsTranslations } from './translations';
+import { TlsTranslations } from '../../../common/translations';
import { AlertTypeInitializer } from '.';
+import { CERTIFICATES_ROUTE } from '../../../common/constants/ui';
+
const { defaultActionMessage, description } = TlsTranslations;
const TLSAlert = React.lazy(() => import('./lazy_wrapper/tls_alert'));
-export const initTlsAlertType: AlertTypeInitializer = ({ core, plugins }): AlertTypeModel => ({
+export const initTlsAlertType: AlertTypeInitializer = ({
+ core,
+ plugins,
+}): ObservabilityRuleTypeModel => ({
id: CLIENT_ALERT_TYPES.TLS,
iconClass: 'uptimeApp',
documentationUrl(docLinks) {
@@ -26,4 +31,8 @@ export const initTlsAlertType: AlertTypeInitializer = ({ core, plugins }): Alert
validate: () => ({ errors: {} }),
defaultActionMessage,
requiresAppContext: false,
+ format: ({ fields }) => ({
+ reason: fields.reason,
+ link: `/app/uptime${CERTIFICATES_ROUTE}`,
+ }),
});
diff --git a/x-pack/plugins/uptime/public/lib/alert_types/tls_legacy.tsx b/x-pack/plugins/uptime/public/lib/alert_types/tls_legacy.tsx
index 9982eb385d90..bed5dae55571 100644
--- a/x-pack/plugins/uptime/public/lib/alert_types/tls_legacy.tsx
+++ b/x-pack/plugins/uptime/public/lib/alert_types/tls_legacy.tsx
@@ -8,12 +8,12 @@
import React from 'react';
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts';
-import { TlsTranslationsLegacy } from './translations';
+import { TlsTranslationsLegacy } from '../../../common/translations';
import { AlertTypeInitializer } from '.';
const { defaultActionMessage, description } = TlsTranslationsLegacy;
const TLSAlert = React.lazy(() => import('./lazy_wrapper/tls_alert'));
-export const initTlsLegacyAlertType: AlertTypeInitializer = ({
+export const initTlsLegacyAlertType: AlertTypeInitializer = ({
core,
plugins,
}): AlertTypeModel => ({
diff --git a/x-pack/plugins/uptime/public/lib/alert_types/translations.ts b/x-pack/plugins/uptime/public/lib/alert_types/translations.ts
deleted file mode 100644
index 5122120479cf..000000000000
--- a/x-pack/plugins/uptime/public/lib/alert_types/translations.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { i18n } from '@kbn/i18n';
-
-export const TlsTranslations = {
- defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.defaultActionMessage', {
- defaultMessage: `Detected TLS certificate {commonName} from issuer {issuer} is {status}. Certificate {summary}
-`,
- values: {
- commonName: '{{state.commonName}}',
- issuer: '{{state.issuer}}',
- summary: '{{state.summary}}',
- status: '{{state.status}}',
- },
- }),
- name: i18n.translate('xpack.uptime.alerts.tls.clientName', {
- defaultMessage: 'Uptime TLS',
- }),
- description: i18n.translate('xpack.uptime.alerts.tls.description', {
- defaultMessage: 'Alert when the TLS certificate of an Uptime monitor is about to expire.',
- }),
-};
-
-export const TlsTranslationsLegacy = {
- defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.legacy.defaultActionMessage', {
- defaultMessage: `Detected {count} TLS certificates expiring or becoming too old.
-{expiringConditionalOpen}
-Expiring cert count: {expiringCount}
-Expiring Certificates: {expiringCommonNameAndDate}
-{expiringConditionalClose}
-{agingConditionalOpen}
-Aging cert count: {agingCount}
-Aging Certificates: {agingCommonNameAndDate}
-{agingConditionalClose}
-`,
- values: {
- count: '{{state.count}}',
- expiringCount: '{{state.expiringCount}}',
- expiringCommonNameAndDate: '{{state.expiringCommonNameAndDate}}',
- expiringConditionalOpen: '{{#state.hasExpired}}',
- expiringConditionalClose: '{{/state.hasExpired}}',
- agingCount: '{{state.agingCount}}',
- agingCommonNameAndDate: '{{state.agingCommonNameAndDate}}',
- agingConditionalOpen: '{{#state.hasAging}}',
- agingConditionalClose: '{{/state.hasAging}}',
- },
- }),
- name: i18n.translate('xpack.uptime.alerts.tls.legacy.clientName', {
- defaultMessage: 'Uptime TLS',
- }),
- description: i18n.translate('xpack.uptime.alerts.tls.legacy.description', {
- defaultMessage:
- 'Alert when the TLS certificate of an Uptime monitor is about to expire. This rule type will be deprecated in a future version.',
- }),
-};
-
-export const DurationAnomalyTranslations = {
- defaultActionMessage: i18n.translate('xpack.uptime.alerts.durationAnomaly.defaultActionMessage', {
- defaultMessage: `Abnormal ({severity} level) response time detected on {monitor} with url {monitorUrl} at {anomalyStartTimestamp}. Anomaly severity score is {severityScore}.
-Response times as high as {slowestAnomalyResponse} have been detected from location {observerLocation}. Expected response time is {expectedResponseTime}.`,
- values: {
- severity: '{{state.severity}}',
- anomalyStartTimestamp: '{{state.anomalyStartTimestamp}}',
- monitor: '{{state.monitor}}',
- monitorUrl: '{{{state.monitorUrl}}}',
- slowestAnomalyResponse: '{{state.slowestAnomalyResponse}}',
- expectedResponseTime: '{{state.expectedResponseTime}}',
- severityScore: '{{state.severityScore}}',
- observerLocation: '{{state.observerLocation}}',
- },
- }),
- name: i18n.translate('xpack.uptime.alerts.durationAnomaly.clientName', {
- defaultMessage: 'Uptime Duration Anomaly',
- }),
- description: i18n.translate('xpack.uptime.alerts.durationAnomaly.description', {
- defaultMessage: 'Alert when the Uptime monitor duration is anaomalous.',
- }),
-};
diff --git a/x-pack/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts
index 82ba70155608..c303c7827333 100644
--- a/x-pack/plugins/uptime/server/kibana.index.ts
+++ b/x-pack/plugins/uptime/server/kibana.index.ts
@@ -6,12 +6,14 @@
*/
import { Request, Server } from '@hapi/hapi';
+import { Logger } from 'kibana/server';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
import { PLUGIN } from '../common/constants/plugin';
import { compose } from './lib/compose/kibana';
import { initUptimeServer } from './uptime_server';
import { UptimeCorePlugins, UptimeCoreSetup } from './lib/adapters/framework';
import { umDynamicSettings } from './lib/saved_objects';
+import { UptimeRuleRegistry } from './plugin';
export interface KibanaRouteOptions {
path: string;
@@ -25,7 +27,12 @@ export interface KibanaServer extends Server {
route: (options: KibanaRouteOptions) => void;
}
-export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCorePlugins) => {
+export const initServerWithKibana = (
+ server: UptimeCoreSetup,
+ plugins: UptimeCorePlugins,
+ ruleRegistry: UptimeRuleRegistry,
+ logger: Logger
+) => {
const { features } = plugins;
const libs = compose(server);
@@ -86,5 +93,5 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor
},
});
- initUptimeServer(server, libs, plugins);
+ initUptimeServer(server, libs, plugins, ruleRegistry, logger);
};
diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts
index d1bbbc1d1856..d5b938d78c86 100644
--- a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts
+++ b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts
@@ -11,9 +11,11 @@ import type {
ISavedObjectsRepository,
IScopedClusterClient,
} from 'src/core/server';
+import { ObservabilityPluginSetup } from '../../../../../observability/server';
import { UMKibanaRoute } from '../../../rest_api';
import { PluginSetupContract } from '../../../../../features/server';
import { MlPluginSetup as MlSetup } from '../../../../../ml/server';
+import { RuleRegistryPluginSetupContract } from '../../../../../rule_registry/server';
import { UptimeESClient } from '../../lib';
import type { UptimeRouter } from '../../../types';
@@ -37,8 +39,10 @@ export interface UptimeCorePlugins {
features: PluginSetupContract;
alerting: any;
elasticsearch: any;
+ observability: ObservabilityPluginSetup;
usageCollection: UsageCollectionSetup;
ml: MlSetup;
+ ruleRegistry: RuleRegistryPluginSetupContract;
}
export interface UMBackendFrameworkAdapter {
diff --git a/x-pack/plugins/uptime/server/lib/alerts/common.ts b/x-pack/plugins/uptime/server/lib/alerts/common.ts
index 29f2c0bde208..6bf9d28c2da9 100644
--- a/x-pack/plugins/uptime/server/lib/alerts/common.ts
+++ b/x-pack/plugins/uptime/server/lib/alerts/common.ts
@@ -6,6 +6,7 @@
*/
import { isRight } from 'fp-ts/lib/Either';
+import Mustache from 'mustache';
import { UptimeCommonState, UptimeCommonStateType } from '../../../common/runtime_types';
export type UpdateUptimeAlertState = (
@@ -55,3 +56,7 @@ export const updateState: UpdateUptimeAlertState = (state, isTriggeredNow) => {
isTriggered: isTriggeredNow,
};
};
+
+export const generateAlertMessage = (messageTemplate: string, fields: Record) => {
+ return Mustache.render(messageTemplate, { state: { ...fields } });
+};
diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts
new file mode 100644
index 000000000000..ce13ae4ce6ce
--- /dev/null
+++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts
@@ -0,0 +1,210 @@
+/*
+ * 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 { durationAnomalyAlertFactory } from './duration_anomaly';
+import { DURATION_ANOMALY } from '../../../common/constants/alerts';
+import { AnomaliesTableRecord, AnomalyRecordDoc } from '../../../../ml/common/types/anomalies';
+import { DynamicSettings } from '../../../common/runtime_types';
+import { createRuleTypeMocks, bootstrapDependencies } from './test_utils';
+import { getSeverityType } from '../../../../ml/common/util/anomaly_utils';
+import { Ping } from '../../../common/runtime_types/ping';
+import {
+ ALERT_SEVERITY_LEVEL,
+ ALERT_SEVERITY_VALUE,
+ ALERT_EVALUATION_VALUE,
+ ALERT_EVALUATION_THRESHOLD,
+} from '@kbn/rule-data-utils/target/technical_field_names';
+
+interface MockAnomaly {
+ severity: AnomaliesTableRecord['severity'];
+ source: Partial;
+ actualSort: AnomaliesTableRecord['actualSort'];
+ typicalSort: AnomaliesTableRecord['typicalSort'];
+ entityValue: AnomaliesTableRecord['entityValue'];
+}
+
+interface MockAnomalyResult {
+ anomalies: MockAnomaly[];
+}
+
+const monitorId = 'uptime-monitor';
+const mockUrl = 'https://elastic.co';
+
+/**
+ * This function aims to provide an easy way to give mock props that will
+ * reduce boilerplate for tests.
+ * @param dynamic the expiration and aging thresholds received at alert creation time
+ * @param params the params received at alert creation time
+ * @param state the state the alert maintains
+ */
+const mockOptions = (
+ dynamicCertSettings?: {
+ certExpirationThreshold: DynamicSettings['certExpirationThreshold'];
+ certAgeThreshold: DynamicSettings['certAgeThreshold'];
+ },
+ state = {},
+ params = {
+ timerange: { from: 'now-15m', to: 'now' },
+ monitorId,
+ severity: 'warning',
+ }
+): any => {
+ const { services } = createRuleTypeMocks(dynamicCertSettings);
+
+ return {
+ params,
+ state,
+ services,
+ };
+};
+
+const mockAnomaliesResult: MockAnomalyResult = {
+ anomalies: [
+ {
+ severity: 25,
+ source: {
+ timestamp: 1622137799,
+ 'monitor.id': 'uptime-monitor',
+ bucket_span: 900,
+ },
+ actualSort: 200000,
+ typicalSort: 10000,
+ entityValue: 'harrisburg',
+ },
+ {
+ severity: 10,
+ source: {
+ timestamp: 1622137799,
+ 'monitor.id': 'uptime-monitor',
+ bucket_span: 900,
+ },
+ actualSort: 300000,
+ typicalSort: 20000,
+ entityValue: 'fairbanks',
+ },
+ ],
+};
+
+const mockPing: Partial = {
+ url: {
+ full: mockUrl,
+ },
+};
+
+describe('duration anomaly alert', () => {
+ let toISOStringSpy: jest.SpyInstance;
+ const mockDate = 'date';
+ beforeAll(() => {
+ Date.now = jest.fn().mockReturnValue(new Date('2021-05-13T12:33:37.000Z'));
+ jest.spyOn(Intl, 'DateTimeFormat').mockImplementation(() => ({
+ format: jest.fn(),
+ formatToParts: jest.fn(),
+ resolvedOptions: () => ({
+ locale: '',
+ calendar: '',
+ numberingSystem: '',
+ timeZone: 'UTC',
+ }),
+ }));
+ toISOStringSpy = jest.spyOn(Date.prototype, 'toISOString');
+ });
+
+ describe('alert executor', () => {
+ it('triggers when aging or expiring alerts are found', async () => {
+ toISOStringSpy.mockImplementation(() => mockDate);
+ const mockResultServiceProviderGetter: jest.Mock<{
+ getAnomaliesTableData: jest.Mock;
+ }> = jest.fn();
+ const mockGetAnomliesTableDataGetter: jest.Mock = jest.fn();
+ const mockGetLatestMonitorGetter: jest.Mock> = jest.fn();
+
+ mockGetLatestMonitorGetter.mockReturnValue(mockPing);
+ mockGetAnomliesTableDataGetter.mockReturnValue(mockAnomaliesResult);
+ mockResultServiceProviderGetter.mockReturnValue({
+ getAnomaliesTableData: mockGetAnomliesTableDataGetter,
+ });
+ const { server, libs, plugins } = bootstrapDependencies(
+ { getLatestMonitor: mockGetLatestMonitorGetter },
+ {
+ ml: {
+ resultsServiceProvider: mockResultServiceProviderGetter,
+ },
+ }
+ );
+ const alert = durationAnomalyAlertFactory(server, libs, plugins);
+ const options = mockOptions();
+ const {
+ services: { alertWithLifecycle },
+ } = options;
+ // @ts-ignore the executor can return `void`, but ours never does
+ const state: Record = await alert.executor(options);
+ expect(mockGetAnomliesTableDataGetter).toHaveBeenCalledTimes(1);
+ expect(alertWithLifecycle).toHaveBeenCalledTimes(2);
+ expect(mockGetAnomliesTableDataGetter).toBeCalledWith(
+ ['uptime_monitor_high_latency_by_geo'],
+ [],
+ [],
+ 'auto',
+ options.params.severity,
+ 1620909217000,
+ 1620909217000,
+ 'UTC',
+ 500,
+ 10,
+ undefined
+ );
+ const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results;
+ expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(2);
+ mockAnomaliesResult.anomalies.forEach((anomaly, index) => {
+ const slowestResponse = Math.round(anomaly.actualSort / 1000);
+ const typicalResponse = Math.round(anomaly.typicalSort / 1000);
+ expect(alertWithLifecycle).toBeCalledWith({
+ fields: {
+ 'monitor.id': options.params.monitorId,
+ 'url.full': mockPing.url?.full,
+ 'anomaly.start': mockDate,
+ 'anomaly.bucket_span.minutes': anomaly.source.bucket_span,
+ 'observer.geo.name': anomaly.entityValue,
+ [ALERT_EVALUATION_VALUE]: anomaly.actualSort,
+ [ALERT_EVALUATION_THRESHOLD]: anomaly.typicalSort,
+ [ALERT_SEVERITY_LEVEL]: getSeverityType(anomaly.severity),
+ [ALERT_SEVERITY_VALUE]: anomaly.severity,
+ reason: `Abnormal (${getSeverityType(
+ anomaly.severity
+ )} level) response time detected on uptime-monitor with url ${
+ mockPing.url?.full
+ } at date. Anomaly severity score is ${anomaly.severity}.
+Response times as high as ${slowestResponse} ms have been detected from location ${
+ anomaly.entityValue
+ }. Expected response time is ${typicalResponse} ms.`,
+ },
+ id: `${DURATION_ANOMALY.id}${index}`,
+ });
+ expect(alertInstanceMock.replaceState).toBeCalledWith({
+ firstCheckedAt: 'date',
+ firstTriggeredAt: undefined,
+ lastCheckedAt: 'date',
+ lastResolvedAt: undefined,
+ isTriggered: false,
+ anomalyStartTimestamp: 'date',
+ currentTriggerStarted: undefined,
+ expectedResponseTime: `${typicalResponse} ms`,
+ lastTriggeredAt: undefined,
+ monitor: monitorId,
+ monitorUrl: mockPing.url?.full,
+ observerLocation: anomaly.entityValue,
+ severity: getSeverityType(anomaly.severity),
+ severityScore: anomaly.severity,
+ slowestAnomalyResponse: `${slowestResponse} ms`,
+ bucketSpan: anomaly.source.bucket_span,
+ });
+ });
+ expect(alertInstanceMock.scheduleActions).toHaveBeenCalledTimes(2);
+ expect(alertInstanceMock.scheduleActions).toBeCalledWith(DURATION_ANOMALY.id);
+ });
+ });
+});
diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts
index 981a7e7ca392..2388a789f3b8 100644
--- a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts
+++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts
@@ -8,8 +8,14 @@
import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server';
import moment from 'moment';
import { schema } from '@kbn/config-schema';
+import {
+ ALERT_SEVERITY_LEVEL,
+ ALERT_SEVERITY_VALUE,
+ ALERT_EVALUATION_VALUE,
+ ALERT_EVALUATION_THRESHOLD,
+} from '@kbn/rule-data-utils/target/technical_field_names';
import { ActionGroupIdsOf } from '../../../../alerting/common';
-import { updateState } from './common';
+import { updateState, generateAlertMessage } from './common';
import { DURATION_ANOMALY } from '../../../common/constants/alerts';
import { commonStateTranslations, durationAnomalyTranslations } from './translations';
import { AnomaliesTableRecord } from '../../../../ml/common/types/anomalies';
@@ -18,8 +24,10 @@ import { UptimeCorePlugins } from '../adapters/framework';
import { UptimeAlertTypeFactory } from './types';
import { Ping } from '../../../common/runtime_types/ping';
import { getMLJobId } from '../../../common/lib';
-import { getLatestMonitor } from '../requests/get_latest_monitor';
-import { uptimeAlertWrapper } from './uptime_alert_wrapper';
+
+import { DurationAnomalyTranslations as CommonDurationAnomalyTranslations } from '../../../common/translations';
+
+import { createUptimeESClient } from '../lib';
export type ActionGroupIds = ActionGroupIdsOf;
@@ -33,6 +41,7 @@ export const getAnomalySummary = (anomaly: AnomaliesTableRecord, monitorInfo: Pi
slowestAnomalyResponse: Math.round(anomaly.actualSort / 1000) + ' ms',
expectedResponseTime: Math.round(anomaly.typicalSort / 1000) + ' ms',
observerLocation: anomaly.entityValue,
+ bucketSpan: anomaly.source.bucket_span,
};
};
@@ -65,61 +74,83 @@ const getAnomalies = async (
export const durationAnomalyAlertFactory: UptimeAlertTypeFactory = (
_server,
- _libs,
+ libs,
plugins
-) =>
- uptimeAlertWrapper({
- id: 'xpack.uptime.alerts.durationAnomaly',
- name: durationAnomalyTranslations.alertFactoryName,
- validate: {
- params: schema.object({
- monitorId: schema.string(),
- severity: schema.number(),
- }),
- },
- defaultActionGroupId: DURATION_ANOMALY.id,
- actionGroups: [
- {
- id: DURATION_ANOMALY.id,
- name: DURATION_ANOMALY.name,
- },
- ],
- actionVariables: {
- context: [],
- state: [...durationAnomalyTranslations.actionVariables, ...commonStateTranslations],
+) => ({
+ id: 'xpack.uptime.alerts.durationAnomaly',
+ producer: 'uptime',
+ name: durationAnomalyTranslations.alertFactoryName,
+ validate: {
+ params: schema.object({
+ monitorId: schema.string(),
+ severity: schema.number(),
+ }),
+ },
+ defaultActionGroupId: DURATION_ANOMALY.id,
+ actionGroups: [
+ {
+ id: DURATION_ANOMALY.id,
+ name: DURATION_ANOMALY.name,
},
- minimumLicenseRequired: 'basic',
- isExportable: true,
- async executor({ options, uptimeEsClient, savedObjectsClient, dynamicSettings }) {
- const {
- services: { alertInstanceFactory },
- state,
- params,
- } = options;
+ ],
+ actionVariables: {
+ context: [],
+ state: [...durationAnomalyTranslations.actionVariables, ...commonStateTranslations],
+ },
+ isExportable: true,
+ minimumLicenseRequired: 'platinum',
+ async executor({
+ params,
+ services: { alertWithLifecycle, scopedClusterClient, savedObjectsClient },
+ state,
+ }) {
+ const uptimeEsClient = createUptimeESClient({
+ esClient: scopedClusterClient.asCurrentUser,
+ savedObjectsClient,
+ });
+ const { anomalies } =
+ (await getAnomalies(plugins, savedObjectsClient, params, state.lastCheckedAt as string)) ??
+ {};
- const { anomalies } =
- (await getAnomalies(plugins, savedObjectsClient, params, state.lastCheckedAt)) ?? {};
+ const foundAnomalies = anomalies?.length > 0;
- const foundAnomalies = anomalies?.length > 0;
+ if (foundAnomalies) {
+ const monitorInfo = await libs.requests.getLatestMonitor({
+ uptimeEsClient,
+ dateStart: 'now-15m',
+ dateEnd: 'now',
+ monitorId: params.monitorId,
+ });
- if (foundAnomalies) {
- const monitorInfo = await getLatestMonitor({
- uptimeEsClient,
- dateStart: 'now-15m',
- dateEnd: 'now',
- monitorId: params.monitorId,
+ anomalies.forEach((anomaly, index) => {
+ const summary = getAnomalySummary(anomaly, monitorInfo);
+
+ const alertInstance = alertWithLifecycle({
+ id: DURATION_ANOMALY.id + index,
+ fields: {
+ 'monitor.id': params.monitorId,
+ 'url.full': summary.monitorUrl,
+ 'observer.geo.name': summary.observerLocation,
+ 'anomaly.start': summary.anomalyStartTimestamp,
+ 'anomaly.bucket_span.minutes': summary.bucketSpan,
+ [ALERT_EVALUATION_VALUE]: anomaly.actualSort,
+ [ALERT_EVALUATION_THRESHOLD]: anomaly.typicalSort,
+ [ALERT_SEVERITY_LEVEL]: summary.severity,
+ [ALERT_SEVERITY_VALUE]: summary.severityScore,
+ reason: generateAlertMessage(
+ CommonDurationAnomalyTranslations.defaultActionMessage,
+ summary
+ ),
+ },
});
- anomalies.forEach((anomaly, index) => {
- const alertInstance = alertInstanceFactory(DURATION_ANOMALY.id + index);
- const summary = getAnomalySummary(anomaly, monitorInfo);
- alertInstance.replaceState({
- ...updateState(state, false),
- ...summary,
- });
- alertInstance.scheduleActions(DURATION_ANOMALY.id);
+ alertInstance.replaceState({
+ ...updateState(state, false),
+ ...summary,
});
- }
+ alertInstance.scheduleActions(DURATION_ANOMALY.id);
+ });
+ }
- return updateState(state, foundAnomalies);
- },
- });
+ return updateState(state, foundAnomalies);
+ },
+});
diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts
index 743e9f6bc75a..dbb199a2e07d 100644
--- a/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts
+++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts
@@ -10,47 +10,99 @@ import {
statusCheckAlertFactory,
getStatusMessage,
getUniqueIdsByLoc,
+ getInstanceId,
} from './status_check';
-import {
- AlertType,
- AlertTypeParams,
- AlertTypeState,
- AlertInstanceState,
- AlertInstanceContext,
-} from '../../../../alerting/server';
-import { UMServerLibs } from '../lib';
-import { UptimeCorePlugins, UptimeCoreSetup } from '../adapters';
-import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants';
-import { alertsMock, AlertServicesMock } from '../../../../alerting/server/mocks';
import { GetMonitorStatusResult } from '../requests/get_monitor_status';
import { makePing } from '../../../common/runtime_types/ping';
import { GetMonitorAvailabilityResult } from '../requests/get_monitor_availability';
-import type { UptimeRouter } from '../../types';
-import { elasticsearchServiceMock } from 'src/core/server/mocks';
+import { DefaultUptimeAlertInstance } from './types';
+import { createRuleTypeMocks, bootstrapDependencies } from './test_utils';
-/**
- * The alert takes some dependencies as parameters; these are things like
- * kibana core services and plugins. This function helps reduce the amount of
- * boilerplate required.
- * @param customRequests client tests can use this paramter to provide their own request mocks,
- * so we don't have to mock them all for each test.
- */
-const bootstrapDependencies = (customRequests?: any) => {
- const router = {} as UptimeRouter;
- // these server/libs parameters don't have any functionality, which is fine
- // because we aren't testing them here
- const server: UptimeCoreSetup = { router };
- const plugins: UptimeCorePlugins = {} as any;
- const libs: UMServerLibs = { requests: {} } as UMServerLibs;
- libs.requests = { ...libs.requests, ...customRequests };
- return { server, libs, plugins };
+const mockMonitors = [
+ {
+ monitorId: 'first',
+ location: 'harrisburg',
+ count: 234,
+ status: 'down',
+ monitorInfo: {
+ ...makePing({
+ id: 'first',
+ location: 'harrisburg',
+ url: 'localhost:8080',
+ }),
+ error: {
+ message: 'error message 1',
+ },
+ },
+ },
+ {
+ monitorId: 'first',
+ location: 'fairbanks',
+ count: 234,
+ status: 'down',
+ monitorInfo: {
+ ...makePing({
+ id: 'first',
+ location: 'fairbanks',
+ url: 'localhost:5601',
+ }),
+ error: {
+ message: 'error message 2',
+ },
+ },
+ },
+];
+
+const mockCommonAlertDocumentFields = (monitorInfo: GetMonitorStatusResult['monitorInfo']) => ({
+ 'agent.name': monitorInfo.agent?.name,
+ 'error.message': monitorInfo.error?.message,
+ 'monitor.id': monitorInfo.monitor.id,
+ 'monitor.name': monitorInfo.monitor.name || monitorInfo.monitor.id,
+ 'monitor.type': monitorInfo.monitor.type,
+ 'url.full': monitorInfo.url?.full,
+ 'observer.geo.name': monitorInfo.observer?.geo?.name,
+});
+
+const mockStatusAlertDocument = (
+ monitor: GetMonitorStatusResult,
+ isAutoGenerated: boolean = false
+) => {
+ const { monitorInfo } = monitor;
+ return {
+ fields: {
+ ...mockCommonAlertDocumentFields(monitor.monitorInfo),
+ reason: `Monitor first with url ${monitorInfo?.url?.full} is down from ${
+ monitorInfo.observer?.geo?.name
+ }. The latest error message is ${monitorInfo.error?.message || ''}`,
+ },
+ id: getInstanceId(
+ monitorInfo,
+ `${isAutoGenerated ? '' : monitorInfo?.monitor.id + '-'}${monitorInfo.observer?.geo?.name}`
+ ),
+ };
+};
+
+const mockAvailabilityAlertDocument = (monitor: GetMonitorAvailabilityResult) => {
+ const { monitorInfo } = monitor;
+ return {
+ fields: {
+ ...mockCommonAlertDocumentFields(monitor.monitorInfo),
+ reason: `Monitor ${monitorInfo.monitor.name || monitorInfo.monitor.id} with url ${
+ monitorInfo?.url?.full
+ } is below threshold with ${(monitor.availabilityRatio! * 100).toFixed(
+ 2
+ )}% availability expected is 99.34% from ${
+ monitorInfo.observer?.geo?.name
+ }. The latest error message is ${monitorInfo.error?.message || ''}`,
+ },
+ id: getInstanceId(monitorInfo, `${monitorInfo?.monitor.id}-${monitorInfo.observer?.geo?.name}`),
+ };
};
/**
* This function aims to provide an easy way to give mock props that will
* reduce boilerplate for tests.
* @param params the params received at alert creation time
- * @param services the core services provided by kibana/alerting platforms
* @param state the state the alert maintains
*/
const mockOptions = (
@@ -60,7 +112,6 @@ const mockOptions = (
timerange: { from: 'now-15m', to: 'now' },
shouldCheckStatus: true,
},
- services = alertsMock.createAlertServices(),
state = {},
rule = {
schedule: {
@@ -68,19 +119,12 @@ const mockOptions = (
},
}
): any => {
- services.scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
- services.scopedClusterClient.asCurrentUser = (jest.fn() as unknown) as any;
+ const { services } = createRuleTypeMocks();
- services.savedObjectsClient.get.mockResolvedValue({
- id: '',
- type: '',
- references: [],
- attributes: DYNAMIC_SETTINGS_DEFAULTS,
- });
return {
params,
- services,
state,
+ services,
rule,
};
};
@@ -98,100 +142,134 @@ describe('status check alert', () => {
});
describe('executor', () => {
it('does not trigger when there are no monitors down', async () => {
- expect.assertions(4);
+ expect.assertions(5);
const mockGetter = jest.fn();
mockGetter.mockReturnValue([]);
const { server, libs, plugins } = bootstrapDependencies({ getMonitorStatus: mockGetter });
const alert = statusCheckAlertFactory(server, libs, plugins);
// @ts-ignore the executor can return `void`, but ours never does
- const state: Record = await alert.executor(mockOptions());
+ const options = mockOptions();
+ const state: Record = await alert.executor(options);
+ const {
+ services: { alertWithLifecycle },
+ } = options;
expect(state).not.toBeUndefined();
expect(state?.isTriggered).toBe(false);
+ expect(alertWithLifecycle).not.toHaveBeenCalled();
expect(mockGetter).toHaveBeenCalledTimes(1);
- expect(mockGetter.mock.calls[0]).toMatchInlineSnapshot(`
- Array [
- Object {
- "filters": undefined,
- "locations": Array [],
- "numTimes": 5,
- "timespanRange": Object {
- "from": "now-15m",
- "to": "now",
- },
- "timestampRange": Object {
- "from": 1620821917000,
- "to": "now",
- },
- "uptimeEsClient": Object {
- "baseESClient": [MockFunction],
- "count": [Function],
- "getSavedObjectsClient": [Function],
- "search": [Function],
- },
+ expect(mockGetter.mock.calls[0][0]).toEqual(
+ expect.objectContaining({
+ filters: undefined,
+ locations: [],
+ numTimes: 5,
+ timespanRange: {
+ from: 'now-15m',
+ to: 'now',
},
- ]
- `);
+ timestampRange: {
+ from: 1620821917000,
+ to: 'now',
+ },
+ })
+ );
});
it('triggers when monitors are down and provides expected state', async () => {
toISOStringSpy.mockImplementation(() => 'foo date string');
const mockGetter: jest.Mock = jest.fn();
- mockGetter.mockReturnValue([
- {
- monitorId: 'first',
- location: 'harrisburg',
- count: 234,
- status: 'down',
- monitorInfo: makePing({
- id: 'first',
- location: 'harrisburg',
- }),
- },
- {
- monitorId: 'first',
- location: 'fairbanks',
- count: 234,
- status: 'down',
- monitorInfo: makePing({
- id: 'first',
- location: 'fairbanks',
- }),
- },
- ]);
+ mockGetter.mockReturnValue(mockMonitors);
const { server, libs, plugins } = bootstrapDependencies({ getMonitorStatus: mockGetter });
const alert = statusCheckAlertFactory(server, libs, plugins);
const options = mockOptions();
- const alertServices: AlertServicesMock = options.services;
+ const {
+ services: { alertWithLifecycle },
+ } = options;
// @ts-ignore the executor can return `void`, but ours never does
const state: Record = await alert.executor(options);
expect(mockGetter).toHaveBeenCalledTimes(1);
- expect(alertServices.alertInstanceFactory).toHaveBeenCalledTimes(2);
- expect(mockGetter.mock.calls[0]).toMatchInlineSnapshot(`
+ expect(alertWithLifecycle).toHaveBeenCalledTimes(2);
+ mockMonitors.forEach((monitor) => {
+ expect(alertWithLifecycle).toBeCalledWith(mockStatusAlertDocument(monitor));
+ });
+ expect(mockGetter.mock.calls[0][0]).toEqual(
+ expect.objectContaining({
+ filters: undefined,
+ locations: [],
+ numTimes: 5,
+ timespanRange: {
+ from: 'now-15m',
+ to: 'now',
+ },
+ })
+ );
+ const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results;
+ expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(2);
+ expect(alertInstanceMock.replaceState.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
- "filters": undefined,
- "locations": Array [],
- "numTimes": 5,
- "timespanRange": Object {
- "from": "now-15m",
- "to": "now",
- },
- "timestampRange": Object {
- "from": 1620821917000,
- "to": "now",
- },
- "uptimeEsClient": Object {
- "baseESClient": [MockFunction],
- "count": [Function],
- "getSavedObjectsClient": [Function],
- "search": [Function],
- },
+ "currentTriggerStarted": "foo date string",
+ "firstCheckedAt": "foo date string",
+ "firstTriggeredAt": "foo date string",
+ "isTriggered": true,
+ "lastCheckedAt": "foo date string",
+ "lastResolvedAt": undefined,
+ "lastTriggeredAt": "foo date string",
+ "latestErrorMessage": "error message 1",
+ "monitorId": "first",
+ "monitorName": "first",
+ "monitorType": "myType",
+ "monitorUrl": "localhost:8080",
+ "observerHostname": undefined,
+ "observerLocation": "harrisburg",
+ "reason": "Monitor first with url localhost:8080 is down from harrisburg. The latest error message is error message 1",
+ "statusMessage": "down",
},
]
`);
- const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results;
+ expect(alertInstanceMock.scheduleActions).toHaveBeenCalledTimes(2);
+ expect(alertInstanceMock.scheduleActions.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "xpack.uptime.alerts.actionGroups.monitorStatus",
+ ]
+ `);
+ });
+
+ it('supports auto generated monitor status alerts', async () => {
+ toISOStringSpy.mockImplementation(() => 'foo date string');
+ const mockGetter: jest.Mock = jest.fn();
+
+ mockGetter.mockReturnValue(mockMonitors);
+ const { server, libs, plugins } = bootstrapDependencies({ getMonitorStatus: mockGetter });
+ const alert = statusCheckAlertFactory(server, libs, plugins);
+ const options = mockOptions({
+ isAutoGenerated: true,
+ timerange: { from: 'now-15m', to: 'now' },
+ numTimes: 5,
+ });
+ const {
+ services: { alertWithLifecycle },
+ } = options;
+ // @ts-ignore the executor can return `void`, but ours never does
+ const state: Record = await alert.executor(options);
+ expect(mockGetter).toHaveBeenCalledTimes(1);
+ expect(alertWithLifecycle).toHaveBeenCalledTimes(2);
+ mockMonitors.forEach((monitor) => {
+ expect(alertWithLifecycle).toBeCalledWith(mockStatusAlertDocument(monitor, true));
+ });
+ expect(mockGetter.mock.calls[0][0]).toEqual(
+ expect.objectContaining({
+ filters: undefined,
+ locations: [],
+ numTimes: 5,
+ timespanRange: {
+ from: 'now-15m',
+ to: 'now',
+ },
+ })
+ );
+ const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results;
expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(2);
expect(alertInstanceMock.replaceState.mock.calls[0]).toMatchInlineSnapshot(`
Array [
@@ -203,13 +281,14 @@ describe('status check alert', () => {
"lastCheckedAt": "foo date string",
"lastResolvedAt": undefined,
"lastTriggeredAt": "foo date string",
- "latestErrorMessage": undefined,
+ "latestErrorMessage": "error message 1",
"monitorId": "first",
"monitorName": "first",
"monitorType": "myType",
- "monitorUrl": undefined,
+ "monitorUrl": "localhost:8080",
"observerHostname": undefined,
"observerLocation": "harrisburg",
+ "reason": "Monitor first with url localhost:8080 is down from harrisburg. The latest error message is error message 1",
"statusMessage": "down",
},
]
@@ -218,9 +297,6 @@ describe('status check alert', () => {
expect(alertInstanceMock.scheduleActions.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"xpack.uptime.alerts.actionGroups.monitorStatus",
- Object {
- "message": "Monitor first with url is down from harrisburg. The latest error message is ",
- },
]
`);
});
@@ -229,29 +305,7 @@ describe('status check alert', () => {
toISOStringSpy.mockImplementation(() => '7.7 date');
const mockGetter: jest.Mock = jest.fn();
- mockGetter.mockReturnValue([
- {
- monitorId: 'first',
- location: 'harrisburg',
- count: 234,
- status: 'down',
- monitorInfo: makePing({
- id: 'first',
- location: 'harrisburg',
- }),
- },
- {
- monitorId: 'first',
- location: 'fairbanks',
- count: 234,
- status: 'down',
-
- monitorInfo: makePing({
- id: 'first',
- location: 'fairbanks',
- }),
- },
- ]);
+ mockGetter.mockReturnValue(mockMonitors);
const { server, libs, plugins } = bootstrapDependencies({
getMonitorStatus: mockGetter,
getIndexPattern: jest.fn(),
@@ -259,14 +313,19 @@ describe('status check alert', () => {
const alert = statusCheckAlertFactory(server, libs, plugins);
const options = mockOptions({
numTimes: 4,
- timerange: { from: 'now-14h', to: 'now' },
+ timespanRange: { from: 'now-14h', to: 'now' },
locations: ['fairbanks'],
filters: '',
});
- const alertServices: AlertServicesMock = options.services;
+ const {
+ services: { alertWithLifecycle },
+ } = options;
const state = await alert.executor(options);
- const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results;
+ const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results;
+ mockMonitors.forEach((monitor) => {
+ expect(alertWithLifecycle).toBeCalledWith(mockStatusAlertDocument(monitor));
+ });
expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(2);
expect(alertInstanceMock.replaceState.mock.calls[0]).toMatchInlineSnapshot(`
Array [
@@ -278,13 +337,14 @@ describe('status check alert', () => {
"lastCheckedAt": "7.7 date",
"lastResolvedAt": undefined,
"lastTriggeredAt": "7.7 date",
- "latestErrorMessage": undefined,
+ "latestErrorMessage": "error message 1",
"monitorId": "first",
"monitorName": "first",
"monitorType": "myType",
- "monitorUrl": undefined,
+ "monitorUrl": "localhost:8080",
"observerHostname": undefined,
"observerLocation": "harrisburg",
+ "reason": "Monitor first with url localhost:8080 is down from harrisburg. The latest error message is error message 1",
"statusMessage": "down",
},
]
@@ -303,33 +363,11 @@ describe('status check alert', () => {
});
it('supports 7.8 alert format', async () => {
- expect.assertions(5);
+ expect.assertions(8);
toISOStringSpy.mockImplementation(() => 'foo date string');
const mockGetter: jest.Mock = jest.fn();
- mockGetter.mockReturnValueOnce([
- {
- monitorId: 'first',
- location: 'harrisburg',
- count: 234,
- status: 'down',
- monitorInfo: makePing({
- id: 'first',
- location: 'harrisburg',
- }),
- },
- {
- monitorId: 'first',
- location: 'fairbanks',
- count: 234,
- status: 'down',
-
- monitorInfo: makePing({
- id: 'first',
- location: 'fairbanks',
- }),
- },
- ]);
+ mockGetter.mockReturnValueOnce(mockMonitors);
const { server, libs, plugins } = bootstrapDependencies({
getMonitorStatus: mockGetter,
getIndexPattern: jest.fn(),
@@ -347,166 +385,160 @@ describe('status check alert', () => {
tags: ['unsecured', 'containers', 'org:google'],
},
});
- const alertServices: AlertServicesMock = options.services;
const state = await alert.executor(options);
- const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results;
+ const {
+ services: { alertWithLifecycle },
+ } = options;
+ const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results;
+ mockMonitors.forEach((monitor) => {
+ expect(alertWithLifecycle).toBeCalledWith(mockStatusAlertDocument(monitor));
+ });
expect(mockGetter).toHaveBeenCalledTimes(1);
-
- expect(mockGetter.mock.calls[0]).toMatchInlineSnapshot(`
- Array [
- Object {
- "filters": Object {
- "bool": Object {
- "filter": Array [
- Object {
- "bool": Object {
- "filter": Array [
- Object {
- "bool": Object {
- "minimum_should_match": 1,
- "should": Array [
- Object {
- "bool": Object {
- "minimum_should_match": 1,
- "should": Array [
- Object {
- "match": Object {
- "url.port": "12349",
- },
- },
- ],
- },
- },
- Object {
- "bool": Object {
- "minimum_should_match": 1,
- "should": Array [
- Object {
- "match": Object {
- "url.port": "5601",
- },
- },
- ],
- },
- },
- Object {
- "bool": Object {
- "minimum_should_match": 1,
- "should": Array [
- Object {
- "match": Object {
- "url.port": "443",
- },
- },
- ],
+ expect(mockGetter.mock.calls[0][0]).toEqual(
+ expect.objectContaining({
+ locations: [],
+ numTimes: 3,
+ timespanRange: {
+ from: 'now-15m',
+ to: 'now',
+ },
+ })
+ );
+ expect(mockGetter.mock.calls[0][0].filters).toMatchInlineSnapshot(`
+ Object {
+ "bool": Object {
+ "filter": Array [
+ Object {
+ "bool": Object {
+ "filter": Array [
+ Object {
+ "bool": Object {
+ "minimum_should_match": 1,
+ "should": Array [
+ Object {
+ "bool": Object {
+ "minimum_should_match": 1,
+ "should": Array [
+ Object {
+ "match": Object {
+ "url.port": "12349",
+ },
},
- },
- ],
+ ],
+ },
},
- },
- Object {
- "bool": Object {
- "minimum_should_match": 1,
- "should": Array [
- Object {
- "match": Object {
- "observer.geo.name": "harrisburg",
+ Object {
+ "bool": Object {
+ "minimum_should_match": 1,
+ "should": Array [
+ Object {
+ "match": Object {
+ "url.port": "5601",
+ },
},
- },
- ],
+ ],
+ },
},
- },
- Object {
- "bool": Object {
- "minimum_should_match": 1,
- "should": Array [
- Object {
- "match": Object {
- "monitor.type": "http",
+ Object {
+ "bool": Object {
+ "minimum_should_match": 1,
+ "should": Array [
+ Object {
+ "match": Object {
+ "url.port": "443",
+ },
},
- },
- ],
+ ],
+ },
+ },
+ ],
+ },
+ },
+ Object {
+ "bool": Object {
+ "minimum_should_match": 1,
+ "should": Array [
+ Object {
+ "match": Object {
+ "observer.geo.name": "harrisburg",
+ },
+ },
+ ],
+ },
+ },
+ Object {
+ "bool": Object {
+ "minimum_should_match": 1,
+ "should": Array [
+ Object {
+ "match": Object {
+ "monitor.type": "http",
+ },
},
- },
- Object {
- "bool": Object {
- "minimum_should_match": 1,
- "should": Array [
- Object {
- "bool": Object {
- "minimum_should_match": 1,
- "should": Array [
- Object {
- "match": Object {
- "tags": "unsecured",
- },
- },
- ],
+ ],
+ },
+ },
+ Object {
+ "bool": Object {
+ "minimum_should_match": 1,
+ "should": Array [
+ Object {
+ "bool": Object {
+ "minimum_should_match": 1,
+ "should": Array [
+ Object {
+ "match": Object {
+ "tags": "unsecured",
+ },
},
- },
- Object {
- "bool": Object {
- "minimum_should_match": 1,
- "should": Array [
- Object {
- "match": Object {
- "tags": "containers",
- },
- },
- ],
+ ],
+ },
+ },
+ Object {
+ "bool": Object {
+ "minimum_should_match": 1,
+ "should": Array [
+ Object {
+ "match": Object {
+ "tags": "containers",
+ },
},
- },
- Object {
- "bool": Object {
- "minimum_should_match": 1,
- "should": Array [
- Object {
- "match_phrase": Object {
- "tags": "org:google",
- },
- },
- ],
+ ],
+ },
+ },
+ Object {
+ "bool": Object {
+ "minimum_should_match": 1,
+ "should": Array [
+ Object {
+ "match_phrase": Object {
+ "tags": "org:google",
+ },
},
- },
- ],
+ ],
+ },
},
- },
- ],
+ ],
+ },
},
- },
- Object {
- "bool": Object {
- "minimum_should_match": 1,
- "should": Array [
- Object {
- "exists": Object {
- "field": "monitor.ip",
- },
- },
- ],
+ ],
+ },
+ },
+ Object {
+ "bool": Object {
+ "minimum_should_match": 1,
+ "should": Array [
+ Object {
+ "exists": Object {
+ "field": "monitor.ip",
+ },
},
- },
- ],
+ ],
+ },
},
- },
- "locations": Array [],
- "numTimes": 3,
- "timespanRange": Object {
- "from": "now-15m",
- "to": "now",
- },
- "timestampRange": Object {
- "from": 1620821917000,
- "to": "now",
- },
- "uptimeEsClient": Object {
- "baseESClient": [MockFunction],
- "count": [Function],
- "getSavedObjectsClient": [Function],
- "search": [Function],
- },
+ ],
},
- ]
+ }
`);
expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(2);
expect(alertInstanceMock.replaceState.mock.calls[0]).toMatchInlineSnapshot(`
@@ -519,13 +551,14 @@ describe('status check alert', () => {
"lastCheckedAt": "foo date string",
"lastResolvedAt": undefined,
"lastTriggeredAt": "foo date string",
- "latestErrorMessage": undefined,
+ "latestErrorMessage": "error message 1",
"monitorId": "first",
"monitorName": "first",
"monitorType": "myType",
- "monitorUrl": undefined,
+ "monitorUrl": "localhost:8080",
"observerHostname": undefined,
"observerLocation": "harrisburg",
+ "reason": "Monitor first with url localhost:8080 is down from harrisburg. The latest error message is error message 1",
"statusMessage": "down",
},
]
@@ -567,67 +600,56 @@ describe('status check alert', () => {
await alert.executor(options);
expect(mockGetter).toHaveBeenCalledTimes(1);
- expect(mockGetter.mock.calls[0]).toMatchInlineSnapshot(`
- Array [
- Object {
- "filters": Object {
- "bool": Object {
- "filter": Array [
- Object {
- "bool": Object {
- "minimum_should_match": 1,
- "should": Array [
- Object {
- "match": Object {
- "monitor.type": "http",
- },
- },
- ],
+ expect(mockGetter.mock.calls[0][0].filters).toMatchInlineSnapshot(`
+ Object {
+ "bool": Object {
+ "filter": Array [
+ Object {
+ "bool": Object {
+ "minimum_should_match": 1,
+ "should": Array [
+ Object {
+ "match": Object {
+ "monitor.type": "http",
+ },
},
- },
- Object {
- "bool": Object {
- "minimum_should_match": 1,
- "should": Array [
- Object {
- "exists": Object {
- "field": "url.full",
- },
- },
- ],
+ ],
+ },
+ },
+ Object {
+ "bool": Object {
+ "minimum_should_match": 1,
+ "should": Array [
+ Object {
+ "exists": Object {
+ "field": "url.full",
+ },
},
- },
- ],
+ ],
+ },
},
- },
- "locations": Array [],
- "numTimes": 20,
- "timespanRange": Object {
- "from": "now-30h",
- "to": "now",
- },
- "timestampRange": Object {
- "from": 1620714817000,
- "to": "now",
- },
- "uptimeEsClient": Object {
- "baseESClient": [MockFunction],
- "count": [Function],
- "getSavedObjectsClient": [Function],
- "search": [Function],
- },
+ ],
},
- ]
+ }
`);
+ expect(mockGetter.mock.calls[0][0]).toEqual(
+ expect.objectContaining({
+ locations: [],
+ numTimes: 20,
+ timespanRange: {
+ from: 'now-30h',
+ to: 'now',
+ },
+ })
+ );
});
it('supports availability checks', async () => {
- expect.assertions(8);
+ expect.assertions(13);
toISOStringSpy.mockImplementation(() => 'availability test');
const mockGetter: jest.Mock = jest.fn();
mockGetter.mockReturnValue([]);
- const mockAvailability: jest.Mock = jest.fn();
- mockAvailability.mockReturnValue([
+ const mockAvailabilityMonitors = [
{
monitorId: 'foo',
location: 'harrisburg',
@@ -679,7 +701,9 @@ describe('status check alert', () => {
url: 'https://no-name.co',
}),
},
- ]);
+ ];
+ const mockAvailability: jest.Mock = jest.fn();
+ mockAvailability.mockReturnValue(mockAvailabilityMonitors);
const { server, libs, plugins } = bootstrapDependencies({
getMonitorAvailability: mockAvailability,
getMonitorStatus: mockGetter,
@@ -701,9 +725,14 @@ describe('status check alert', () => {
shouldCheckAvailability: true,
shouldCheckStatus: false,
});
- const alertServices: AlertServicesMock = options.services;
+ const {
+ services: { alertWithLifecycle },
+ } = options;
const state = await alert.executor(options);
- const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results;
+ const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results;
+ mockAvailabilityMonitors.forEach((monitor) => {
+ expect(alertWithLifecycle).toBeCalledWith(mockAvailabilityAlertDocument(monitor));
+ });
expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(4);
expect(alertInstanceMock.replaceState.mock.calls[0]).toMatchInlineSnapshot(`
Array [
@@ -722,6 +751,7 @@ describe('status check alert', () => {
"monitorUrl": "https://foo.com",
"observerHostname": undefined,
"observerLocation": "harrisburg",
+ "reason": "Monitor Foo with url https://foo.com is below threshold with 99.28% availability expected is 99.34% from harrisburg. The latest error message is ",
"statusMessage": "below threshold with 99.28% availability expected is 99.34%",
},
]
@@ -731,48 +761,30 @@ describe('status check alert', () => {
Array [
Array [
"xpack.uptime.alerts.actionGroups.monitorStatus",
- Object {
- "message": "Monitor Foo with url https://foo.com is below threshold with 99.28% availability expected is 99.34% from harrisburg. The latest error message is ",
- },
],
Array [
"xpack.uptime.alerts.actionGroups.monitorStatus",
- Object {
- "message": "Monitor Foo with url https://foo.com is below threshold with 98.03% availability expected is 99.34% from fairbanks. The latest error message is ",
- },
],
Array [
"xpack.uptime.alerts.actionGroups.monitorStatus",
- Object {
- "message": "Monitor Unreliable with url https://unreliable.co is below threshold with 90.92% availability expected is 99.34% from fairbanks. The latest error message is ",
- },
],
Array [
"xpack.uptime.alerts.actionGroups.monitorStatus",
- Object {
- "message": "Monitor no-name with url https://no-name.co is below threshold with 90.92% availability expected is 99.34% from fairbanks. The latest error message is ",
- },
],
]
`);
expect(mockGetter).not.toHaveBeenCalled();
expect(mockAvailability).toHaveBeenCalledTimes(1);
- expect(mockAvailability.mock.calls[0]).toMatchInlineSnapshot(`
- Array [
- Object {
- "filters": "{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":\\"12349\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":\\"5601\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":\\"443\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"observer.geo.name\\":\\"harrisburg\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"monitor.type\\":\\"http\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"tags\\":\\"unsecured\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"tags\\":\\"containers\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"tags\\":\\"org:google\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}]}}",
- "range": 35,
- "rangeUnit": "d",
- "threshold": "99.34",
- "uptimeEsClient": Object {
- "baseESClient": [MockFunction],
- "count": [Function],
- "getSavedObjectsClient": [Function],
- "search": [Function],
- },
- },
- ]
- `);
+ expect(mockAvailability.mock.calls[0][0].filters).toMatchInlineSnapshot(
+ `"{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":\\"12349\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":\\"5601\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":\\"443\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"observer.geo.name\\":\\"harrisburg\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"monitor.type\\":\\"http\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"tags\\":\\"unsecured\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"tags\\":\\"containers\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"tags\\":\\"org:google\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}]}}"`
+ );
+ expect(mockAvailability.mock.calls[0][0]).toEqual(
+ expect.objectContaining({
+ range: 35,
+ rangeUnit: 'd',
+ threshold: '99.34',
+ })
+ );
expect(state).toMatchInlineSnapshot(`
Object {
"currentTriggerStarted": undefined,
@@ -787,7 +799,7 @@ describe('status check alert', () => {
});
it('supports availability checks with search', async () => {
- expect.assertions(2);
+ expect.assertions(3);
toISOStringSpy.mockImplementation(() => 'availability with search');
const mockGetter = jest.fn();
mockGetter.mockReturnValue([]);
@@ -811,22 +823,16 @@ describe('status check alert', () => {
await alert.executor(options);
expect(mockAvailability).toHaveBeenCalledTimes(1);
- expect(mockAvailability.mock.calls[0]).toMatchInlineSnapshot(`
- Array [
- Object {
- "filters": "{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\":\\"ur.port\\"}}],\\"minimum_should_match\\":1}}",
- "range": 23,
- "rangeUnit": "w",
- "threshold": "90",
- "uptimeEsClient": Object {
- "baseESClient": [MockFunction],
- "count": [Function],
- "getSavedObjectsClient": [Function],
- "search": [Function],
- },
- },
- ]
- `);
+ expect(mockAvailability.mock.calls[0][0].filters).toMatchInlineSnapshot(
+ `"{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\":\\"ur.port\\"}}],\\"minimum_should_match\\":1}}"`
+ );
+ expect(mockAvailability.mock.calls[0][0]).toEqual(
+ expect.objectContaining({
+ range: 23,
+ rangeUnit: 'w',
+ threshold: '90',
+ })
+ );
});
it('supports availability checks with no filter or search', async () => {
@@ -841,12 +847,13 @@ describe('status check alert', () => {
getIndexPattern: jest.fn(),
});
const alert = statusCheckAlertFactory(server, libs, plugins);
+ const availability = {
+ range: 23,
+ rangeUnit: 'w',
+ threshold: '90',
+ };
const options = mockOptions({
- availability: {
- range: 23,
- rangeUnit: 'w',
- threshold: '90',
- },
+ availability,
shouldCheckAvailability: true,
shouldCheckStatus: false,
});
@@ -854,34 +861,20 @@ describe('status check alert', () => {
await alert.executor(options);
expect(mockAvailability).toHaveBeenCalledTimes(1);
- expect(mockAvailability.mock.calls[0]).toMatchInlineSnapshot(`
- Array [
- Object {
- "filters": undefined,
- "range": 23,
- "rangeUnit": "w",
- "threshold": "90",
- "uptimeEsClient": Object {
- "baseESClient": [MockFunction],
- "count": [Function],
- "getSavedObjectsClient": [Function],
- "search": [Function],
- },
- },
- ]
- `);
+ expect(mockAvailability.mock.calls[0][0]).toEqual(
+ expect.objectContaining({
+ filters: undefined,
+ range: availability.range,
+ rangeUnit: availability.rangeUnit,
+ threshold: availability.threshold,
+ })
+ );
});
});
describe('alert factory', () => {
// @ts-ignore
- let alert: AlertType<
- AlertTypeParams,
- AlertTypeState,
- AlertInstanceState,
- AlertInstanceContext,
- 'xpack.uptime.alerts.actionGroups.monitorStatus'
- >;
+ let alert: DefaultUptimeAlertInstance;
beforeEach(() => {
const { server, libs, plugins } = bootstrapDependencies();
@@ -982,7 +975,6 @@ describe('status check alert', () => {
search: 'url.full: *',
},
undefined,
- undefined,
{ schedule: { interval: '60h' } }
);
await alert.executor(options);
diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts
index 364518bba720..249eaa33ec24 100644
--- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts
+++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts
@@ -4,13 +4,11 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import datemath from '@elastic/datemath';
import { min } from 'lodash';
+import datemath from '@elastic/datemath';
import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
-import Mustache from 'mustache';
import { JsonObject } from '@kbn/common-utils';
-import { ActionGroupIdsOf } from '../../../../alerting/common';
import { UptimeAlertTypeFactory } from './types';
import { esKuery } from '../../../../../../src/plugins/data/server';
import {
@@ -19,16 +17,16 @@ import {
GetMonitorAvailabilityParams,
} from '../../../common/runtime_types';
import { MONITOR_STATUS } from '../../../common/constants/alerts';
-import { updateState } from './common';
+import { updateState, generateAlertMessage } from './common';
import { commonMonitorStateI18, commonStateTranslations, DOWN_LABEL } from './translations';
import { stringifyKueries, combineFiltersAndUserSearch } from '../../../common/lib';
import { GetMonitorAvailabilityResult } from '../requests/get_monitor_availability';
import { GetMonitorStatusResult } from '../requests/get_monitor_status';
import { UNNAMED_LOCATION } from '../../../common/constants';
-import { uptimeAlertWrapper } from './uptime_alert_wrapper';
import { MonitorStatusTranslations } from '../../../common/translations';
import { getUptimeIndexPattern, IndexPatternTitleAndFields } from '../requests/get_index_pattern';
-import { UMServerLibs, UptimeESClient } from '../lib';
+import { UMServerLibs, UptimeESClient, createUptimeESClient } from '../lib';
+import { ActionGroupIdsOf } from '../../../../alerting/common';
export type ActionGroupIds = ActionGroupIdsOf;
@@ -134,8 +132,8 @@ export const formatFilterString = async (
search
);
-export const getMonitorSummary = (monitorInfo: Ping) => {
- return {
+export const getMonitorSummary = (monitorInfo: Ping, statusMessage: string) => {
+ const summary = {
monitorUrl: monitorInfo.url?.full,
monitorId: monitorInfo.monitor?.id,
monitorName: monitorInfo.monitor?.name ?? monitorInfo.monitor?.id,
@@ -144,16 +142,26 @@ export const getMonitorSummary = (monitorInfo: Ping) => {
observerLocation: monitorInfo.observer?.geo?.name ?? UNNAMED_LOCATION,
observerHostname: monitorInfo.agent?.name,
};
+ const reason = generateAlertMessage(MonitorStatusTranslations.defaultActionMessage, {
+ ...summary,
+ statusMessage,
+ });
+ return {
+ ...summary,
+ reason,
+ };
};
-const generateMessageForOlderVersions = (fields: Record) => {
- const messageTemplate = MonitorStatusTranslations.defaultActionMessage;
-
- // Monitor {{state.monitorName}} with url {{{state.monitorUrl}}} is {{state.statusMessage}} from
- // {{state.observerLocation}}. The latest error message is {{{state.latestErrorMessage}}}
-
- return Mustache.render(messageTemplate, { state: { ...fields } });
-};
+export const getMonitorAlertDocument = (monitorSummary: Record) => ({
+ 'monitor.id': monitorSummary.monitorId,
+ 'monitor.type': monitorSummary.monitorType,
+ 'monitor.name': monitorSummary.monitorName,
+ 'url.full': monitorSummary.monitorUrl,
+ 'observer.geo.name': monitorSummary.observerLocation,
+ 'error.message': monitorSummary.latestErrorMessage,
+ 'agent.name': monitorSummary.observerHostname,
+ reason: monitorSummary.reason,
+});
export const getStatusMessage = (
downMonInfo?: Ping,
@@ -194,7 +202,7 @@ export const getStatusMessage = (
return statusMessage + availabilityMessage;
};
-const getInstanceId = (monitorInfo: Ping, monIdByLoc: string) => {
+export const getInstanceId = (monitorInfo: Ping, monIdByLoc: string) => {
const normalizeText = (txt: string) => {
// replace url and name special characters with -
return txt.replace(/[^A-Z0-9]+/gi, '_').toLowerCase();
@@ -209,200 +217,204 @@ const getInstanceId = (monitorInfo: Ping, monIdByLoc: string) => {
return `${urlText}_${monIdByLoc}`;
};
-export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) =>
- uptimeAlertWrapper({
- id: 'xpack.uptime.alerts.monitorStatus',
- name: i18n.translate('xpack.uptime.alerts.monitorStatus', {
- defaultMessage: 'Uptime monitor status',
- }),
- validate: {
- params: schema.object({
- availability: schema.maybe(
- schema.object({
- range: schema.number(),
- rangeUnit: schema.string(),
- threshold: schema.string(),
- })
- ),
- filters: schema.maybe(
- schema.oneOf([
- // deprecated
- schema.object({
- 'monitor.type': schema.maybe(schema.arrayOf(schema.string())),
- 'observer.geo.name': schema.maybe(schema.arrayOf(schema.string())),
- tags: schema.maybe(schema.arrayOf(schema.string())),
- 'url.port': schema.maybe(schema.arrayOf(schema.string())),
- }),
- schema.string(),
- ])
- ),
- // deprecated
- locations: schema.maybe(schema.arrayOf(schema.string())),
- numTimes: schema.number(),
- search: schema.maybe(schema.string()),
- shouldCheckStatus: schema.boolean(),
- shouldCheckAvailability: schema.boolean(),
- timerangeCount: schema.maybe(schema.number()),
- timerangeUnit: schema.maybe(schema.string()),
- // deprecated
- timerange: schema.maybe(
+export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) => ({
+ id: 'xpack.uptime.alerts.monitorStatus',
+ producer: 'uptime',
+ name: i18n.translate('xpack.uptime.alerts.monitorStatus', {
+ defaultMessage: 'Uptime monitor status',
+ }),
+ validate: {
+ params: schema.object({
+ availability: schema.maybe(
+ schema.object({
+ range: schema.number(),
+ rangeUnit: schema.string(),
+ threshold: schema.string(),
+ })
+ ),
+ filters: schema.maybe(
+ schema.oneOf([
+ // deprecated
schema.object({
- from: schema.string(),
- to: schema.string(),
- })
- ),
- version: schema.maybe(schema.number()),
- isAutoGenerated: schema.maybe(schema.boolean()),
- }),
+ 'monitor.type': schema.maybe(schema.arrayOf(schema.string())),
+ 'observer.geo.name': schema.maybe(schema.arrayOf(schema.string())),
+ tags: schema.maybe(schema.arrayOf(schema.string())),
+ 'url.port': schema.maybe(schema.arrayOf(schema.string())),
+ }),
+ schema.string(),
+ ])
+ ),
+ // deprecated
+ locations: schema.maybe(schema.arrayOf(schema.string())),
+ numTimes: schema.number(),
+ search: schema.maybe(schema.string()),
+ shouldCheckStatus: schema.boolean(),
+ shouldCheckAvailability: schema.boolean(),
+ timerangeCount: schema.maybe(schema.number()),
+ timerangeUnit: schema.maybe(schema.string()),
+ // deprecated
+ timerange: schema.maybe(
+ schema.object({
+ from: schema.string(),
+ to: schema.string(),
+ })
+ ),
+ version: schema.maybe(schema.number()),
+ isAutoGenerated: schema.maybe(schema.boolean()),
+ }),
+ },
+ defaultActionGroupId: MONITOR_STATUS.id,
+ actionGroups: [
+ {
+ id: MONITOR_STATUS.id,
+ name: MONITOR_STATUS.name,
},
- defaultActionGroupId: MONITOR_STATUS.id,
- actionGroups: [
+ ],
+ actionVariables: {
+ context: [
{
- id: MONITOR_STATUS.id,
- name: MONITOR_STATUS.name,
+ name: 'message',
+ description: i18n.translate(
+ 'xpack.uptime.alerts.monitorStatus.actionVariables.context.message.description',
+ {
+ defaultMessage: 'A generated message summarizing the currently down monitors',
+ }
+ ),
+ },
+ {
+ name: 'downMonitorsWithGeo',
+ description: i18n.translate(
+ 'xpack.uptime.alerts.monitorStatus.actionVariables.context.downMonitorsWithGeo.description',
+ {
+ defaultMessage:
+ 'A generated summary that shows some or all of the monitors detected as "down" by the alert',
+ }
+ ),
},
],
- actionVariables: {
- context: [
- {
- name: 'message',
- description: i18n.translate(
- 'xpack.uptime.alerts.monitorStatus.actionVariables.context.message.description',
- {
- defaultMessage: 'A generated message summarizing the currently down monitors',
- }
- ),
- },
- {
- name: 'downMonitorsWithGeo',
- description: i18n.translate(
- 'xpack.uptime.alerts.monitorStatus.actionVariables.context.downMonitorsWithGeo.description',
- {
- defaultMessage:
- 'A generated summary that shows some or all of the monitors detected as "down" by the alert',
- }
- ),
- },
- ],
- state: [...commonMonitorStateI18, ...commonStateTranslations],
+ state: [...commonMonitorStateI18, ...commonStateTranslations],
+ },
+ isExportable: true,
+ minimumLicenseRequired: 'basic',
+ async executor({
+ params: rawParams,
+ state,
+ services: { savedObjectsClient, scopedClusterClient, alertWithLifecycle },
+ rule: {
+ schedule: { interval },
},
- minimumLicenseRequired: 'basic',
- isExportable: true,
- async executor({
- options: {
- params: rawParams,
- state,
- services: { alertInstanceFactory },
- rule: {
- schedule: { interval },
- },
- },
- uptimeEsClient,
- }) {
- const {
- filters,
- search,
+ }) {
+ const {
+ filters,
+ search,
+ numTimes,
+ timerangeCount,
+ timerangeUnit,
+ availability,
+ shouldCheckAvailability,
+ shouldCheckStatus,
+ isAutoGenerated,
+ timerange: oldVersionTimeRange,
+ } = rawParams;
+ const uptimeEsClient = createUptimeESClient({
+ esClient: scopedClusterClient.asCurrentUser,
+ savedObjectsClient,
+ });
+
+ const filterString = await formatFilterString(uptimeEsClient, filters, search, libs);
+
+ const timespanInterval = `${String(timerangeCount)}${timerangeUnit}`;
+
+ // Range filter for `monitor.timespan`, the range of time the ping is valid
+ const timespanRange = oldVersionTimeRange || {
+ from: `now-${timespanInterval}`,
+ to: 'now',
+ };
+
+ // Range filter for `@timestamp`, the time the document was indexed
+ const timestampRange = getTimestampRange({
+ ruleScheduleLookback: `now-${interval}`,
+ timerangeLookback: timespanRange.from,
+ });
+
+ let downMonitorsByLocation: GetMonitorStatusResult[] = [];
+
+ // if oldVersionTimeRange present means it's 7.7 format and
+ // after that shouldCheckStatus should be explicitly false
+ if (!(!oldVersionTimeRange && shouldCheckStatus === false)) {
+ downMonitorsByLocation = await libs.requests.getMonitorStatus({
+ uptimeEsClient,
+ timespanRange,
+ timestampRange,
numTimes,
- timerangeCount,
- timerangeUnit,
- availability,
- shouldCheckAvailability,
- shouldCheckStatus,
- isAutoGenerated,
- timerange: oldVersionTimeRange,
- } = rawParams;
- const filterString = await formatFilterString(uptimeEsClient, filters, search, libs);
-
- const timespanInterval = `${String(timerangeCount)}${timerangeUnit}`;
-
- // Range filter for `monitor.timespan`, the range of time the ping is valid
- const timespanRange = oldVersionTimeRange || {
- from: `now-${timespanInterval}`,
- to: 'now',
- };
-
- // Range filter for `@timestamp`, the time the document was indexed
- const timestampRange = getTimestampRange({
- ruleScheduleLookback: `now-${interval}`,
- timerangeLookback: timespanRange.from,
+ locations: [],
+ filters: filterString,
});
+ }
- let downMonitorsByLocation: GetMonitorStatusResult[] = [];
-
- // if oldVersionTimeRange present means it's 7.7 format and
- // after that shouldCheckStatus should be explicitly false
- if (!(!oldVersionTimeRange && shouldCheckStatus === false)) {
- downMonitorsByLocation = await libs.requests.getMonitorStatus({
- uptimeEsClient,
- timespanRange,
- timestampRange,
- numTimes,
- locations: [],
- filters: filterString,
- });
- }
-
- if (isAutoGenerated) {
- for (const monitorLoc of downMonitorsByLocation) {
- const monitorInfo = monitorLoc.monitorInfo;
-
- const alertInstance = alertInstanceFactory(
- getInstanceId(monitorInfo, monitorLoc.location)
- );
-
- const monitorSummary = getMonitorSummary(monitorInfo);
- const statusMessage = getStatusMessage(monitorInfo);
+ if (isAutoGenerated) {
+ for (const monitorLoc of downMonitorsByLocation) {
+ const monitorInfo = monitorLoc.monitorInfo;
- alertInstance.replaceState({
- ...state,
- ...monitorSummary,
- statusMessage,
- ...updateState(state, true),
- });
+ const statusMessage = getStatusMessage(monitorInfo);
+ const monitorSummary = getMonitorSummary(monitorInfo, statusMessage);
- alertInstance.scheduleActions(MONITOR_STATUS.id);
- }
- return updateState(state, downMonitorsByLocation.length > 0);
- }
+ const alert = alertWithLifecycle({
+ id: getInstanceId(monitorInfo, monitorLoc.location),
+ fields: getMonitorAlertDocument(monitorSummary),
+ });
- let availabilityResults: GetMonitorAvailabilityResult[] = [];
- if (shouldCheckAvailability) {
- availabilityResults = await libs.requests.getMonitorAvailability({
- uptimeEsClient,
- ...availability,
- filters: JSON.stringify(filterString) || undefined,
+ alert.replaceState({
+ ...state,
+ ...monitorSummary,
+ statusMessage,
+ ...updateState(state, true),
});
+
+ alert.scheduleActions(MONITOR_STATUS.id);
}
+ return updateState(state, downMonitorsByLocation.length > 0);
+ }
- const mergedIdsByLoc = getUniqueIdsByLoc(downMonitorsByLocation, availabilityResults);
+ let availabilityResults: GetMonitorAvailabilityResult[] = [];
+ if (shouldCheckAvailability) {
+ availabilityResults = await libs.requests.getMonitorAvailability({
+ uptimeEsClient,
+ ...availability,
+ filters: JSON.stringify(filterString) || undefined,
+ });
+ }
- mergedIdsByLoc.forEach((monIdByLoc) => {
- const availMonInfo = availabilityResults.find(
- ({ monitorId, location }) => getMonIdByLoc(monitorId, location) === monIdByLoc
- );
+ const mergedIdsByLoc = getUniqueIdsByLoc(downMonitorsByLocation, availabilityResults);
- const downMonInfo = downMonitorsByLocation.find(
- ({ monitorId, location }) => getMonIdByLoc(monitorId, location) === monIdByLoc
- )?.monitorInfo;
+ mergedIdsByLoc.forEach((monIdByLoc) => {
+ const availMonInfo = availabilityResults.find(
+ ({ monitorId, location }) => getMonIdByLoc(monitorId, location) === monIdByLoc
+ );
- const monitorInfo = downMonInfo || availMonInfo?.monitorInfo!;
+ const downMonInfo = downMonitorsByLocation.find(
+ ({ monitorId, location }) => getMonIdByLoc(monitorId, location) === monIdByLoc
+ )?.monitorInfo;
- const monitorSummary = getMonitorSummary(monitorInfo);
- const statusMessage = getStatusMessage(downMonInfo!, availMonInfo!, availability);
+ const monitorInfo = downMonInfo || availMonInfo?.monitorInfo!;
- const alertInstance = alertInstanceFactory(getInstanceId(monitorInfo, monIdByLoc));
+ const statusMessage = getStatusMessage(downMonInfo!, availMonInfo!, availability);
+ const monitorSummary = getMonitorSummary(monitorInfo, statusMessage);
- alertInstance.replaceState({
- ...updateState(state, true),
- ...monitorSummary,
- statusMessage,
- });
+ const alert = alertWithLifecycle({
+ id: getInstanceId(monitorInfo, monIdByLoc),
+ fields: getMonitorAlertDocument(monitorSummary),
+ });
- alertInstance.scheduleActions(MONITOR_STATUS.id, {
- message: generateMessageForOlderVersions({ ...monitorSummary, statusMessage }),
- });
+ alert.replaceState({
+ ...updateState(state, true),
+ ...monitorSummary,
+ statusMessage,
});
- return updateState(state, downMonitorsByLocation.length > 0);
- },
- });
+ alert.scheduleActions(MONITOR_STATUS.id);
+ });
+
+ return updateState(state, downMonitorsByLocation.length > 0);
+ },
+});
diff --git a/x-pack/plugins/uptime/server/lib/alerts/test_utils/index.ts b/x-pack/plugins/uptime/server/lib/alerts/test_utils/index.ts
new file mode 100644
index 000000000000..8bbf20f3a64a
--- /dev/null
+++ b/x-pack/plugins/uptime/server/lib/alerts/test_utils/index.ts
@@ -0,0 +1,81 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { Logger } from 'kibana/server';
+import { UMServerLibs } from '../../lib';
+import { UptimeCorePlugins, UptimeCoreSetup } from '../../adapters';
+import type { UptimeRouter } from '../../../types';
+import type { RuleDataClient } from '../../../../../rule_registry/server';
+import { getUptimeESMockClient } from '../../requests/helper';
+import { alertsMock } from '../../../../../alerting/server/mocks';
+import { DynamicSettings } from '../../../../common/runtime_types';
+import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants';
+
+/**
+ * The alert takes some dependencies as parameters; these are things like
+ * kibana core services and plugins. This function helps reduce the amount of
+ * boilerplate required.
+ * @param customRequests client tests can use this paramter to provide their own request mocks,
+ * so we don't have to mock them all for each test.
+ */
+export const bootstrapDependencies = (customRequests?: any, customPlugins: any = {}) => {
+ const router = {} as UptimeRouter;
+ // these server/libs parameters don't have any functionality, which is fine
+ // because we aren't testing them here
+ const server: UptimeCoreSetup = { router };
+ const plugins: UptimeCorePlugins = customPlugins as any;
+ const libs: UMServerLibs = { requests: {} } as UMServerLibs;
+ libs.requests = { ...libs.requests, ...customRequests };
+ return { server, libs, plugins };
+};
+
+export const createRuleTypeMocks = (
+ dynamicCertSettings: {
+ certAgeThreshold: DynamicSettings['certAgeThreshold'];
+ certExpirationThreshold: DynamicSettings['certExpirationThreshold'];
+ } = {
+ certAgeThreshold: DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold,
+ certExpirationThreshold: DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold,
+ }
+) => {
+ const loggerMock = ({
+ debug: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+ } as unknown) as Logger;
+
+ const scheduleActions = jest.fn();
+ const replaceState = jest.fn();
+
+ const services = {
+ ...getUptimeESMockClient(),
+ ...alertsMock.createAlertServices(),
+ alertWithLifecycle: jest.fn().mockReturnValue({ scheduleActions, replaceState }),
+ logger: loggerMock,
+ };
+
+ return {
+ dependencies: {
+ logger: loggerMock,
+ ruleDataClient: ({
+ getReader: () => {
+ return {
+ search: jest.fn(),
+ };
+ },
+ getWriter: () => {
+ return {
+ bulk: jest.fn(),
+ };
+ },
+ } as unknown) as RuleDataClient,
+ },
+ services,
+ scheduleActions,
+ replaceState,
+ };
+};
diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls.test.ts b/x-pack/plugins/uptime/server/lib/alerts/tls.test.ts
index a77fe10f0b9a..2536056363dd 100644
--- a/x-pack/plugins/uptime/server/lib/alerts/tls.test.ts
+++ b/x-pack/plugins/uptime/server/lib/alerts/tls.test.ts
@@ -4,52 +4,180 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
import moment from 'moment';
-import { getCertSummary } from './tls';
-import { Cert } from '../../../common/runtime_types';
+
+import { tlsAlertFactory, getCertSummary, DEFAULT_SIZE } from './tls';
+import { TLS } from '../../../common/constants/alerts';
+import { CertResult, DynamicSettings } from '../../../common/runtime_types';
+import { createRuleTypeMocks, bootstrapDependencies } from './test_utils';
+import { DEFAULT_FROM, DEFAULT_TO } from '../../rest_api/certs/certs';
+import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants';
+
+import { savedObjectsAdapter, UMSavedObjectsAdapter } from '../saved_objects';
+
+/**
+ * This function aims to provide an easy way to give mock props that will
+ * reduce boilerplate for tests.
+ * @param params the params received at alert creation time
+ * @param state the state the alert maintains
+ */
+const mockOptions = (
+ dynamicCertSettings?: {
+ certExpirationThreshold: DynamicSettings['certExpirationThreshold'];
+ certAgeThreshold: DynamicSettings['certAgeThreshold'];
+ },
+ state = {}
+): any => {
+ const { services } = createRuleTypeMocks(dynamicCertSettings);
+ const params = {
+ timerange: { from: 'now-15m', to: 'now' },
+ };
+
+ return {
+ params,
+ state,
+ services,
+ };
+};
+
+const mockCertResult: CertResult = {
+ certs: [
+ {
+ not_after: '2020-07-16T03:15:39.000Z',
+ not_before: '2019-07-24T03:15:39.000Z',
+ issuer: 'Sample issuer',
+ common_name: 'Common-One',
+ monitors: [{ name: 'monitor-one', id: 'monitor1' }],
+ sha256: 'abc',
+ },
+ {
+ not_after: '2020-07-18T03:15:39.000Z',
+ not_before: '2019-07-20T03:15:39.000Z',
+ issuer: 'Sample issuer',
+ common_name: 'Common-Two',
+ monitors: [{ name: 'monitor-two', id: 'monitor2' }],
+ sha256: 'bcd',
+ },
+ {
+ not_after: '2020-07-19T03:15:39.000Z',
+ not_before: '2019-07-22T03:15:39.000Z',
+ issuer: 'Sample issuer',
+ common_name: 'Common-Three',
+ monitors: [{ name: 'monitor-three', id: 'monitor3' }],
+ sha256: 'cde',
+ },
+ {
+ not_after: '2020-07-25T03:15:39.000Z',
+ not_before: '2019-07-25T03:15:39.000Z',
+ issuer: 'Sample issuer',
+ common_name: 'Common-Four',
+ monitors: [{ name: 'monitor-four', id: 'monitor4' }],
+ sha256: 'def',
+ },
+ ],
+ total: 4,
+};
describe('tls alert', () => {
+ let toISOStringSpy: jest.SpyInstance;
+ let savedObjectsAdapterSpy: jest.SpyInstance<
+ ReturnType
+ >;
+ const mockDate = 'date';
+ beforeAll(() => {
+ Date.now = jest.fn().mockReturnValue(new Date('2021-05-13T12:33:37.000Z'));
+ });
+
+ describe('alert executor', () => {
+ beforeEach(() => {
+ toISOStringSpy = jest.spyOn(Date.prototype, 'toISOString');
+ savedObjectsAdapterSpy = jest.spyOn(savedObjectsAdapter, 'getUptimeDynamicSettings');
+ });
+
+ it('triggers when aging or expiring alerts are found', async () => {
+ toISOStringSpy.mockImplementation(() => mockDate);
+ const mockGetter: jest.Mock = jest.fn();
+
+ mockGetter.mockReturnValue(mockCertResult);
+ const { server, libs, plugins } = bootstrapDependencies({ getCerts: mockGetter });
+ const alert = tlsAlertFactory(server, libs, plugins);
+ const options = mockOptions();
+ const {
+ services: { alertWithLifecycle },
+ } = options;
+ await alert.executor(options);
+ expect(mockGetter).toHaveBeenCalledTimes(1);
+ expect(alertWithLifecycle).toHaveBeenCalledTimes(4);
+ mockCertResult.certs.forEach((cert) => {
+ expect(alertWithLifecycle).toBeCalledWith({
+ fields: expect.objectContaining({
+ 'tls.server.x509.subject.common_name': cert.common_name,
+ 'tls.server.x509.issuer.common_name': cert.issuer,
+ 'tls.server.x509.not_after': cert.not_after,
+ 'tls.server.x509.not_before': cert.not_before,
+ 'tls.server.hash.sha256': cert.sha256,
+ }),
+ id: `${cert.common_name}-${cert.issuer?.replace(/\s/g, '_')}-${cert.sha256}`,
+ });
+ });
+ expect(mockGetter).toBeCalledWith(
+ expect.objectContaining({
+ from: DEFAULT_FROM,
+ to: DEFAULT_TO,
+ index: 0,
+ size: DEFAULT_SIZE,
+ notValidAfter: `now+${DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold}d`,
+ notValidBefore: `now-${DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold}d`,
+ sortBy: 'common_name',
+ direction: 'desc',
+ })
+ );
+ const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results;
+ expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(4);
+ mockCertResult.certs.forEach((cert) => {
+ expect(alertInstanceMock.replaceState).toBeCalledWith(
+ expect.objectContaining({
+ commonName: cert.common_name,
+ issuer: cert.issuer,
+ status: 'expired',
+ })
+ );
+ });
+ expect(alertInstanceMock.scheduleActions).toHaveBeenCalledTimes(4);
+ expect(alertInstanceMock.scheduleActions).toBeCalledWith(TLS.id);
+ });
+
+ it('handles dynamic settings for aging or expiration threshold', async () => {
+ toISOStringSpy.mockImplementation(() => mockDate);
+ const certSettings = {
+ certAgeThreshold: 10,
+ certExpirationThreshold: 5,
+ heartbeatIndices: 'heartbeat-*',
+ defaultConnectors: [],
+ };
+ savedObjectsAdapterSpy.mockImplementation(() => certSettings);
+ const mockGetter: jest.Mock = jest.fn();
+
+ mockGetter.mockReturnValue(mockCertResult);
+ const { server, libs, plugins } = bootstrapDependencies({ getCerts: mockGetter });
+ const alert = tlsAlertFactory(server, libs, plugins);
+ const options = mockOptions();
+ await alert.executor(options);
+ expect(mockGetter).toHaveBeenCalledTimes(1);
+ expect(mockGetter).toBeCalledWith(
+ expect.objectContaining({
+ notValidAfter: `now+${certSettings.certExpirationThreshold}d`,
+ notValidBefore: `now-${certSettings.certAgeThreshold}d`,
+ })
+ );
+ });
+ });
+
describe('getCertSummary', () => {
- let mockCerts: Cert[];
let diffSpy: jest.SpyInstance;
beforeEach(() => {
diffSpy = jest.spyOn(moment.prototype, 'diff');
- mockCerts = [
- {
- not_after: '2020-07-16T03:15:39.000Z',
- not_before: '2019-07-24T03:15:39.000Z',
- common_name: 'Common-One',
- monitors: [{ name: 'monitor-one', id: 'monitor1' }],
- sha256: 'abc',
- issuer: 'Cloudflare Inc ECC CA-3',
- },
- {
- not_after: '2020-07-18T03:15:39.000Z',
- not_before: '2019-07-20T03:15:39.000Z',
- common_name: 'Common-Two',
- monitors: [{ name: 'monitor-two', id: 'monitor2' }],
- sha256: 'bcd',
- issuer: 'Cloudflare Inc ECC CA-3',
- },
- {
- not_after: '2020-07-19T03:15:39.000Z',
- not_before: '2019-07-22T03:15:39.000Z',
- common_name: 'Common-Three',
- monitors: [{ name: 'monitor-three', id: 'monitor3' }],
- sha256: 'cde',
- issuer: 'Cloudflare Inc ECC CA-3',
- },
- {
- not_after: '2020-07-25T03:15:39.000Z',
- not_before: '2019-07-25T03:15:39.000Z',
- common_name: 'Common-Four',
- monitors: [{ name: 'monitor-four', id: 'monitor4' }],
- sha256: 'def',
- issuer: 'Cloudflare Inc ECC CA-3',
- },
- ];
});
afterEach(() => {
@@ -59,13 +187,13 @@ describe('tls alert', () => {
it('handles positive diffs for expired certs appropriately', () => {
diffSpy.mockReturnValueOnce(900);
const result = getCertSummary(
- mockCerts[0],
+ mockCertResult.certs[0],
new Date('2020-07-20T05:00:00.000Z').valueOf(),
new Date('2019-03-01T00:00:00.000Z').valueOf()
);
expect(result).toEqual({
- commonName: mockCerts[0].common_name,
- issuer: mockCerts[0].issuer,
+ commonName: mockCertResult.certs[0].common_name,
+ issuer: mockCertResult.certs[0].issuer,
summary: 'expired on Jul 15, 2020 EDT, 900 days ago.',
status: 'expired',
});
@@ -74,13 +202,13 @@ describe('tls alert', () => {
it('handles positive diffs for agining certs appropriately', () => {
diffSpy.mockReturnValueOnce(702);
const result = getCertSummary(
- mockCerts[0],
+ mockCertResult.certs[0],
new Date('2020-07-01T12:00:00.000Z').valueOf(),
new Date('2019-09-01T03:00:00.000Z').valueOf()
);
expect(result).toEqual({
- commonName: mockCerts[0].common_name,
- issuer: mockCerts[0].issuer,
+ commonName: mockCertResult.certs[0].common_name,
+ issuer: mockCertResult.certs[0].issuer,
summary: 'valid since Jul 23, 2019 EDT, 702 days ago.',
status: 'becoming too old',
});
@@ -89,13 +217,13 @@ describe('tls alert', () => {
it('handles negative diff values appropriately for aging certs', () => {
diffSpy.mockReturnValueOnce(-90);
const result = getCertSummary(
- mockCerts[0],
+ mockCertResult.certs[0],
new Date('2020-07-01T12:00:00.000Z').valueOf(),
new Date('2019-09-01T03:00:00.000Z').valueOf()
);
expect(result).toEqual({
- commonName: mockCerts[0].common_name,
- issuer: mockCerts[0].issuer,
+ commonName: mockCertResult.certs[0].common_name,
+ issuer: mockCertResult.certs[0].issuer,
summary: 'invalid until Jul 23, 2019 EDT, 90 days from now.',
status: 'invalid',
});
@@ -106,13 +234,13 @@ describe('tls alert', () => {
// negative days are in the future, positive days are in the past
.mockReturnValueOnce(-96);
const result = getCertSummary(
- mockCerts[0],
+ mockCertResult.certs[0],
new Date('2020-07-20T05:00:00.000Z').valueOf(),
new Date('2019-03-01T00:00:00.000Z').valueOf()
);
expect(result).toEqual({
- commonName: mockCerts[0].common_name,
- issuer: mockCerts[0].issuer,
+ commonName: mockCertResult.certs[0].common_name,
+ issuer: mockCertResult.certs[0].issuer,
summary: 'expires on Jul 15, 2020 EDT in 96 days.',
status: 'expiring',
});
diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls.ts b/x-pack/plugins/uptime/server/lib/alerts/tls.ts
index 09f5e2fe0f6d..8056fe210bf5 100644
--- a/x-pack/plugins/uptime/server/lib/alerts/tls.ts
+++ b/x-pack/plugins/uptime/server/lib/alerts/tls.ts
@@ -8,18 +8,22 @@
import moment from 'moment';
import { schema } from '@kbn/config-schema';
import { UptimeAlertTypeFactory } from './types';
-import { updateState } from './common';
+import { updateState, generateAlertMessage } from './common';
import { TLS } from '../../../common/constants/alerts';
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants';
import { Cert, CertResult } from '../../../common/runtime_types';
import { commonStateTranslations, tlsTranslations } from './translations';
import { DEFAULT_FROM, DEFAULT_TO } from '../../rest_api/certs/certs';
-import { uptimeAlertWrapper } from './uptime_alert_wrapper';
+import { TlsTranslations } from '../../../common/translations';
+
import { ActionGroupIdsOf } from '../../../../alerting/common';
+import { savedObjectsAdapter } from '../saved_objects';
+import { createUptimeESClient } from '../lib';
+
export type ActionGroupIds = ActionGroupIdsOf;
-const DEFAULT_SIZE = 20;
+export const DEFAULT_SIZE = 20;
interface TlsAlertState {
commonName: string;
@@ -93,78 +97,92 @@ export const getCertSummary = (
};
};
-export const tlsAlertFactory: UptimeAlertTypeFactory = (_server, libs) =>
- uptimeAlertWrapper({
- id: 'xpack.uptime.alerts.tlsCertificate',
- name: tlsTranslations.alertFactoryName,
- validate: {
- params: schema.object({}),
- },
- defaultActionGroupId: TLS.id,
- actionGroups: [
- {
- id: TLS.id,
- name: TLS.name,
- },
- ],
- actionVariables: {
- context: [],
- state: [...tlsTranslations.actionVariables, ...commonStateTranslations],
+export const tlsAlertFactory: UptimeAlertTypeFactory = (_server, libs) => ({
+ id: 'xpack.uptime.alerts.tlsCertificate',
+ producer: 'uptime',
+ name: tlsTranslations.alertFactoryName,
+ validate: {
+ params: schema.object({}),
+ },
+ defaultActionGroupId: TLS.id,
+ actionGroups: [
+ {
+ id: TLS.id,
+ name: TLS.name,
},
- minimumLicenseRequired: 'basic',
- isExportable: true,
- async executor({ options, dynamicSettings, uptimeEsClient }) {
- const {
- services: { alertInstanceFactory },
- state,
- } = options;
-
- const { certs, total }: CertResult = await libs.requests.getCerts({
- uptimeEsClient,
- from: DEFAULT_FROM,
- to: DEFAULT_TO,
- index: 0,
- size: DEFAULT_SIZE,
- notValidAfter: `now+${
- dynamicSettings?.certExpirationThreshold ??
- DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold
- }d`,
- notValidBefore: `now-${
- dynamicSettings?.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold
- }d`,
- sortBy: 'common_name',
- direction: 'desc',
- });
-
- const foundCerts = total > 0;
-
- if (foundCerts) {
- certs.forEach((cert) => {
- const absoluteExpirationThreshold = moment()
- .add(
- dynamicSettings.certExpirationThreshold ??
- DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold,
- 'd'
- )
- .valueOf();
- const absoluteAgeThreshold = moment()
- .subtract(
- dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold,
- 'd'
- )
- .valueOf();
- const alertInstance = alertInstanceFactory(
- `${cert.common_name}-${cert.issuer?.replace(/\s/g, '_')}-${cert.sha256}`
- );
- const summary = getCertSummary(cert, absoluteExpirationThreshold, absoluteAgeThreshold);
- alertInstance.replaceState({
- ...updateState(state, foundCerts),
- ...summary,
- });
- alertInstance.scheduleActions(TLS.id);
+ ],
+ actionVariables: {
+ context: [],
+ state: [...tlsTranslations.actionVariables, ...commonStateTranslations],
+ },
+ isExportable: true,
+ minimumLicenseRequired: 'basic',
+ async executor({
+ services: { alertWithLifecycle, savedObjectsClient, scopedClusterClient },
+ state,
+ }) {
+ const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient);
+
+ const uptimeEsClient = createUptimeESClient({
+ esClient: scopedClusterClient.asCurrentUser,
+ savedObjectsClient,
+ });
+
+ const { certs, total }: CertResult = await libs.requests.getCerts({
+ uptimeEsClient,
+ from: DEFAULT_FROM,
+ to: DEFAULT_TO,
+ index: 0,
+ size: DEFAULT_SIZE,
+ notValidAfter: `now+${
+ dynamicSettings?.certExpirationThreshold ??
+ DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold
+ }d`,
+ notValidBefore: `now-${
+ dynamicSettings?.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold
+ }d`,
+ sortBy: 'common_name',
+ direction: 'desc',
+ });
+
+ const foundCerts = total > 0;
+
+ if (foundCerts) {
+ certs.forEach((cert) => {
+ const absoluteExpirationThreshold = moment()
+ .add(
+ dynamicSettings.certExpirationThreshold ??
+ DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold,
+ 'd'
+ )
+ .valueOf();
+ const absoluteAgeThreshold = moment()
+ .subtract(
+ dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold,
+ 'd'
+ )
+ .valueOf();
+ const summary = getCertSummary(cert, absoluteExpirationThreshold, absoluteAgeThreshold);
+
+ const alertInstance = alertWithLifecycle({
+ id: `${cert.common_name}-${cert.issuer?.replace(/\s/g, '_')}-${cert.sha256}`,
+ fields: {
+ 'tls.server.x509.subject.common_name': cert.common_name,
+ 'tls.server.x509.issuer.common_name': cert.issuer,
+ 'tls.server.x509.not_after': cert.not_after,
+ 'tls.server.x509.not_before': cert.not_before,
+ 'tls.server.hash.sha256': cert.sha256,
+ reason: generateAlertMessage(TlsTranslations.defaultActionMessage, summary),
+ },
});
- }
+ alertInstance.replaceState({
+ ...updateState(state, foundCerts),
+ ...summary,
+ });
+ alertInstance.scheduleActions(TLS.id);
+ });
+ }
- return updateState(state, foundCerts);
- },
- });
+ return updateState(state, foundCerts);
+ },
+});
diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts b/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts
index 5bf91b7c5486..812925f22b24 100644
--- a/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts
+++ b/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts
@@ -14,11 +14,18 @@ import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants';
import { Cert, CertResult } from '../../../common/runtime_types';
import { commonStateTranslations, tlsTranslations } from './translations';
import { DEFAULT_FROM, DEFAULT_TO } from '../../rest_api/certs/certs';
-import { uptimeAlertWrapper } from './uptime_alert_wrapper';
import { ActionGroupIdsOf } from '../../../../alerting/common';
+import { AlertInstanceContext } from '../../../../alerting/common';
+import { AlertInstance } from '../../../../alerting/server';
+
+import { savedObjectsAdapter } from '../saved_objects';
+import { createUptimeESClient } from '../lib';
+
export type ActionGroupIds = ActionGroupIdsOf;
+type TLSAlertInstance = AlertInstance, AlertInstanceContext, ActionGroupIds>;
+
const DEFAULT_SIZE = 20;
interface TlsAlertState {
@@ -84,74 +91,78 @@ export const getCertSummary = (
};
};
-export const tlsLegacyAlertFactory: UptimeAlertTypeFactory = (_server, libs) =>
- uptimeAlertWrapper({
- id: 'xpack.uptime.alerts.tls',
- name: tlsTranslations.legacyAlertFactoryName,
- validate: {
- params: schema.object({}),
+export const tlsLegacyAlertFactory: UptimeAlertTypeFactory = (_server, libs) => ({
+ id: 'xpack.uptime.alerts.tls',
+ producer: 'uptime',
+ name: tlsTranslations.legacyAlertFactoryName,
+ validate: {
+ params: schema.object({}),
+ },
+ defaultActionGroupId: TLS_LEGACY.id,
+ actionGroups: [
+ {
+ id: TLS_LEGACY.id,
+ name: TLS_LEGACY.name,
},
- defaultActionGroupId: TLS_LEGACY.id,
- actionGroups: [
- {
- id: TLS_LEGACY.id,
- name: TLS_LEGACY.name,
- },
- ],
- actionVariables: {
- context: [],
- state: [...tlsTranslations.actionVariables, ...commonStateTranslations],
- },
- minimumLicenseRequired: 'basic',
- isExportable: true,
- async executor({ options, dynamicSettings, uptimeEsClient }) {
- const {
- services: { alertInstanceFactory },
- state,
- } = options;
-
- const { certs, total }: CertResult = await libs.requests.getCerts({
- uptimeEsClient,
- from: DEFAULT_FROM,
- to: DEFAULT_TO,
- index: 0,
- size: DEFAULT_SIZE,
- notValidAfter: `now+${
- dynamicSettings?.certExpirationThreshold ??
- DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold
- }d`,
- notValidBefore: `now-${
- dynamicSettings?.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold
- }d`,
- sortBy: 'common_name',
- direction: 'desc',
+ ],
+ actionVariables: {
+ context: [],
+ state: [...tlsTranslations.actionVariables, ...commonStateTranslations],
+ },
+ isExportable: true,
+ minimumLicenseRequired: 'basic',
+ async executor({
+ services: { alertInstanceFactory, scopedClusterClient, savedObjectsClient },
+ state,
+ }) {
+ const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient);
+
+ const uptimeEsClient = createUptimeESClient({
+ esClient: scopedClusterClient.asCurrentUser,
+ savedObjectsClient,
+ });
+ const { certs, total }: CertResult = await libs.requests.getCerts({
+ uptimeEsClient,
+ from: DEFAULT_FROM,
+ to: DEFAULT_TO,
+ index: 0,
+ size: DEFAULT_SIZE,
+ notValidAfter: `now+${
+ dynamicSettings?.certExpirationThreshold ??
+ DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold
+ }d`,
+ notValidBefore: `now-${
+ dynamicSettings?.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold
+ }d`,
+ sortBy: 'common_name',
+ direction: 'desc',
+ });
+
+ const foundCerts = total > 0;
+
+ if (foundCerts) {
+ const absoluteExpirationThreshold = moment()
+ .add(
+ dynamicSettings.certExpirationThreshold ??
+ DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold,
+ 'd'
+ )
+ .valueOf();
+ const absoluteAgeThreshold = moment()
+ .subtract(
+ dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold,
+ 'd'
+ )
+ .valueOf();
+ const alertInstance: TLSAlertInstance = alertInstanceFactory(TLS_LEGACY.id);
+ const summary = getCertSummary(certs, absoluteExpirationThreshold, absoluteAgeThreshold);
+ alertInstance.replaceState({
+ ...updateState(state, foundCerts),
+ ...summary,
});
+ alertInstance.scheduleActions(TLS_LEGACY.id);
+ }
- const foundCerts = total > 0;
-
- if (foundCerts) {
- const absoluteExpirationThreshold = moment()
- .add(
- dynamicSettings.certExpirationThreshold ??
- DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold,
- 'd'
- )
- .valueOf();
- const absoluteAgeThreshold = moment()
- .subtract(
- dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold,
- 'd'
- )
- .valueOf();
- const alertInstance = alertInstanceFactory(TLS_LEGACY.id);
- const summary = getCertSummary(certs, absoluteExpirationThreshold, absoluteAgeThreshold);
- alertInstance.replaceState({
- ...updateState(state, foundCerts),
- ...summary,
- });
- alertInstance.scheduleActions(TLS_LEGACY.id);
- }
-
- return updateState(state, foundCerts);
- },
- });
+ return updateState(state, foundCerts);
+ },
+});
diff --git a/x-pack/plugins/uptime/server/lib/alerts/types.ts b/x-pack/plugins/uptime/server/lib/alerts/types.ts
index 6f9ca42e54ad..28f9eba7ab38 100644
--- a/x-pack/plugins/uptime/server/lib/alerts/types.ts
+++ b/x-pack/plugins/uptime/server/lib/alerts/types.ts
@@ -4,21 +4,27 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
import { UptimeCorePlugins, UptimeCoreSetup } from '../adapters';
import { UMServerLibs } from '../lib';
-import { AlertType, AlertInstanceState, AlertInstanceContext } from '../../../../alerting/server';
+import { AlertTypeWithExecutor, LifecycleAlertService } from '../../../../rule_registry/server';
+import { AlertInstanceContext } from '../../../../alerting/common';
-export type UptimeAlertTypeParam = Record;
-export type UptimeAlertTypeState = Record;
-export type UptimeAlertTypeFactory = (
+/**
+ * Because all of our types are presumably going to list the `producer` as `'uptime'`,
+ * we should just omit this field from the returned value to simplify the returned alert type.
+ *
+ * When we register all the alerts we can inject this field.
+ */
+export type DefaultUptimeAlertInstance = AlertTypeWithExecutor<
+ Record,
+ AlertInstanceContext,
+ {
+ alertWithLifecycle: LifecycleAlertService;
+ }
+>;
+
+export type UptimeAlertTypeFactory = (
server: UptimeCoreSetup,
libs: UMServerLibs,
plugins: UptimeCorePlugins
-) => AlertType<
- UptimeAlertTypeParam,
- UptimeAlertTypeState,
- AlertInstanceState,
- AlertInstanceContext,
- ActionGroupIds
->;
+) => DefaultUptimeAlertInstance;
diff --git a/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts b/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts
deleted file mode 100644
index 654f99cb0265..000000000000
--- a/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { SavedObjectsClientContract } from 'kibana/server';
-import {
- AlertExecutorOptions,
- AlertInstanceState,
- AlertInstanceContext,
-} from '../../../../alerting/server';
-import { savedObjectsAdapter } from '../saved_objects';
-import { DynamicSettings } from '../../../common/runtime_types';
-import { createUptimeESClient, UptimeESClient } from '../lib';
-import { UptimeAlertTypeFactory, UptimeAlertTypeParam, UptimeAlertTypeState } from './types';
-
-export interface UptimeAlertType
- extends Omit>, 'executor' | 'producer'> {
- executor: ({
- options,
- uptimeEsClient,
- dynamicSettings,
- }: {
- options: AlertExecutorOptions<
- UptimeAlertTypeParam,
- UptimeAlertTypeState,
- AlertInstanceState,
- AlertInstanceContext,
- ActionGroupIds
- >;
- uptimeEsClient: UptimeESClient;
- dynamicSettings: DynamicSettings;
- savedObjectsClient: SavedObjectsClientContract;
- }) => Promise;
-}
-
-export const uptimeAlertWrapper = (
- uptimeAlert: UptimeAlertType
-) => ({
- ...uptimeAlert,
- producer: 'uptime',
- executor: async (
- options: AlertExecutorOptions<
- UptimeAlertTypeParam,
- UptimeAlertTypeState,
- AlertInstanceState,
- AlertInstanceContext,
- ActionGroupIds
- >
- ) => {
- const {
- services: { scopedClusterClient: esClient, savedObjectsClient },
- } = options;
-
- const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(
- options.services.savedObjectsClient
- );
-
- const uptimeEsClient = createUptimeESClient({
- esClient: esClient.asCurrentUser,
- savedObjectsClient,
- });
-
- return uptimeAlert.executor({ options, dynamicSettings, uptimeEsClient, savedObjectsClient });
- },
-});
diff --git a/x-pack/plugins/uptime/server/plugin.ts b/x-pack/plugins/uptime/server/plugin.ts
index c0fecf6f19af..5ef5e17d4e33 100644
--- a/x-pack/plugins/uptime/server/plugin.ts
+++ b/x-pack/plugins/uptime/server/plugin.ts
@@ -4,30 +4,97 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
+import { once } from 'lodash';
import {
PluginInitializerContext,
CoreStart,
CoreSetup,
Plugin as PluginType,
ISavedObjectsRepository,
+ Logger,
} from '../../../../src/core/server';
+import { uptimeRuleFieldMap } from '../common/rules/uptime_rule_field_map';
import { initServerWithKibana } from './kibana.index';
import { KibanaTelemetryAdapter, UptimeCorePlugins } from './lib/adapters';
import { umDynamicSettings } from './lib/saved_objects';
+import { mappingFromFieldMap } from '../../rule_registry/common/mapping_from_field_map';
+import { TECHNICAL_COMPONENT_TEMPLATE_NAME } from '../../rule_registry/common/assets';
+
+export type UptimeRuleRegistry = ReturnType['ruleRegistry'];
export class Plugin implements PluginType {
private savedObjectsClient?: ISavedObjectsRepository;
+ private initContext: PluginInitializerContext;
+ private logger?: Logger;
- constructor(_initializerContext: PluginInitializerContext) {}
+ constructor(_initializerContext: PluginInitializerContext) {
+ this.initContext = _initializerContext;
+ }
public setup(core: CoreSetup, plugins: UptimeCorePlugins) {
- initServerWithKibana({ router: core.http.createRouter() }, plugins);
+ this.logger = this.initContext.logger.get();
+ const { ruleDataService } = plugins.ruleRegistry;
+
+ const ready = once(async () => {
+ const componentTemplateName = ruleDataService.getFullAssetName('synthetics-mappings');
+ const alertsIndexPattern = ruleDataService.getFullAssetName('observability.synthetics*');
+
+ if (!ruleDataService.isWriteEnabled()) {
+ return;
+ }
+
+ await ruleDataService.createOrUpdateComponentTemplate({
+ name: componentTemplateName,
+ body: {
+ template: {
+ settings: {
+ number_of_shards: 1,
+ },
+ mappings: mappingFromFieldMap(uptimeRuleFieldMap),
+ },
+ },
+ });
+
+ await ruleDataService.createOrUpdateIndexTemplate({
+ name: ruleDataService.getFullAssetName('synthetics-index-template'),
+ body: {
+ index_patterns: [alertsIndexPattern],
+ composed_of: [
+ ruleDataService.getFullAssetName(TECHNICAL_COMPONENT_TEMPLATE_NAME),
+ componentTemplateName,
+ ],
+ },
+ });
+
+ await ruleDataService.updateIndexMappingsMatchingPattern(alertsIndexPattern);
+ });
+
+ // initialize eagerly
+ const initializeRuleDataTemplatesPromise = ready().catch((err) => {
+ this.logger!.error(err);
+ });
+
+ const ruleDataClient = ruleDataService.getRuleDataClient(
+ 'synthetics',
+ ruleDataService.getFullAssetName('observability.synthetics'),
+ () => initializeRuleDataTemplatesPromise
+ );
+
+ initServerWithKibana(
+ { router: core.http.createRouter() },
+ plugins,
+ ruleDataClient,
+ this.logger
+ );
core.savedObjects.registerType(umDynamicSettings);
KibanaTelemetryAdapter.registerUsageCollector(
plugins.usageCollection,
() => this.savedObjectsClient
);
+
+ return {
+ ruleRegistry: ruleDataClient,
+ };
}
public start(core: CoreStart, _plugins: any) {
diff --git a/x-pack/plugins/uptime/server/uptime_server.ts b/x-pack/plugins/uptime/server/uptime_server.ts
index 39e5c9bff202..f52b4a806335 100644
--- a/x-pack/plugins/uptime/server/uptime_server.ts
+++ b/x-pack/plugins/uptime/server/uptime_server.ts
@@ -5,21 +5,47 @@
* 2.0.
*/
+import { Logger } from 'kibana/server';
+import { createLifecycleRuleTypeFactory, RuleDataClient } from '../../rule_registry/server';
import { UMServerLibs } from './lib/lib';
import { createRouteWithAuth, restApiRoutes, uptimeRouteWrapper } from './rest_api';
import { UptimeCoreSetup, UptimeCorePlugins } from './lib/adapters';
-import { uptimeAlertTypeFactories } from './lib/alerts';
+
+import { statusCheckAlertFactory } from './lib/alerts/status_check';
+import { tlsAlertFactory } from './lib/alerts/tls';
+import { tlsLegacyAlertFactory } from './lib/alerts/tls_legacy';
+import { durationAnomalyAlertFactory } from './lib/alerts/duration_anomaly';
export const initUptimeServer = (
server: UptimeCoreSetup,
libs: UMServerLibs,
- plugins: UptimeCorePlugins
+ plugins: UptimeCorePlugins,
+ ruleDataClient: RuleDataClient,
+ logger: Logger
) => {
restApiRoutes.forEach((route) =>
libs.framework.registerRoute(uptimeRouteWrapper(createRouteWithAuth(libs, route)))
);
- uptimeAlertTypeFactories.forEach((alertTypeFactory) =>
- plugins.alerting.registerType(alertTypeFactory(server, libs, plugins))
- );
+ const {
+ alerting: { registerType },
+ } = plugins;
+
+ const statusAlert = statusCheckAlertFactory(server, libs, plugins);
+ const tlsLegacyAlert = tlsLegacyAlertFactory(server, libs, plugins);
+ const tlsAlert = tlsAlertFactory(server, libs, plugins);
+ const durationAlert = durationAnomalyAlertFactory(server, libs, plugins);
+
+ const createLifecycleRuleType = createLifecycleRuleTypeFactory({
+ ruleDataClient,
+ logger,
+ });
+
+ registerType(createLifecycleRuleType(statusAlert));
+ registerType(createLifecycleRuleType(tlsAlert));
+ registerType(createLifecycleRuleType(durationAlert));
+
+ /* TLS Legacy rule supported at least through 8.0.
+ * Not registered with RAC */
+ registerType(tlsLegacyAlert);
};
diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js
index 19c3543ce4d0..9e527835231b 100644
--- a/x-pack/scripts/functional_tests.js
+++ b/x-pack/scripts/functional_tests.js
@@ -88,7 +88,6 @@ const onlyNotInCoverageTests = [
require.resolve('../test/saved_object_tagging/api_integration/security_and_spaces/config.ts'),
require.resolve('../test/saved_object_tagging/api_integration/tagging_api/config.ts'),
require.resolve('../test/examples/config.ts'),
- require.resolve('../test/cloud_integration/config.ts'),
];
require('../../src/setup_node_env');
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts
index 29f2ed40be79..23f7f337aa81 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts
@@ -103,6 +103,50 @@ export default function alertTests({ getService }: FtrProviderContext) {
}
});
+ it('runs correctly: use epoch millis - threshold on hit count < >', async () => {
+ // write documents from now to the future end date in groups
+ createEsDocumentsInGroups(ES_GROUPS_TO_WRITE);
+
+ await createAlert({
+ name: 'never fire',
+ esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
+ size: 100,
+ thresholdComparator: '<',
+ threshold: [0],
+ timeField: 'date_epoch_millis',
+ });
+
+ await createAlert({
+ name: 'always fire',
+ esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
+ size: 100,
+ thresholdComparator: '>',
+ threshold: [-1],
+ timeField: 'date_epoch_millis',
+ });
+
+ const docs = await waitForDocs(2);
+ for (let i = 0; i < docs.length; i++) {
+ const doc = docs[i];
+ const { previousTimestamp, hits } = doc._source;
+ const { name, title, message } = doc._source.params;
+
+ expect(name).to.be('always fire');
+ expect(title).to.be(`alert 'always fire' matched query`);
+ const messagePattern = /alert 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than -1 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
+ expect(message).to.match(messagePattern);
+ expect(hits).not.to.be.empty();
+
+ // during the first execution, the latestTimestamp value should be empty
+ // since this alert always fires, the latestTimestamp value should be updated each execution
+ if (!i) {
+ expect(previousTimestamp).to.be.empty();
+ } else {
+ expect(previousTimestamp).not.to.be.empty();
+ }
+ }
+ });
+
it('runs correctly with query: threshold on hit count < >', async () => {
// write documents from now to the future end date in groups
createEsDocumentsInGroups(ES_GROUPS_TO_WRITE);
diff --git a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts
index 4e3740a1ccb1..0677acd500c1 100644
--- a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts
+++ b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts
@@ -376,6 +376,9 @@ export default function ApiTest({ getService }: FtrProviderContext) {
"kibana.rac.alert.status": Array [
"open",
],
+ "kibana.space_ids": Array [
+ "default",
+ ],
"processor.event": Array [
"transaction",
],
@@ -449,6 +452,9 @@ export default function ApiTest({ getService }: FtrProviderContext) {
"kibana.rac.alert.status": Array [
"open",
],
+ "kibana.space_ids": Array [
+ "default",
+ ],
"processor.event": Array [
"transaction",
],
@@ -556,6 +562,9 @@ export default function ApiTest({ getService }: FtrProviderContext) {
"kibana.rac.alert.status": Array [
"closed",
],
+ "kibana.space_ids": Array [
+ "default",
+ ],
"processor.event": Array [
"transaction",
],
diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency_ml.ts b/x-pack/test/apm_api_integration/tests/correlations/latency_ml.ts
index cc8f48fb5894..bbb2097f6301 100644
--- a/x-pack/test/apm_api_integration/tests/correlations/latency_ml.ts
+++ b/x-pack/test/apm_api_integration/tests/correlations/latency_ml.ts
@@ -26,7 +26,6 @@ export default function ApiTest({ getService }: FtrProviderContext) {
const getRequestBody = () => {
const partialSearchRequest: PartialSearchRequest = {
params: {
- index: 'apm-*',
environment: 'ENVIRONMENT_ALL',
start: '2020',
end: '2021',
@@ -141,7 +140,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
registry.when(
'Correlations latency_ml with data and opbeans-node args',
- { config: 'trial', archives: ['ml_8.0.0'] },
+ { config: 'trial', archives: ['8.0.0'] },
() => {
// putting this into a single `it` because the responses depend on each other
it('queries the search strategy and returns results', async () => {
@@ -235,30 +234,30 @@ export default function ApiTest({ getService }: FtrProviderContext) {
const { rawResponse: finalRawResponse } = followUpResult;
expect(typeof finalRawResponse?.took).to.be('number');
- expect(finalRawResponse?.percentileThresholdValue).to.be(1404927.875);
+ expect(finalRawResponse?.percentileThresholdValue).to.be(1309695.875);
expect(finalRawResponse?.overallHistogram.length).to.be(101);
expect(finalRawResponse?.values.length).to.eql(
- 1,
- `Expected 1 identified correlations, got ${finalRawResponse?.values.length}.`
+ 13,
+ `Expected 13 identified correlations, got ${finalRawResponse?.values.length}.`
);
expect(finalRawResponse?.log.map((d: string) => d.split(': ')[1])).to.eql([
- 'Fetched 95th percentile value of 1404927.875 based on 989 documents.',
+ 'Fetched 95th percentile value of 1309695.875 based on 1244 documents.',
'Loaded histogram range steps.',
'Loaded overall histogram chart data.',
'Loaded percentiles.',
- 'Identified 67 fieldCandidates.',
- 'Identified 339 fieldValuePairs.',
- 'Loaded fractions and totalDocCount of 989.',
- 'Identified 1 significant correlations out of 339 field/value pairs.',
+ 'Identified 69 fieldCandidates.',
+ 'Identified 379 fieldValuePairs.',
+ 'Loaded fractions and totalDocCount of 1244.',
+ 'Identified 13 significant correlations out of 379 field/value pairs.',
]);
const correlation = finalRawResponse?.values[0];
expect(typeof correlation).to.be('object');
expect(correlation?.field).to.be('transaction.result');
expect(correlation?.value).to.be('success');
- expect(correlation?.correlation).to.be(0.37418510688551887);
- expect(correlation?.ksTest).to.be(1.1238496968312214e-10);
+ expect(correlation?.correlation).to.be(0.6275246559191225);
+ expect(correlation?.ksTest).to.be(4.806503252860024e-13);
expect(correlation?.histogram.length).to.be(101);
});
}
diff --git a/x-pack/test/cloud_integration/tests/fullstory.ts b/x-pack/test/cloud_integration/tests/fullstory.ts
index 5c328d27cc52..1cdad719e94e 100644
--- a/x-pack/test/cloud_integration/tests/fullstory.ts
+++ b/x-pack/test/cloud_integration/tests/fullstory.ts
@@ -19,7 +19,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
describe('Cloud FullStory integration', function () {
- this.tags('ciGroup13');
before(async () => {
// Create role mapping so user gets superuser access
await getService('esSupertest')
diff --git a/x-pack/test/functional/apps/apm/correlations/index.ts b/x-pack/test/functional/apps/apm/correlations/index.ts
new file mode 100644
index 000000000000..ae5f594e5440
--- /dev/null
+++ b/x-pack/test/functional/apps/apm/correlations/index.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { FtrProviderContext } from '../../../ftr_provider_context';
+
+export default function ({ loadTestFile }: FtrProviderContext) {
+ describe('correlations', function () {
+ this.tags('skipFirefox');
+ loadTestFile(require.resolve('./latency_correlations'));
+ });
+}
diff --git a/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts b/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts
new file mode 100644
index 000000000000..bc06b7299363
--- /dev/null
+++ b/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts
@@ -0,0 +1,139 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../../ftr_provider_context';
+
+export default function ({ getPageObjects, getService }: FtrProviderContext) {
+ const esArchiver = getService('esArchiver');
+ const find = getService('find');
+ const retry = getService('retry');
+ const spacesService = getService('spaces');
+ const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security']);
+ const testSubjects = getService('testSubjects');
+ const appsMenu = getService('appsMenu');
+
+ const testData = { serviceName: 'opbeans-go' };
+
+ describe('latency correlations', () => {
+ describe('space with no features disabled', () => {
+ before(async () => {
+ await esArchiver.load('x-pack/test/functional/es_archives/infra/8.0.0/metrics_and_apm');
+ await spacesService.create({
+ id: 'custom_space',
+ name: 'custom_space',
+ disabledFeatures: [],
+ });
+ });
+
+ after(async () => {
+ await spacesService.delete('custom_space');
+ });
+
+ it('shows apm navlink', async () => {
+ await PageObjects.common.navigateToApp('home', {
+ basePath: '/s/custom_space',
+ });
+ const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
+ expect(navLinks).to.contain('APM');
+ });
+
+ it('can navigate to APM app', async () => {
+ await PageObjects.common.navigateToApp('apm');
+
+ await retry.try(async () => {
+ await testSubjects.existOrFail('apmMainContainer', {
+ timeout: 10000,
+ });
+
+ const apmMainContainerText = await testSubjects.getVisibleTextAll('apmMainContainer');
+ const apmMainContainerTextItems = apmMainContainerText[0].split('\n');
+ expect(apmMainContainerTextItems).to.contain('No services found');
+ });
+ });
+
+ it('sets the timePicker to return data', async () => {
+ await PageObjects.timePicker.timePickerExists();
+
+ const fromTime = 'Jul 29, 2019 @ 00:00:00.000';
+ const toTime = 'Jul 30, 2019 @ 00:00:00.000';
+ await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
+
+ await retry.try(async () => {
+ const apmMainContainerText = await testSubjects.getVisibleTextAll('apmMainContainer');
+ const apmMainContainerTextItems = apmMainContainerText[0].split('\n');
+
+ expect(apmMainContainerTextItems).to.not.contain('No services found');
+
+ expect(apmMainContainerTextItems).to.contain('opbeans-go');
+ expect(apmMainContainerTextItems).to.contain('opbeans-node');
+ expect(apmMainContainerTextItems).to.contain('opbeans-ruby');
+ expect(apmMainContainerTextItems).to.contain('opbeans-python');
+ expect(apmMainContainerTextItems).to.contain('opbeans-dotnet');
+ expect(apmMainContainerTextItems).to.contain('opbeans-java');
+
+ expect(apmMainContainerTextItems).to.contain('development');
+
+ const items = await testSubjects.findAll('apmServiceListAppLink');
+ expect(items.length).to.be(6);
+ });
+ });
+
+ it(`navigates to the 'opbeans-go' service overview page`, async function () {
+ await find.clickByDisplayedLinkText(testData.serviceName);
+
+ await retry.try(async () => {
+ const apmMainTemplateHeaderServiceName = await testSubjects.getVisibleTextAll(
+ 'apmMainTemplateHeaderServiceName'
+ );
+ expect(apmMainTemplateHeaderServiceName).to.contain('opbeans-go');
+ });
+ });
+
+ it('shows the correlations flyout', async function () {
+ await testSubjects.click('apmViewCorrelationsButton');
+
+ await retry.try(async () => {
+ await testSubjects.existOrFail('apmCorrelationsFlyout', {
+ timeout: 10000,
+ });
+
+ const apmCorrelationsFlyoutHeader = await testSubjects.getVisibleText(
+ 'apmCorrelationsFlyoutHeader'
+ );
+
+ expect(apmCorrelationsFlyoutHeader).to.contain('Correlations BETA');
+ });
+ });
+
+ it('loads the correlation results', async function () {
+ await retry.try(async () => {
+ // Assert that the data fully loaded to 100%
+ const apmCorrelationsLatencyCorrelationsProgressTitle = await testSubjects.getVisibleText(
+ 'apmCorrelationsLatencyCorrelationsProgressTitle'
+ );
+ expect(apmCorrelationsLatencyCorrelationsProgressTitle).to.be('Progress: 100%');
+
+ // Assert that the Correlations Chart and its header are present
+ const apmCorrelationsLatencyCorrelationsChartTitle = await testSubjects.getVisibleText(
+ 'apmCorrelationsLatencyCorrelationsChartTitle'
+ );
+ expect(apmCorrelationsLatencyCorrelationsChartTitle).to.be(
+ `Latency distribution for ${testData.serviceName}`
+ );
+ await testSubjects.existOrFail('apmCorrelationsChart', {
+ timeout: 10000,
+ });
+
+ // Assert that results for the given service didn't find any correlations
+ const apmCorrelationsTable = await testSubjects.getVisibleText('apmCorrelationsTable');
+ expect(apmCorrelationsTable).to.be('No significant correlations found');
+ });
+ });
+ });
+ });
+}
diff --git a/x-pack/test/functional/apps/apm/index.ts b/x-pack/test/functional/apps/apm/index.ts
index d2531b72e1b5..e4db5a66aa55 100644
--- a/x-pack/test/functional/apps/apm/index.ts
+++ b/x-pack/test/functional/apps/apm/index.ts
@@ -11,5 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
describe('APM specs', function () {
this.tags('ciGroup6');
loadTestFile(require.resolve('./feature_controls'));
+ loadTestFile(require.resolve('./correlations'));
});
}
diff --git a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts b/x-pack/test/functional/apps/dashboard/_async_dashboard.ts
index dc5afe4aa422..92cdc72ffc81 100644
--- a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts
+++ b/x-pack/test/functional/apps/dashboard/_async_dashboard.ts
@@ -15,7 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const esArchiver = getService('esArchiver');
const log = getService('log');
- const pieChart = getService('pieChart');
+ const elasticChart = getService('elasticChart');
const find = getService('find');
const renderable = getService('renderable');
const dashboardExpect = getService('dashboardExpect');
@@ -136,8 +136,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
// check at least one visualization
await renderable.waitForRender();
- log.debug('Checking pie charts rendered');
- await pieChart.expectPieSliceCount(4);
+ log.debug('Checking charts rendered');
+ await elasticChart.waitForRenderComplete('lnsVisualizationContainer');
await appMenu.clickLink('Discover');
await retry.try(async function () {
@@ -147,8 +147,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await appMenu.clickLink('Dashboard');
await PageObjects.header.waitUntilLoadingHasFinished();
await renderable.waitForRender();
- log.debug('Checking pie charts rendered');
- await pieChart.expectPieSliceCount(4);
+ log.debug('Checking charts rendered');
+ await elasticChart.waitForRenderComplete('lnsVisualizationContainer');
});
it('toggle from Discover to Dashboard attempt 1', async () => {
@@ -160,8 +160,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await appMenu.clickLink('Dashboard');
await PageObjects.header.waitUntilLoadingHasFinished();
await renderable.waitForRender();
- log.debug('Checking pie charts rendered');
- await pieChart.expectPieSliceCount(4);
+ log.debug('Checking charts rendered');
+ await elasticChart.waitForRenderComplete('lnsVisualizationContainer');
});
it('toggle from Discover to Dashboard attempt 2', async () => {
@@ -173,11 +173,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await appMenu.clickLink('Dashboard');
await PageObjects.header.waitUntilLoadingHasFinished();
await renderable.waitForRender();
- log.debug('Checking pie charts rendered');
- await pieChart.expectPieSliceCount(4);
+ log.debug('Checking charts rendered');
+ await elasticChart.waitForRenderComplete('lnsVisualizationContainer');
- log.debug('Checking area, bar and heatmap charts rendered');
- await dashboardExpect.seriesElementCount(15);
log.debug('Checking saved searches rendered');
await dashboardExpect.savedSearchRowCount(10);
log.debug('Checking input controls rendered');
@@ -185,8 +183,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
log.debug('Checking tag cloud rendered');
await dashboardExpect.tagCloudWithValuesFound(['Sunny', 'Rain', 'Clear', 'Cloudy', 'Hail']);
log.debug('Checking vega chart rendered');
- const tsvb = await find.existsByCssSelector('.vgaVis__view');
- expect(tsvb).to.be(true);
+ expect(await find.existsByCssSelector('.vgaVis__view')).to.be(true);
});
});
}
diff --git a/x-pack/test/functional/apps/maps/sample_data.js b/x-pack/test/functional/apps/maps/sample_data.js
index 10e760fa9d94..fa12e5158ac7 100644
--- a/x-pack/test/functional/apps/maps/sample_data.js
+++ b/x-pack/test/functional/apps/maps/sample_data.js
@@ -135,7 +135,7 @@ export default function ({ getPageObjects, getService, updateBaselines }) {
describe('flights', () => {
before(async () => {
- await PageObjects.maps.loadSavedMap('[Flights] Origin and Destination Flight Time');
+ await PageObjects.maps.loadSavedMap('[Flights] Origin Time Delayed');
await PageObjects.maps.toggleLayerVisibility('Road map');
await PageObjects.timePicker.setCommonlyUsedTime('sample_data range');
await PageObjects.maps.enterFullScreen();
diff --git a/x-pack/test/functional/es_archives/rule_registry/alerts/data.json b/x-pack/test/functional/es_archives/rule_registry/alerts/data.json
index a9837210c2e5..36a73c1994c9 100644
--- a/x-pack/test/functional/es_archives/rule_registry/alerts/data.json
+++ b/x-pack/test/functional/es_archives/rule_registry/alerts/data.json
@@ -8,7 +8,40 @@
"rule.id": "apm.error_rate",
"message": "hello world 1",
"kibana.rac.alert.owner": "apm",
- "kibana.rac.alert.status": "open"
+ "kibana.rac.alert.status": "open",
+ "kibana.space_ids": ["space1", "space2"]
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".alerts-observability-apm",
+ "id": "space1alert",
+ "source": {
+ "@timestamp": "2020-12-16T15:16:18.570Z",
+ "rule.id": "apm.error_rate",
+ "message": "hello world 1",
+ "kibana.rac.alert.owner": "apm",
+ "kibana.rac.alert.status": "open",
+ "kibana.space_ids": ["space1"]
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".alerts-observability-apm",
+ "id": "space2alert",
+ "source": {
+ "@timestamp": "2020-12-16T15:16:18.570Z",
+ "rule.id": "apm.error_rate",
+ "message": "hello world 1",
+ "kibana.rac.alert.owner": "apm",
+ "kibana.rac.alert.status": "open",
+ "kibana.space_ids": ["space2"]
}
}
}
@@ -23,7 +56,8 @@
"rule.id": "siem.signals",
"message": "hello world security",
"kibana.rac.alert.owner": "siem",
- "kibana.rac.alert.status": "open"
+ "kibana.rac.alert.status": "open",
+ "kibana.space_ids": ["space1", "space2"]
}
}
}
diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_by_id.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_by_id.ts
index cf3cc88f2cfc..05b55128fab3 100644
--- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_by_id.ts
+++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_by_id.ts
@@ -98,6 +98,30 @@ export default ({ getService }: FtrProviderContext) => {
await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts');
});
+ it('superuser should be able to access an alert in a given space', async () => {
+ await supertestWithoutAuth
+ .get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?id=space1alert&index=${APM_ALERT_INDEX}`)
+ .auth(superUser.username, superUser.password)
+ .set('kbn-xsrf', 'true')
+ .expect(200);
+ });
+
+ it('superuser should NOT be able to access an alert in a space which the alert does not exist in', async () => {
+ await supertestWithoutAuth
+ .get(`${getSpaceUrlPrefix(SPACE2)}${TEST_URL}?id=space1alert&index=${APM_ALERT_INDEX}`)
+ .auth(superUser.username, superUser.password)
+ .set('kbn-xsrf', 'true')
+ .expect(404);
+ });
+
+ it('obs only space 1 user should NOT be able to access an alert in a space which the user does not have access to', async () => {
+ await supertestWithoutAuth
+ .get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?id=space2alert&index=${APM_ALERT_INDEX}`)
+ .auth(superUser.username, superUser.password)
+ .set('kbn-xsrf', 'true')
+ .expect(404);
+ });
+
function addTests({ space, authorizedUsers, unauthorizedUsers, alertId, index }: TestCase) {
authorizedUsers.forEach(({ username, password }) => {
it(`${username} should be able to access alert ${alertId} in ${space}/${index}`, async () => {
diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/trial/update_alert.ts b/x-pack/test/rule_registry/security_and_spaces/tests/trial/update_alert.ts
index c126c434bd4c..14d903c7ac55 100644
--- a/x-pack/test/rule_registry/security_and_spaces/tests/trial/update_alert.ts
+++ b/x-pack/test/rule_registry/security_and_spaces/tests/trial/update_alert.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
import expect from '@kbn/expect';
+import { omit } from 'lodash/fp';
import {
superUser,
@@ -104,14 +105,12 @@ export default ({ getService }: FtrProviderContext) => {
_version: Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'),
})
.expect(200);
- expect(res.body).to.eql({
+ expect(omit(['_version', '_seq_no'], res.body)).to.eql({
success: true,
_index: '.alerts-observability-apm',
_id: 'NoxgpHkBqbdrfX07MqXV',
result: 'updated',
_shards: { total: 2, successful: 1, failed: 0 },
- _version: 'WzEsMV0=',
- _seq_no: 1,
_primary_term: 1,
});
});
diff --git a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/save_search_session_relative_time.ts b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/save_search_session_relative_time.ts
index bd5f0896d96c..371b0f8d0b0b 100644
--- a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/save_search_session_relative_time.ts
+++ b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/save_search_session_relative_time.ts
@@ -24,12 +24,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
]);
const dashboardPanelActions = getService('dashboardPanelActions');
const inspector = getService('inspector');
- const pieChart = getService('pieChart');
+ const elasticChart = getService('elasticChart');
const find = getService('find');
const dashboardExpect = getService('dashboardExpect');
const searchSessions = getService('searchSessions');
- describe('save a search sessions with relative time', () => {
+ // Failing: See https://github.com/elastic/kibana/issues/97701
+ describe.skip('save a search sessions with relative time', () => {
before(async () => {
await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', {
useActualUrl: true,
@@ -57,15 +58,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('Saves and restores a session with relative time ranges', async () => {
await PageObjects.dashboard.loadSavedDashboard('[Flights] Global Flight Dashboard');
await PageObjects.dashboard.waitForRenderComplete();
- await PageObjects.timePicker.pauseAutoRefresh(); // sample data has auto-refresh on
await PageObjects.header.waitUntilLoadingHasFinished();
- await PageObjects.dashboard.waitForRenderComplete();
-
- // saving dashboard to populate map buffer. See https://github.com/elastic/kibana/pull/91148 for more info
- // This can be removed after a fix to https://github.com/elastic/kibana/issues/98180 is completed
- await PageObjects.dashboard.switchToEditMode();
- await PageObjects.dashboard.clickQuickSave();
- await PageObjects.dashboard.clickCancelOutOfEditMode();
await searchSessions.expectState('completed');
await searchSessions.save();
@@ -94,10 +87,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
async function checkSampleDashboardLoaded() {
log.debug('Checking no error labels');
await testSubjects.missingOrFail('embeddableErrorLabel');
- log.debug('Checking pie charts rendered');
- await pieChart.expectPieSliceCount(4);
- log.debug('Checking area, bar and heatmap charts rendered');
- await dashboardExpect.seriesElementCount(15);
+ log.debug('Checking charts rendered');
+ await elasticChart.waitForRenderComplete('lnsVisualizationContainer');
log.debug('Checking saved searches rendered');
await dashboardExpect.savedSearchRowCount(11);
log.debug('Checking input controls rendered');
@@ -105,14 +96,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
log.debug('Checking tag cloud rendered');
await dashboardExpect.tagCloudWithValuesFound(['Sunny', 'Rain', 'Clear', 'Cloudy', 'Hail']);
log.debug('Checking vega chart rendered');
- const tsvb = await find.existsByCssSelector('.vgaVis__view');
- expect(tsvb).to.be(true);
+ expect(await find.existsByCssSelector('.vgaVis__view')).to.be(true);
log.debug('Checking map rendered');
- await dashboardPanelActions.openInspectorByTitle(
- '[Flights] Origin and Destination Flight Time'
- );
- await testSubjects.click('inspectorRequestChooser');
- await testSubjects.click(`inspectorRequestChooserFlight Origin Location`);
+ await dashboardPanelActions.openInspectorByTitle('[Flights] Origin Time Delayed');
const requestStats = await inspector.getTableData();
const totalHits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits');
expect(totalHits).to.equal('0');
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 1f57cd1b6db3..4656edc7edfb 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
@@ -23,7 +23,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/106051
+ describe.skip('test metadata api', () => {
describe(`POST ${HOST_METADATA_LIST_ROUTE} when index is empty`, () => {
it('metadata api should return empty result when index is empty', async () => {
await deleteMetadataStream(getService);
diff --git a/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts b/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts
index 17b457151bd9..0c43528ad8b8 100644
--- a/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts
+++ b/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts
@@ -128,7 +128,7 @@ export default function ({
});
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.home.addSampleDataSet('flights');
- await PageObjects.maps.loadSavedMap('[Flights] Origin and Destination Flight Time');
+ await PageObjects.maps.loadSavedMap('[Flights] Origin Time Delayed');
await PageObjects.maps.toggleLayerVisibility('Road map');
await PageObjects.timePicker.setCommonlyUsedTime('sample_data range');
await PageObjects.maps.enterFullScreen();