+ {isManagedPolicy && (
+ <>
+
+ }
+ color="danger"
+ iconType="alert"
+ data-test-subj="editManagedPolicyCallOut"
+ >
+
+
+
+
+
+ >
+ )}
void;
}
export class ConfirmDelete extends Component {
+ public state = {
+ isDeleteConfirmed: false,
+ };
+
+ setIsDeleteConfirmed = (confirmed: boolean) => {
+ this.setState({
+ isDeleteConfirmed: confirmed,
+ });
+ };
+
deletePolicy = async () => {
const { policyToDelete, callback } = this.props;
const policyName = policyToDelete.name;
@@ -43,8 +53,12 @@ export class ConfirmDelete extends Component {
callback();
}
};
+ isPolicyPolicy = true;
render() {
const { policyToDelete, onCancel } = this.props;
+ const { isDeleteConfirmed } = this.state;
+ const isManagedPolicy = policyToDelete.policy?._meta?.managed;
+
const title = i18n.translate('xpack.indexLifecycleMgmt.confirmDelete.title', {
defaultMessage: 'Delete policy "{name}"',
values: { name: policyToDelete.name },
@@ -68,13 +82,47 @@ export class ConfirmDelete extends Component {
/>
}
buttonColor="danger"
+ confirmButtonDisabled={isManagedPolicy ? !isDeleteConfirmed : false}
>
-
-
-
+ {isManagedPolicy ? (
+
+ }
+ color="danger"
+ iconType="alert"
+ data-test-subj="deleteManagedPolicyCallOut"
+ >
+
+
+
+
+ }
+ checked={isDeleteConfirmed}
+ onChange={(e) => this.setIsDeleteConfirmed(e.target.checked)}
+ />
+
+ ) : (
+
+
+
+ )}
);
}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/components/policy_table.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/components/policy_table.tsx
index 8a89759a4225e..2d79737baf2bc 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/components/policy_table.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/components/policy_table.tsx
@@ -5,8 +5,17 @@
* 2.0.
*/
-import React from 'react';
-import { EuiButtonEmpty, EuiLink, EuiInMemoryTable, EuiToolTip, EuiButtonIcon } from '@elastic/eui';
+import React, { useMemo } from 'react';
+import {
+ EuiButtonEmpty,
+ EuiLink,
+ EuiInMemoryTable,
+ EuiToolTip,
+ EuiButtonIcon,
+ EuiBadge,
+ EuiFlexItem,
+ EuiSwitch,
+} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@@ -15,6 +24,8 @@ import { METRIC_TYPE } from '@kbn/analytics';
import { useHistory } from 'react-router-dom';
import { EuiBasicTableColumn } from '@elastic/eui/src/components/basic_table/basic_table';
import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { useStateWithLocalStorage } from '../../../lib/settings_local_storage';
import { PolicyFromES } from '../../../../../common/types';
import { useKibana } from '../../../../shared_imports';
import { getIndicesListPath, getPolicyEditPath } from '../../../services/navigation';
@@ -45,17 +56,63 @@ const actionTooltips = {
),
};
+const managedPolicyTooltips = {
+ badge: i18n.translate('xpack.indexLifecycleMgmt.policyTable.templateBadgeType.managedLabel', {
+ defaultMessage: 'Managed',
+ }),
+ badgeTooltip: i18n.translate(
+ 'xpack.indexLifecycleMgmt.policyTable.templateBadgeType.managedDescription',
+ {
+ defaultMessage:
+ 'This policy is preconfigured and managed by Elastic; editing or deleting this policy might break Kibana.',
+ }
+ ),
+};
+
interface Props {
policies: PolicyFromES[];
}
+const SHOW_MANAGED_POLICIES_BY_DEFAULT = 'ILM_SHOW_MANAGED_POLICIES_BY_DEFAULT';
+
export const PolicyTable: React.FunctionComponent = ({ policies }) => {
const history = useHistory();
const {
services: { getUrlForApp },
} = useKibana();
-
+ const [managedPoliciesVisible, setManagedPoliciesVisible] = useStateWithLocalStorage(
+ SHOW_MANAGED_POLICIES_BY_DEFAULT,
+ false
+ );
const { setListAction } = usePolicyListContext();
+ const searchOptions = useMemo(
+ () => ({
+ box: { incremental: true, 'data-test-subj': 'ilmSearchBar' },
+ toolsRight: (
+
+ setManagedPoliciesVisible(event.target.checked)}
+ label={
+
+ }
+ />
+
+ ),
+ }),
+ [managedPoliciesVisible, setManagedPoliciesVisible]
+ );
+
+ const filteredPolicies = useMemo(() => {
+ return managedPoliciesVisible
+ ? policies
+ : policies.filter((item) => !item.policy?._meta?.managed);
+ }, [policies, managedPoliciesVisible]);
const columns: Array> = [
{
@@ -65,17 +122,31 @@ export const PolicyTable: React.FunctionComponent = ({ policies }) => {
defaultMessage: 'Name',
}),
sortable: true,
- render: (value: string) => {
+ render: (value: string, item) => {
+ const isManaged = item.policy?._meta?.managed;
return (
-
- trackUiMetric(METRIC_TYPE.CLICK, UIM_EDIT_CLICK)
+ <>
+
+ trackUiMetric(METRIC_TYPE.CLICK, UIM_EDIT_CLICK)
+ )}
+ >
+ {value}
+
+
+ {isManaged && (
+ <>
+
+
+
+ {managedPolicyTooltips.badge}
+
+
+ >
)}
- >
- {value}
-
+ >
);
},
},
@@ -191,11 +262,9 @@ export const PolicyTable: React.FunctionComponent = ({ policies }) => {
direction: 'asc',
},
}}
- search={{
- box: { incremental: true, 'data-test-subj': 'ilmSearchBar' },
- }}
+ search={searchOptions}
tableLayout="auto"
- items={policies}
+ items={filteredPolicies}
columns={columns}
rowProps={(policy: PolicyFromES) => ({ 'data-test-subj': `policyTableRow-${policy.name}` })}
/>
From c24488361a0f53b9d71d4248dd1a57d00520adad Mon Sep 17 00:00:00 2001
From: Nathan Reese
Date: Fri, 20 May 2022 10:35:00 -0600
Subject: [PATCH 22/39] [maps] show marker size in legend (#132549)
* [Maps] size legend
* clean-up
* refine spacing
* clean up
* more cleanup
* use euiTheme for colors
* fix jest test
* do not show marker sizes for icons
* remove lodash
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../components/legend/marker_size_legend.tsx | 164 ++++++++++++++++
.../dynamic_size_property.test.tsx.snap | 176 +++++++++++++++++-
.../dynamic_size_property.test.tsx | 48 ++++-
.../dynamic_size_property.tsx | 7 +-
4 files changed, 386 insertions(+), 9 deletions(-)
create mode 100644 x-pack/plugins/maps/public/classes/styles/vector/components/legend/marker_size_legend.tsx
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/marker_size_legend.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/marker_size_legend.tsx
new file mode 100644
index 0000000000000..295e7c57b7a22
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/marker_size_legend.tsx
@@ -0,0 +1,164 @@
+/*
+ * 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, { Component } from 'react';
+import { euiThemeVars } from '@kbn/ui-theme';
+import { EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui';
+import { DynamicSizeProperty } from '../../properties/dynamic_size_property';
+
+const FONT_SIZE = 10;
+const HALF_FONT_SIZE = FONT_SIZE / 2;
+const MIN_MARKER_DISTANCE = (FONT_SIZE + 2) / 2;
+
+const EMPTY_VALUE = '';
+
+interface Props {
+ style: DynamicSizeProperty;
+}
+
+interface State {
+ label: string;
+}
+
+export class MarkerSizeLegend extends Component {
+ private _isMounted: boolean = false;
+
+ state: State = {
+ label: EMPTY_VALUE,
+ };
+
+ componentDidMount() {
+ this._isMounted = true;
+ this._loadLabel();
+ }
+
+ componentDidUpdate() {
+ this._loadLabel();
+ }
+
+ componentWillUnmount() {
+ this._isMounted = false;
+ }
+
+ async _loadLabel() {
+ const field = this.props.style.getField();
+ if (!field) {
+ return;
+ }
+ const label = await field.getLabel();
+ if (this._isMounted && this.state.label !== label) {
+ this.setState({ label });
+ }
+ }
+
+ _formatValue(value: string | number) {
+ return value === EMPTY_VALUE ? value : this.props.style.formatField(value);
+ }
+
+ _renderMarkers() {
+ const fieldMeta = this.props.style.getRangeFieldMeta();
+ const options = this.props.style.getOptions();
+ if (!fieldMeta || !options) {
+ return null;
+ }
+
+ const circleStyle = {
+ fillOpacity: 0,
+ stroke: euiThemeVars.euiTextColor,
+ strokeWidth: 1,
+ };
+
+ const svgHeight = options.maxSize * 2 + HALF_FONT_SIZE + circleStyle.strokeWidth * 2;
+ const circleCenterX = options.maxSize + circleStyle.strokeWidth;
+ const circleBottomY = svgHeight - circleStyle.strokeWidth;
+
+ function makeMarker(radius: number, formattedValue: string | number) {
+ const circleCenterY = circleBottomY - radius;
+ const circleTopY = circleCenterY - radius;
+ return (
+
+
+
+ {formattedValue}
+
+
+
+ );
+ }
+
+ function getMarkerRadius(percentage: number) {
+ const delta = options.maxSize - options.minSize;
+ return percentage * delta + options.minSize;
+ }
+
+ function getValue(percentage: number) {
+ // Markers interpolated by area instead of radius to be more consistent with how the human eye+brain perceive shapes
+ // and their visual relevance
+ // This function mirrors output of maplibre expression created from DynamicSizeProperty.getMbSizeExpression
+ const value = Math.pow(percentage * Math.sqrt(fieldMeta!.delta), 2) + fieldMeta!.min;
+ return fieldMeta!.delta > 3 ? Math.round(value) : value;
+ }
+
+ const markers = [];
+
+ if (fieldMeta.delta > 0) {
+ const smallestMarker = makeMarker(options.minSize, this._formatValue(fieldMeta.min));
+ markers.push(smallestMarker);
+
+ const markerDelta = options.maxSize - options.minSize;
+ if (markerDelta > MIN_MARKER_DISTANCE * 3) {
+ markers.push(makeMarker(getMarkerRadius(0.25), this._formatValue(getValue(0.25))));
+ markers.push(makeMarker(getMarkerRadius(0.5), this._formatValue(getValue(0.5))));
+ markers.push(makeMarker(getMarkerRadius(0.75), this._formatValue(getValue(0.75))));
+ } else if (markerDelta > MIN_MARKER_DISTANCE) {
+ markers.push(makeMarker(getMarkerRadius(0.5), this._formatValue(getValue(0.5))));
+ }
+ }
+
+ const largestMarker = makeMarker(options.maxSize, this._formatValue(fieldMeta.max));
+ markers.push(largestMarker);
+
+ return (
+
+ );
+ }
+
+ render() {
+ return (
+
+
+
+
+
+
+ {this.state.label}
+
+
+
+
+
+ {this._renderMarkers()}
+
+ );
+ }
+}
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/__snapshots__/dynamic_size_property.test.tsx.snap b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/__snapshots__/dynamic_size_property.test.tsx.snap
index 9dc0e99669c79..bf239aa40e33a 100644
--- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/__snapshots__/dynamic_size_property.test.tsx.snap
+++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/__snapshots__/dynamic_size_property.test.tsx.snap
@@ -1,6 +1,165 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`renderLegendDetailRow Should render as range 1`] = `
+exports[`renderLegendDetailRow Should render icon size scale 1`] = `
+
+
+
+
+
+
+
+ foobar_label
+
+
+
+
+
+
+
+
+`;
+
+exports[`renderLegendDetailRow Should render line width simple range 1`] = `
@@ -36,9 +196,10 @@ exports[`renderLegendDetailRow Should render as range 1`] = `
@@ -56,8 +217,9 @@ exports[`renderLegendDetailRow Should render as range 1`] = `
`;
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/dynamic_size_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/dynamic_size_property.test.tsx
index 0446b9e30f47b..9f92d81313da7 100644
--- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/dynamic_size_property.test.tsx
+++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/dynamic_size_property.test.tsx
@@ -20,7 +20,53 @@ import { IField } from '../../../../fields/field';
import { IVectorLayer } from '../../../../layers/vector_layer';
describe('renderLegendDetailRow', () => {
- test('Should render as range', async () => {
+ test('Should render line width simple range', async () => {
+ const field = {
+ getLabel: async () => {
+ return 'foobar_label';
+ },
+ getName: () => {
+ return 'foodbar';
+ },
+ getOrigin: () => {
+ return FIELD_ORIGIN.SOURCE;
+ },
+ supportsFieldMetaFromEs: () => {
+ return true;
+ },
+ supportsFieldMetaFromLocalData: () => {
+ return true;
+ },
+ } as unknown as IField;
+ const sizeProp = new DynamicSizeProperty(
+ { minSize: 0, maxSize: 10, fieldMetaOptions: { isEnabled: true } },
+ VECTOR_STYLES.LINE_WIDTH,
+ field,
+ {} as unknown as IVectorLayer,
+ () => {
+ return (value: RawValue) => value + '_format';
+ },
+ false
+ );
+ sizeProp.getRangeFieldMeta = () => {
+ return {
+ min: 0,
+ max: 100,
+ delta: 100,
+ };
+ };
+
+ const legendRow = sizeProp.renderLegendDetailRow();
+ const component = shallow(legendRow);
+
+ // Ensure all promises resolve
+ await new Promise((resolve) => process.nextTick(resolve));
+ // Ensure the state changes are reflected
+ component.update();
+ expect(component).toMatchSnapshot();
+ });
+
+ test('Should render icon size scale', async () => {
const field = {
getLabel: async () => {
return 'foobar_label';
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/dynamic_size_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/dynamic_size_property.tsx
index d8fe8463edba8..83ac50c7b4eaa 100644
--- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/dynamic_size_property.tsx
+++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/dynamic_size_property.tsx
@@ -9,6 +9,7 @@ import React from 'react';
import type { Map as MbMap } from '@kbn/mapbox-gl';
import { DynamicStyleProperty } from '../dynamic_style_property';
import { OrdinalLegend } from '../../components/legend/ordinal_legend';
+import { MarkerSizeLegend } from '../../components/legend/marker_size_legend';
import { makeMbClampedNumberExpression } from '../../style_util';
import {
FieldFormatter,
@@ -141,6 +142,10 @@ export class DynamicSizeProperty extends DynamicStyleProperty;
+ return this.getStyleName() === VECTOR_STYLES.ICON_SIZE && !this._isSymbolizedAsIcon ? (
+
+ ) : (
+
+ );
}
}
From 583d2b78e085ec7e51f6d7608b0d1fe75f1bfcc4 Mon Sep 17 00:00:00 2001
From: Byron Hulcher
Date: Fri, 20 May 2022 13:12:32 -0400
Subject: [PATCH 23/39] [Workplace Search] Add documentation links for v8.3.0
connectors (#132547)
---
packages/kbn-doc-links/src/get_doc_links.ts | 6 ++++++
packages/kbn-doc-links/src/types.ts | 6 ++++++
.../shared/doc_links/doc_links.ts | 20 +++++++++++++++++++
.../views/content_sources/source_data.tsx | 12 +++++------
4 files changed, 38 insertions(+), 6 deletions(-)
diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts
index 55909e360b0e5..53f69411c43dd 100644
--- a/packages/kbn-doc-links/src/get_doc_links.ts
+++ b/packages/kbn-doc-links/src/get_doc_links.ts
@@ -125,7 +125,9 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
apiKeys: `${WORKPLACE_SEARCH_DOCS}workplace-search-api-authentication.html`,
box: `${WORKPLACE_SEARCH_DOCS}workplace-search-box-connector.html`,
confluenceCloud: `${WORKPLACE_SEARCH_DOCS}workplace-search-confluence-cloud-connector.html`,
+ confluenceCloudConnectorPackage: `${WORKPLACE_SEARCH_DOCS}confluence-cloud.html`,
confluenceServer: `${WORKPLACE_SEARCH_DOCS}workplace-search-confluence-server-connector.html`,
+ customConnectorPackage: `${WORKPLACE_SEARCH_DOCS}custom-connector-package.html`,
customSources: `${WORKPLACE_SEARCH_DOCS}workplace-search-custom-api-sources.html`,
customSourcePermissions: `${WORKPLACE_SEARCH_DOCS}workplace-search-custom-api-sources.html#custom-api-source-document-level-access-control`,
documentPermissions: `${WORKPLACE_SEARCH_DOCS}workplace-search-sources-document-permissions.html`,
@@ -139,7 +141,9 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
indexingSchedule: `${WORKPLACE_SEARCH_DOCS}workplace-search-customizing-indexing-rules.html#_indexing_schedule`,
jiraCloud: `${WORKPLACE_SEARCH_DOCS}workplace-search-jira-cloud-connector.html`,
jiraServer: `${WORKPLACE_SEARCH_DOCS}workplace-search-jira-server-connector.html`,
+ networkDrive: `${WORKPLACE_SEARCH_DOCS}network-drives.html`,
oneDrive: `${WORKPLACE_SEARCH_DOCS}workplace-search-onedrive-connector.html`,
+ outlook: `${WORKPLACE_SEARCH_DOCS}microsoft-outlook.html`,
permissions: `${WORKPLACE_SEARCH_DOCS}workplace-search-permissions.html#organizational-sources-private-sources`,
salesforce: `${WORKPLACE_SEARCH_DOCS}workplace-search-salesforce-connector.html`,
security: `${WORKPLACE_SEARCH_DOCS}workplace-search-security.html`,
@@ -148,7 +152,9 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
sharePointServer: `${WORKPLACE_SEARCH_DOCS}sharepoint-server.html`,
slack: `${WORKPLACE_SEARCH_DOCS}workplace-search-slack-connector.html`,
synch: `${WORKPLACE_SEARCH_DOCS}workplace-search-customizing-indexing-rules.html`,
+ teams: `${WORKPLACE_SEARCH_DOCS}microsoft-teams.html`,
zendesk: `${WORKPLACE_SEARCH_DOCS}workplace-search-zendesk-connector.html`,
+ zoom: `${WORKPLACE_SEARCH_DOCS}zoom.html`,
},
metricbeat: {
base: `${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}`,
diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts
index c492509e80511..6dc3ad0f5fdda 100644
--- a/packages/kbn-doc-links/src/types.ts
+++ b/packages/kbn-doc-links/src/types.ts
@@ -111,7 +111,9 @@ export interface DocLinks {
readonly apiKeys: string;
readonly box: string;
readonly confluenceCloud: string;
+ readonly confluenceCloudConnectorPackage: string;
readonly confluenceServer: string;
+ readonly customConnectorPackage: string;
readonly customSources: string;
readonly customSourcePermissions: string;
readonly documentPermissions: string;
@@ -125,7 +127,9 @@ export interface DocLinks {
readonly indexingSchedule: string;
readonly jiraCloud: string;
readonly jiraServer: string;
+ readonly networkDrive: string;
readonly oneDrive: string;
+ readonly outlook: string;
readonly permissions: string;
readonly salesforce: string;
readonly security: string;
@@ -134,7 +138,9 @@ export interface DocLinks {
readonly sharePointServer: string;
readonly slack: string;
readonly synch: string;
+ readonly teams: string;
readonly zendesk: string;
+ readonly zoom: string;
};
readonly heartbeat: {
readonly base: string;
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts
index b037a5aed6217..1d38cb584fa43 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts
@@ -64,7 +64,9 @@ class DocLinks {
public workplaceSearchApiKeys: string;
public workplaceSearchBox: string;
public workplaceSearchConfluenceCloud: string;
+ public workplaceSearchConfluenceCloudConnectorPackage: string;
public workplaceSearchConfluenceServer: string;
+ public workplaceSearchCustomConnectorPackage: string;
public workplaceSearchCustomSources: string;
public workplaceSearchCustomSourcePermissions: string;
public workplaceSearchDocumentPermissions: string;
@@ -78,7 +80,9 @@ class DocLinks {
public workplaceSearchIndexingSchedule: string;
public workplaceSearchJiraCloud: string;
public workplaceSearchJiraServer: string;
+ public workplaceSearchNetworkDrive: string;
public workplaceSearchOneDrive: string;
+ public workplaceSearchOutlook: string;
public workplaceSearchPermissions: string;
public workplaceSearchSalesforce: string;
public workplaceSearchSecurity: string;
@@ -87,7 +91,9 @@ class DocLinks {
public workplaceSearchSharePointServer: string;
public workplaceSearchSlack: string;
public workplaceSearchSynch: string;
+ public workplaceSearchTeams: string;
public workplaceSearchZendesk: string;
+ public workplaceSearchZoom: string;
constructor() {
this.appSearchApis = '';
@@ -146,7 +152,9 @@ class DocLinks {
this.workplaceSearchApiKeys = '';
this.workplaceSearchBox = '';
this.workplaceSearchConfluenceCloud = '';
+ this.workplaceSearchConfluenceCloudConnectorPackage = '';
this.workplaceSearchConfluenceServer = '';
+ this.workplaceSearchCustomConnectorPackage = '';
this.workplaceSearchCustomSources = '';
this.workplaceSearchCustomSourcePermissions = '';
this.workplaceSearchDocumentPermissions = '';
@@ -160,7 +168,9 @@ class DocLinks {
this.workplaceSearchIndexingSchedule = '';
this.workplaceSearchJiraCloud = '';
this.workplaceSearchJiraServer = '';
+ this.workplaceSearchNetworkDrive = '';
this.workplaceSearchOneDrive = '';
+ this.workplaceSearchOutlook = '';
this.workplaceSearchPermissions = '';
this.workplaceSearchSalesforce = '';
this.workplaceSearchSecurity = '';
@@ -169,7 +179,9 @@ class DocLinks {
this.workplaceSearchSharePointServer = '';
this.workplaceSearchSlack = '';
this.workplaceSearchSynch = '';
+ this.workplaceSearchTeams = '';
this.workplaceSearchZendesk = '';
+ this.workplaceSearchZoom = '';
}
public setDocLinks(docLinks: DocLinksStart): void {
@@ -230,7 +242,11 @@ class DocLinks {
this.workplaceSearchApiKeys = docLinks.links.workplaceSearch.apiKeys;
this.workplaceSearchBox = docLinks.links.workplaceSearch.box;
this.workplaceSearchConfluenceCloud = docLinks.links.workplaceSearch.confluenceCloud;
+ this.workplaceSearchConfluenceCloudConnectorPackage =
+ docLinks.links.workplaceSearch.confluenceCloudConnectorPackage;
this.workplaceSearchConfluenceServer = docLinks.links.workplaceSearch.confluenceServer;
+ this.workplaceSearchCustomConnectorPackage =
+ docLinks.links.workplaceSearch.customConnectorPackage;
this.workplaceSearchCustomSources = docLinks.links.workplaceSearch.customSources;
this.workplaceSearchCustomSourcePermissions =
docLinks.links.workplaceSearch.customSourcePermissions;
@@ -246,7 +262,9 @@ class DocLinks {
this.workplaceSearchIndexingSchedule = docLinks.links.workplaceSearch.indexingSchedule;
this.workplaceSearchJiraCloud = docLinks.links.workplaceSearch.jiraCloud;
this.workplaceSearchJiraServer = docLinks.links.workplaceSearch.jiraServer;
+ this.workplaceSearchNetworkDrive = docLinks.links.workplaceSearch.networkDrive;
this.workplaceSearchOneDrive = docLinks.links.workplaceSearch.oneDrive;
+ this.workplaceSearchOutlook = docLinks.links.workplaceSearch.outlook;
this.workplaceSearchPermissions = docLinks.links.workplaceSearch.permissions;
this.workplaceSearchSalesforce = docLinks.links.workplaceSearch.salesforce;
this.workplaceSearchSecurity = docLinks.links.workplaceSearch.security;
@@ -255,7 +273,9 @@ class DocLinks {
this.workplaceSearchSharePointServer = docLinks.links.workplaceSearch.sharePointServer;
this.workplaceSearchSlack = docLinks.links.workplaceSearch.slack;
this.workplaceSearchSynch = docLinks.links.workplaceSearch.synch;
+ this.workplaceSearchTeams = docLinks.links.workplaceSearch.teams;
this.workplaceSearchZendesk = docLinks.links.workplaceSearch.zendesk;
+ this.workplaceSearchZoom = docLinks.links.workplaceSearch.zoom;
}
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx
index db3da678e1e00..181cd8b7c9a73 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx
@@ -25,7 +25,7 @@ export const staticGenericExternalSourceData: SourceDataItem = {
isPublicKey: false,
hasOauthRedirect: false,
needsBaseUrl: false,
- documentationUrl: docLinks.workplaceSearchCustomSources, // TODO Update this when we have a doclink
+ documentationUrl: docLinks.workplaceSearchCustomConnectorPackage,
applicationPortalUrl: '',
},
objTypes: [],
@@ -107,7 +107,7 @@ export const staticSourceData: SourceDataItem[] = [
isPublicKey: false,
hasOauthRedirect: true,
needsBaseUrl: true,
- documentationUrl: docLinks.workplaceSearchConfluenceCloud, // TODO Update this when we have a doclink
+ documentationUrl: docLinks.workplaceSearchConfluenceCloudConnectorPackage,
applicationPortalUrl: 'https://developer.atlassian.com/console/myapps/',
},
objTypes: [
@@ -387,7 +387,7 @@ export const staticSourceData: SourceDataItem[] = [
isPublicKey: false,
hasOauthRedirect: false,
needsBaseUrl: false,
- documentationUrl: docLinks.workplaceSearchCustomSources, // TODO Update this when we have a doclink
+ documentationUrl: docLinks.workplaceSearchNetworkDrive,
applicationPortalUrl: '',
githubRepository: 'elastic/enterprise-search-network-drive-connector',
},
@@ -433,7 +433,7 @@ export const staticSourceData: SourceDataItem[] = [
isPublicKey: false,
hasOauthRedirect: false,
needsBaseUrl: false,
- documentationUrl: docLinks.workplaceSearchCustomSources, // TODO Update this when we have a doclink
+ documentationUrl: docLinks.workplaceSearchOutlook,
applicationPortalUrl: '',
githubRepository: 'elastic/enterprise-search-outlook-connector',
},
@@ -649,7 +649,7 @@ export const staticSourceData: SourceDataItem[] = [
isPublicKey: false,
hasOauthRedirect: false,
needsBaseUrl: false,
- documentationUrl: docLinks.workplaceSearchCustomSources, // TODO Update this when we have a doclink
+ documentationUrl: docLinks.workplaceSearchTeams,
applicationPortalUrl: '',
githubRepository: 'elastic/enterprise-search-teams-connector',
},
@@ -691,7 +691,7 @@ export const staticSourceData: SourceDataItem[] = [
isPublicKey: false,
hasOauthRedirect: false,
needsBaseUrl: false,
- documentationUrl: docLinks.workplaceSearchCustomSources, // TODO Update this when we have a doclink
+ documentationUrl: docLinks.workplaceSearchZoom,
applicationPortalUrl: '',
githubRepository: 'elastic/enterprise-search-zoom-connector',
},
From 065ea3e772f50cd0e5357ba98ba5bb04ccd4323f Mon Sep 17 00:00:00 2001
From: Byron Hulcher
Date: Fri, 20 May 2022 13:12:49 -0400
Subject: [PATCH 24/39] [Workplace Search] Remove Custom API Source Integration
tile (#132538)
---
.../apis/custom_integration/integrations.ts | 2 +-
.../assets/source_icons/custom_api_source.svg | 1 -
.../enterprise_search/server/integrations.ts | 18 ------------------
.../translations/translations/fr-FR.json | 2 --
.../translations/translations/ja-JP.json | 2 --
.../translations/translations/zh-CN.json | 2 --
6 files changed, 1 insertion(+), 26 deletions(-)
delete mode 100644 x-pack/plugins/enterprise_search/public/assets/source_icons/custom_api_source.svg
diff --git a/test/api_integration/apis/custom_integration/integrations.ts b/test/api_integration/apis/custom_integration/integrations.ts
index c1b6518f6684a..c4fda918328f8 100644
--- a/test/api_integration/apis/custom_integration/integrations.ts
+++ b/test/api_integration/apis/custom_integration/integrations.ts
@@ -22,7 +22,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.body).to.be.an('array');
- expect(resp.body.length).to.be(43);
+ expect(resp.body.length).to.be(42);
// Test for sample data card
expect(resp.body.findIndex((c: { id: string }) => c.id === 'sample_data_all')).to.be.above(
diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/custom_api_source.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/custom_api_source.svg
deleted file mode 100644
index cc07fbbc50877..0000000000000
--- a/x-pack/plugins/enterprise_search/public/assets/source_icons/custom_api_source.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/x-pack/plugins/enterprise_search/server/integrations.ts b/x-pack/plugins/enterprise_search/server/integrations.ts
index d2d3b5d4d6829..140e36ba15555 100644
--- a/x-pack/plugins/enterprise_search/server/integrations.ts
+++ b/x-pack/plugins/enterprise_search/server/integrations.ts
@@ -338,24 +338,6 @@ const workplaceSearchIntegrations: WorkplaceSearchIntegration[] = [
categories: ['enterprise_search', 'communications', 'productivity'],
uiInternalPath: '/app/enterprise_search/workplace_search/sources/add/zoom',
},
- {
- id: 'custom_api_source',
- title: i18n.translate(
- 'xpack.enterpriseSearch.workplaceSearch.integrations.customApiSourceName',
- {
- defaultMessage: 'Custom API Source',
- }
- ),
- description: i18n.translate(
- 'xpack.enterpriseSearch.workplaceSearch.integrations.customApiSourceDescription',
- {
- defaultMessage:
- 'Search over anything by building your own integration with Workplace Search.',
- }
- ),
- categories: ['custom'],
- uiInternalPath: '/app/enterprise_search/workplace_search/sources/add/custom',
- },
];
export const registerEnterpriseSearchIntegrations = (
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index 85ea8a0ffc348..b62a957cfa927 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -11801,8 +11801,6 @@
"xpack.enterpriseSearch.workplaceSearch.integrations.confluenceCloudName": "Cloud Confluence",
"xpack.enterpriseSearch.workplaceSearch.integrations.confluenceServerDescription": "Effectuez des recherches sur le contenu de votre organisation sur le serveur Confluence avec Workplace Search.",
"xpack.enterpriseSearch.workplaceSearch.integrations.confluenceServerName": "Serveur Confluence",
- "xpack.enterpriseSearch.workplaceSearch.integrations.customApiSourceDescription": "Effectuez n'importe quelle recherche en créant votre propre intégration avec Workplace Search.",
- "xpack.enterpriseSearch.workplaceSearch.integrations.customApiSourceName": "Source d'API personnalisée",
"xpack.enterpriseSearch.workplaceSearch.integrations.dropboxDescription": "Effectuez des recherches dans vos fichiers et dossiers stockés sur Dropbox avec Workplace Search.",
"xpack.enterpriseSearch.workplaceSearch.integrations.dropboxName": "Dropbox",
"xpack.enterpriseSearch.workplaceSearch.integrations.githubDescription": "Effectuez des recherches sur vos projets et référentiels sur GitHub avec Workplace Search.",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index cf84dbd2d6305..fe7056a5e3ec1 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -11900,8 +11900,6 @@
"xpack.enterpriseSearch.workplaceSearch.integrations.confluenceCloudName": "Confluence Cloud",
"xpack.enterpriseSearch.workplaceSearch.integrations.confluenceServerDescription": "Workplace Searchを使用して、Confluence Serverの組織コンテンツを検索します。",
"xpack.enterpriseSearch.workplaceSearch.integrations.confluenceServerName": "Confluence Server",
- "xpack.enterpriseSearch.workplaceSearch.integrations.customApiSourceDescription": "Workplace Searchを使用して、独自の統合を構築し、項目を検索します。",
- "xpack.enterpriseSearch.workplaceSearch.integrations.customApiSourceName": "カスタムAPIソース",
"xpack.enterpriseSearch.workplaceSearch.integrations.dropboxDescription": "Workplace Searchを使用して、Dropboxに保存されたファイルとフォルダーを検索します。",
"xpack.enterpriseSearch.workplaceSearch.integrations.dropboxName": "Dropbox",
"xpack.enterpriseSearch.workplaceSearch.integrations.githubDescription": "Workplace Searchを使用して、GitHubのプロジェクトとリポジトリを検索します。",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index b15cacd8dc8ab..990a113fcd9d6 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -11922,8 +11922,6 @@
"xpack.enterpriseSearch.workplaceSearch.integrations.confluenceCloudName": "Confluence Cloud",
"xpack.enterpriseSearch.workplaceSearch.integrations.confluenceServerDescription": "通过 Workplace Search 搜索 Confluence Server 上的组织内容。",
"xpack.enterpriseSearch.workplaceSearch.integrations.confluenceServerName": "Confluence Server",
- "xpack.enterpriseSearch.workplaceSearch.integrations.customApiSourceDescription": "通过使用 Workplace Search 构建自己的集成来搜索任何内容。",
- "xpack.enterpriseSearch.workplaceSearch.integrations.customApiSourceName": "定制 API 源",
"xpack.enterpriseSearch.workplaceSearch.integrations.dropboxDescription": "通过 Workplace Search 搜索存储在 Dropbox 上的文件和文件夹。",
"xpack.enterpriseSearch.workplaceSearch.integrations.dropboxName": "Dropbox",
"xpack.enterpriseSearch.workplaceSearch.integrations.githubDescription": "通过 Workplace Search 搜索 GitHub 上的项目和存储库。",
From ecca23166e0a619c4a529d463aefecf31da39830 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Felix=20St=C3=BCrmer?=
Date: Fri, 20 May 2022 19:37:03 +0200
Subject: [PATCH 25/39] [Stack Monitoring] Convert setup routes to TypeScript
(#131265)
---
x-pack/plugins/infra/server/mocks.ts | 34 ++++++
.../log_views/log_views_service.mock.ts | 6 +-
.../http_api/cluster/index.ts} | 5 +-
.../common/http_api/cluster/post_cluster.ts | 29 +++++
.../common/http_api/cluster/post_clusters.ts | 20 ++++
.../monitoring/common/http_api/setup/index.ts | 10 ++
.../setup/post_cluster_setup_status.ts | 44 +++++++
.../setup/post_disable_internal_collection.ts | 14 +++
.../http_api/setup/post_node_setup_status.ts | 43 +++++++
.../http_api/shared/literal_value.test.ts | 30 +++++
.../shared/query_string_boolean.test.ts | 23 ++++
.../plugins/monitoring/server/debug_logger.ts | 19 +--
.../lib/cluster/flag_supported_clusters.ts | 18 ++-
.../server/lib/cluster/get_index_patterns.ts | 6 +-
.../elasticsearch/verify_monitoring_auth.ts | 4 +-
....test.js => get_collection_status.test.ts} | 108 ++++++++++++------
.../setup/collection/get_collection_status.ts | 23 ++--
x-pack/plugins/monitoring/server/mocks.ts | 25 ++++
.../server/routes/api/v1/alerts/enable.ts | 8 +-
.../server/routes/api/v1/alerts/index.ts | 10 +-
.../server/routes/api/v1/alerts/status.ts | 7 +-
.../server/routes/api/v1/apm/index.ts | 13 ++-
.../server/routes/api/v1/beats/index.ts | 13 ++-
.../api/v1/check_access/check_access.ts | 9 +-
.../routes/api/v1/check_access/index.ts | 7 +-
.../server/routes/api/v1/cluster/cluster.ts | 46 ++++----
.../server/routes/api/v1/cluster/clusters.ts | 40 +++----
.../server/routes/api/v1/cluster/index.ts | 10 +-
.../routes/api/v1/elasticsearch/index.ts | 28 +++--
.../check/internal_monitoring.ts | 4 +-
.../api/v1/elasticsearch_settings/index.ts | 22 +++-
.../monitoring/server/routes/api/v1/index.ts | 16 +++
.../server/routes/api/v1/logstash/index.ts | 25 ++--
.../api/v1/setup/cluster_setup_status.js | 72 ------------
.../api/v1/setup/cluster_setup_status.ts | 62 ++++++++++
...able_elasticsearch_internal_collection.ts} | 18 ++-
.../server/routes/api/v1/setup/index.ts | 17 +++
.../routes/api/v1/setup/node_setup_status.js | 74 ------------
.../routes/api/v1/setup/node_setup_status.ts | 64 +++++++++++
.../monitoring/server/routes/api/v1/ui.js | 42 -------
.../monitoring/server/routes/api/v1/ui.ts | 14 +++
.../plugins/monitoring/server/routes/index.ts | 35 ++++--
x-pack/plugins/monitoring/server/types.ts | 3 +-
43 files changed, 753 insertions(+), 367 deletions(-)
create mode 100644 x-pack/plugins/infra/server/mocks.ts
rename x-pack/plugins/monitoring/{server/routes/api/v1/setup/index.js => common/http_api/cluster/index.ts} (52%)
create mode 100644 x-pack/plugins/monitoring/common/http_api/cluster/post_cluster.ts
create mode 100644 x-pack/plugins/monitoring/common/http_api/cluster/post_clusters.ts
create mode 100644 x-pack/plugins/monitoring/common/http_api/setup/index.ts
create mode 100644 x-pack/plugins/monitoring/common/http_api/setup/post_cluster_setup_status.ts
create mode 100644 x-pack/plugins/monitoring/common/http_api/setup/post_disable_internal_collection.ts
create mode 100644 x-pack/plugins/monitoring/common/http_api/setup/post_node_setup_status.ts
create mode 100644 x-pack/plugins/monitoring/common/http_api/shared/literal_value.test.ts
create mode 100644 x-pack/plugins/monitoring/common/http_api/shared/query_string_boolean.test.ts
rename x-pack/plugins/monitoring/server/lib/setup/collection/{get_collection_status.test.js => get_collection_status.test.ts} (79%)
create mode 100644 x-pack/plugins/monitoring/server/mocks.ts
create mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/index.ts
delete mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/setup/cluster_setup_status.js
create mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/setup/cluster_setup_status.ts
rename x-pack/plugins/monitoring/server/routes/api/v1/setup/{disable_elasticsearch_internal_collection.js => disable_elasticsearch_internal_collection.ts} (74%)
create mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/setup/index.ts
delete mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/setup/node_setup_status.js
create mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/setup/node_setup_status.ts
delete mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/ui.js
create mode 100644 x-pack/plugins/monitoring/server/routes/api/v1/ui.ts
diff --git a/x-pack/plugins/infra/server/mocks.ts b/x-pack/plugins/infra/server/mocks.ts
new file mode 100644
index 0000000000000..5b587a1fe80d5
--- /dev/null
+++ b/x-pack/plugins/infra/server/mocks.ts
@@ -0,0 +1,34 @@
+/*
+ * 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 {
+ createLogViewsServiceSetupMock,
+ createLogViewsServiceStartMock,
+} from './services/log_views/log_views_service.mock';
+import { InfraPluginSetup, InfraPluginStart } from './types';
+
+const createInfraSetupMock = () => {
+ const infraSetupMock: jest.Mocked = {
+ defineInternalSourceConfiguration: jest.fn(),
+ logViews: createLogViewsServiceSetupMock(),
+ };
+
+ return infraSetupMock;
+};
+
+const createInfraStartMock = () => {
+ const infraStartMock: jest.Mocked = {
+ getMetricIndices: jest.fn(),
+ logViews: createLogViewsServiceStartMock(),
+ };
+ return infraStartMock;
+};
+
+export const infraPluginMock = {
+ createSetupContract: createInfraSetupMock,
+ createStartContract: createInfraStartMock,
+};
diff --git a/x-pack/plugins/infra/server/services/log_views/log_views_service.mock.ts b/x-pack/plugins/infra/server/services/log_views/log_views_service.mock.ts
index becd5a015b2ec..e472e30fae2b4 100644
--- a/x-pack/plugins/infra/server/services/log_views/log_views_service.mock.ts
+++ b/x-pack/plugins/infra/server/services/log_views/log_views_service.mock.ts
@@ -6,7 +6,11 @@
*/
import { createLogViewsClientMock } from './log_views_client.mock';
-import { LogViewsServiceStart } from './types';
+import { LogViewsServiceSetup, LogViewsServiceStart } from './types';
+
+export const createLogViewsServiceSetupMock = (): jest.Mocked => ({
+ defineInternalLogView: jest.fn(),
+});
export const createLogViewsServiceStartMock = (): jest.Mocked => ({
getClient: jest.fn((_savedObjectsClient: any, _elasticsearchClient: any) =>
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/setup/index.js b/x-pack/plugins/monitoring/common/http_api/cluster/index.ts
similarity index 52%
rename from x-pack/plugins/monitoring/server/routes/api/v1/setup/index.js
rename to x-pack/plugins/monitoring/common/http_api/cluster/index.ts
index f450fc906d076..af53ade67f610 100644
--- a/x-pack/plugins/monitoring/server/routes/api/v1/setup/index.js
+++ b/x-pack/plugins/monitoring/common/http_api/cluster/index.ts
@@ -5,6 +5,5 @@
* 2.0.
*/
-export { clusterSetupStatusRoute } from './cluster_setup_status';
-export { nodeSetupStatusRoute } from './node_setup_status';
-export { disableElasticsearchInternalCollectionRoute } from './disable_elasticsearch_internal_collection';
+export * from './post_cluster';
+export * from './post_clusters';
diff --git a/x-pack/plugins/monitoring/common/http_api/cluster/post_cluster.ts b/x-pack/plugins/monitoring/common/http_api/cluster/post_cluster.ts
new file mode 100644
index 0000000000000..faa26989fec37
--- /dev/null
+++ b/x-pack/plugins/monitoring/common/http_api/cluster/post_cluster.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import * as rt from 'io-ts';
+import { ccsRT, clusterUuidRT, timeRangeRT } from '../shared';
+
+export const postClusterRequestParamsRT = rt.type({
+ clusterUuid: clusterUuidRT,
+});
+
+export const postClusterRequestPayloadRT = rt.intersection([
+ rt.partial({
+ ccs: ccsRT,
+ }),
+ rt.type({
+ timeRange: timeRangeRT,
+ codePaths: rt.array(rt.string),
+ }),
+]);
+
+export type PostClusterRequestPayload = rt.TypeOf;
+
+export const postClusterResponsePayloadRT = rt.type({
+ // TODO: add payload entries
+});
diff --git a/x-pack/plugins/monitoring/common/http_api/cluster/post_clusters.ts b/x-pack/plugins/monitoring/common/http_api/cluster/post_clusters.ts
new file mode 100644
index 0000000000000..ad3214c354bc5
--- /dev/null
+++ b/x-pack/plugins/monitoring/common/http_api/cluster/post_clusters.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 rt from 'io-ts';
+import { timeRangeRT } from '../shared';
+
+export const postClustersRequestPayloadRT = rt.type({
+ timeRange: timeRangeRT,
+ codePaths: rt.array(rt.string),
+});
+
+export type PostClustersRequestPayload = rt.TypeOf;
+
+export const postClustersResponsePayloadRT = rt.type({
+ // TODO: add payload entries
+});
diff --git a/x-pack/plugins/monitoring/common/http_api/setup/index.ts b/x-pack/plugins/monitoring/common/http_api/setup/index.ts
new file mode 100644
index 0000000000000..33cce5833c3c5
--- /dev/null
+++ b/x-pack/plugins/monitoring/common/http_api/setup/index.ts
@@ -0,0 +1,10 @@
+/*
+ * 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 * from './post_cluster_setup_status';
+export * from './post_node_setup_status';
+export * from './post_disable_internal_collection';
diff --git a/x-pack/plugins/monitoring/common/http_api/setup/post_cluster_setup_status.ts b/x-pack/plugins/monitoring/common/http_api/setup/post_cluster_setup_status.ts
new file mode 100644
index 0000000000000..2c4f1293fb89e
--- /dev/null
+++ b/x-pack/plugins/monitoring/common/http_api/setup/post_cluster_setup_status.ts
@@ -0,0 +1,44 @@
+/*
+ * 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 rt from 'io-ts';
+import {
+ booleanFromStringRT,
+ ccsRT,
+ clusterUuidRT,
+ createLiteralValueFromUndefinedRT,
+ timeRangeRT,
+} from '../shared';
+
+export const postClusterSetupStatusRequestParamsRT = rt.partial({
+ clusterUuid: clusterUuidRT,
+});
+
+export const postClusterSetupStatusRequestQueryRT = rt.partial({
+ // This flag is not intended to be used in production. It was introduced
+ // as a way to ensure consistent API testing - the typical data source
+ // for API tests are archived data, where the cluster configuration and data
+ // are consistent from environment to environment. However, this endpoint
+ // also attempts to retrieve data from the running stack products (ES and Kibana)
+ // which will vary from environment to environment making it difficult
+ // to write tests against. Therefore, this flag exists and should only be used
+ // in our testing environment.
+ skipLiveData: rt.union([booleanFromStringRT, createLiteralValueFromUndefinedRT(false)]),
+});
+
+export const postClusterSetupStatusRequestPayloadRT = rt.partial({
+ ccs: ccsRT,
+ timeRange: timeRangeRT,
+});
+
+export type PostClusterSetupStatusRequestPayload = rt.TypeOf<
+ typeof postClusterSetupStatusRequestPayloadRT
+>;
+
+export const postClusterSetupStatusResponsePayloadRT = rt.type({
+ // TODO: add payload entries
+});
diff --git a/x-pack/plugins/monitoring/common/http_api/setup/post_disable_internal_collection.ts b/x-pack/plugins/monitoring/common/http_api/setup/post_disable_internal_collection.ts
new file mode 100644
index 0000000000000..d44794d7e1829
--- /dev/null
+++ b/x-pack/plugins/monitoring/common/http_api/setup/post_disable_internal_collection.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import * as rt from 'io-ts';
+import { clusterUuidRT } from '../shared';
+
+export const postDisableInternalCollectionRequestParamsRT = rt.partial({
+ // the cluster uuid seems to be required but never used
+ clusterUuid: clusterUuidRT,
+});
diff --git a/x-pack/plugins/monitoring/common/http_api/setup/post_node_setup_status.ts b/x-pack/plugins/monitoring/common/http_api/setup/post_node_setup_status.ts
new file mode 100644
index 0000000000000..1d51d36ae4477
--- /dev/null
+++ b/x-pack/plugins/monitoring/common/http_api/setup/post_node_setup_status.ts
@@ -0,0 +1,43 @@
+/*
+ * 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 rt from 'io-ts';
+import {
+ booleanFromStringRT,
+ ccsRT,
+ createLiteralValueFromUndefinedRT,
+ timeRangeRT,
+} from '../shared';
+
+export const postNodeSetupStatusRequestParamsRT = rt.type({
+ nodeUuid: rt.string,
+});
+
+export const postNodeSetupStatusRequestQueryRT = rt.partial({
+ // This flag is not intended to be used in production. It was introduced
+ // as a way to ensure consistent API testing - the typical data source
+ // for API tests are archived data, where the cluster configuration and data
+ // are consistent from environment to environment. However, this endpoint
+ // also attempts to retrieve data from the running stack products (ES and Kibana)
+ // which will vary from environment to environment making it difficult
+ // to write tests against. Therefore, this flag exists and should only be used
+ // in our testing environment.
+ skipLiveData: rt.union([booleanFromStringRT, createLiteralValueFromUndefinedRT(false)]),
+});
+
+export const postNodeSetupStatusRequestPayloadRT = rt.partial({
+ ccs: ccsRT,
+ timeRange: timeRangeRT,
+});
+
+export type PostNodeSetupStatusRequestPayload = rt.TypeOf<
+ typeof postNodeSetupStatusRequestPayloadRT
+>;
+
+export const postNodeSetupStatusResponsePayloadRT = rt.type({
+ // TODO: add payload entries
+});
diff --git a/x-pack/plugins/monitoring/common/http_api/shared/literal_value.test.ts b/x-pack/plugins/monitoring/common/http_api/shared/literal_value.test.ts
new file mode 100644
index 0000000000000..3d70e86620602
--- /dev/null
+++ b/x-pack/plugins/monitoring/common/http_api/shared/literal_value.test.ts
@@ -0,0 +1,30 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { either } from 'fp-ts';
+import * as rt from 'io-ts';
+import { createLiteralValueFromUndefinedRT } from './literal_value';
+
+describe('LiteralValueFromUndefined runtime type', () => {
+ it('decodes undefined to a given literal value', () => {
+ expect(createLiteralValueFromUndefinedRT('SOME_VALUE').decode(undefined)).toEqual(
+ either.right('SOME_VALUE')
+ );
+ });
+
+ it('can be used to define default values when decoding', () => {
+ expect(
+ rt.union([rt.boolean, createLiteralValueFromUndefinedRT(true)]).decode(undefined)
+ ).toEqual(either.right(true));
+ });
+
+ it('rejects other values', () => {
+ expect(
+ either.isLeft(createLiteralValueFromUndefinedRT('SOME_VALUE').decode('DEFINED'))
+ ).toBeTruthy();
+ });
+});
diff --git a/x-pack/plugins/monitoring/common/http_api/shared/query_string_boolean.test.ts b/x-pack/plugins/monitoring/common/http_api/shared/query_string_boolean.test.ts
new file mode 100644
index 0000000000000..1801c6746feb2
--- /dev/null
+++ b/x-pack/plugins/monitoring/common/http_api/shared/query_string_boolean.test.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { either } from 'fp-ts';
+import { booleanFromStringRT } from './query_string_boolean';
+
+describe('BooleanFromString runtime type', () => {
+ it('decodes string "true" to a boolean', () => {
+ expect(booleanFromStringRT.decode('true')).toEqual(either.right(true));
+ });
+
+ it('decodes string "false" to a boolean', () => {
+ expect(booleanFromStringRT.decode('false')).toEqual(either.right(false));
+ });
+
+ it('rejects other strings', () => {
+ expect(either.isLeft(booleanFromStringRT.decode('maybe'))).toBeTruthy();
+ });
+});
diff --git a/x-pack/plugins/monitoring/server/debug_logger.ts b/x-pack/plugins/monitoring/server/debug_logger.ts
index 0add1f12f0304..cce00f834cbb2 100644
--- a/x-pack/plugins/monitoring/server/debug_logger.ts
+++ b/x-pack/plugins/monitoring/server/debug_logger.ts
@@ -4,18 +4,19 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+import { RouteMethod } from '@kbn/core/server';
import fs from 'fs';
import { MonitoringConfig } from './config';
-import { RouteDependencies } from './types';
+import { LegacyRequest, MonitoringCore, MonitoringRouteConfig, RouteDependencies } from './types';
export function decorateDebugServer(
- _server: any,
+ server: MonitoringCore,
config: MonitoringConfig,
logger: RouteDependencies['logger']
-) {
+): MonitoringCore {
// bail if the proper config value is not set (extra protection)
if (!config.ui.debug_mode) {
- return _server;
+ return server;
}
// create a debug logger that will either write to file (if debug_log_path exists) or log out via logger
@@ -23,14 +24,16 @@ export function decorateDebugServer(
return {
// maintain the rest of _server untouched
- ..._server,
+ ...server,
// TODO: replace any
- route: (options: any) => {
+ route: (
+ options: MonitoringRouteConfig
+ ) => {
const apiPath = options.path;
- return _server.route({
+ return server.route({
...options,
// TODO: replace any
- handler: async (req: any) => {
+ handler: async (req: LegacyRequest): Promise => {
const { elasticsearch: cached } = req.server.plugins;
const apiRequestHeaders = req.headers;
req.server.plugins.elasticsearch = {
diff --git a/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.ts b/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.ts
index 80d17a8ad0627..f93c3f8ad7590 100644
--- a/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.ts
+++ b/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.ts
@@ -6,13 +6,18 @@
*/
import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../common/constants';
+import { TimeRange } from '../../../common/http_api/shared';
import { ElasticsearchResponse } from '../../../common/types/es';
-import { LegacyRequest, Cluster } from '../../types';
-import { getNewIndexPatterns } from './get_index_patterns';
import { Globals } from '../../static_globals';
+import { Cluster, LegacyRequest } from '../../types';
+import { getNewIndexPatterns } from './get_index_patterns';
+
+export interface FindSupportClusterRequestPayload {
+ timeRange: TimeRange;
+}
async function findSupportedBasicLicenseCluster(
- req: LegacyRequest,
+ req: LegacyRequest,
clusters: Cluster[],
ccs: string,
kibanaUuid: string,
@@ -53,7 +58,7 @@ async function findSupportedBasicLicenseCluster(
},
},
{ term: { 'kibana_stats.kibana.uuid': kibanaUuid } },
- { range: { timestamp: { gte, lte, format: 'strict_date_optional_time' } } },
+ { range: { timestamp: { gte, lte, format: 'epoch_millis' } } },
],
},
},
@@ -86,7 +91,10 @@ async function findSupportedBasicLicenseCluster(
* Non-Basic license clusters and any cluster in a single-cluster environment
* are also flagged as supported in this method.
*/
-export function flagSupportedClusters(req: LegacyRequest, ccs: string) {
+export function flagSupportedClusters(
+ req: LegacyRequest,
+ ccs: string
+) {
const serverLog = (message: string) => req.getLogger('supported-clusters').debug(message);
const flagAllSupported = (clusters: Cluster[]) => {
clusters.forEach((cluster) => {
diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts b/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts
index 7d470857dfe5a..2ebf4fe6b480e 100644
--- a/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts
+++ b/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
-import { LegacyServer } from '../../types';
import { prefixIndexPatternWithCcs } from '../../../common/ccs_utils';
import {
INDEX_PATTERN_ELASTICSEARCH,
@@ -20,14 +19,13 @@ import {
INDEX_PATTERN_ENTERPRISE_SEARCH,
CCS_REMOTE_PATTERN,
} from '../../../common/constants';
-import { MonitoringConfig } from '../..';
+import { MonitoringConfig } from '../../config';
export function getIndexPatterns(
- server: LegacyServer,
+ config: MonitoringConfig,
additionalPatterns: Record = {},
ccs: string = CCS_REMOTE_PATTERN
) {
- const config = server.config;
const esIndexPattern = prefixIndexPatternWithCcs(config, INDEX_PATTERN_ELASTICSEARCH, ccs);
const kbnIndexPattern = prefixIndexPatternWithCcs(config, INDEX_PATTERN_KIBANA, ccs);
const lsIndexPattern = prefixIndexPatternWithCcs(config, INDEX_PATTERN_LOGSTASH, ccs);
diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_monitoring_auth.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_monitoring_auth.ts
index 3bd9f6d2265dc..a5ee876012c1d 100644
--- a/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_monitoring_auth.ts
+++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_monitoring_auth.ts
@@ -19,7 +19,7 @@ import { LegacyRequest } from '../../types';
*/
// TODO: replace LegacyRequest with current request object + plugin retrieval
-export async function verifyMonitoringAuth(req: LegacyRequest) {
+export async function verifyMonitoringAuth(req: LegacyRequest) {
const xpackInfo = get(req.server.plugins.monitoring, 'info');
if (xpackInfo) {
@@ -42,7 +42,7 @@ export async function verifyMonitoringAuth(req: LegacyRequest) {
*/
// TODO: replace LegacyRequest with current request object + plugin retrieval
-async function verifyHasPrivileges(req: LegacyRequest) {
+async function verifyHasPrivileges(req: LegacyRequest): Promise {
const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring');
let response;
diff --git a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.test.js b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.test.ts
similarity index 79%
rename from x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.test.js
rename to x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.test.ts
index 214e8d5907443..ed92948be8e3b 100644
--- a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.test.js
+++ b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.test.ts
@@ -5,49 +5,50 @@
* 2.0.
*/
-import { getCollectionStatus } from '.';
+import { featuresPluginMock } from '@kbn/features-plugin/server/mocks';
+import { infraPluginMock } from '@kbn/infra-plugin/server/mocks';
+import { loggerMock } from '@kbn/logging-mocks';
+import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/server/mocks';
+import { configSchema, createConfig } from '../../../config';
+import { monitoringPluginMock } from '../../../mocks';
+import { LegacyRequest } from '../../../types';
import { getIndexPatterns } from '../../cluster/get_index_patterns';
+import { getCollectionStatus } from './get_collection_status';
const liveClusterUuid = 'a12';
const mockReq = (
- searchResult = {},
- securityEnabled = true,
- userHasPermissions = true,
- securityErrorMessage = null
-) => {
+ searchResult: object = {},
+ securityEnabled: boolean = true,
+ userHasPermissions: boolean = true,
+ securityErrorMessage: string | null = null
+): LegacyRequest => {
+ const usageCollectionSetup = usageCollectionPluginMock.createSetupContract();
+ const licenseService = monitoringPluginMock.createLicenseServiceMock();
+ licenseService.getSecurityFeature.mockReturnValue({
+ isAvailable: securityEnabled,
+ isEnabled: securityEnabled,
+ });
+ const logger = loggerMock.create();
+
return {
server: {
instanceUuid: 'kibana-1234',
newPlatform: {
setup: {
plugins: {
- usageCollection: {
- getCollectorByType: () => ({
- isReady: () => false,
- }),
- },
+ usageCollection: usageCollectionSetup,
+ features: featuresPluginMock.createSetup(),
+ infra: infraPluginMock.createSetupContract(),
},
},
},
- config: { ui: { ccs: { enabled: false } } },
- usage: {
- collectorSet: {
- getCollectorByType: () => ({
- isReady: () => false,
- }),
- },
- },
+ config: createConfig(configSchema.validate({ ui: { ccs: { enabled: false } } })),
+ log: logger,
+ route: jest.fn(),
plugins: {
monitoring: {
info: {
- getLicenseService: () => ({
- getSecurityFeature: () => {
- return {
- isAvailable: securityEnabled,
- isEnabled: securityEnabled,
- };
- },
- }),
+ getLicenseService: () => licenseService,
},
},
elasticsearch: {
@@ -86,6 +87,17 @@ const mockReq = (
},
},
},
+ logger,
+ getLogger: () => logger,
+ params: {},
+ payload: {},
+ query: {},
+ headers: {},
+ getKibanaStatsCollector: () => null,
+ getUiSettingsService: () => null,
+ getActionTypeRegistry: () => null,
+ getRulesClient: () => null,
+ getActionsClient: () => null,
};
};
@@ -124,7 +136,7 @@ describe('getCollectionStatus', () => {
},
});
- const result = await getCollectionStatus(req, getIndexPatterns(req.server));
+ const result = await getCollectionStatus(req, getIndexPatterns(req.server.config));
expect(result.kibana.totalUniqueInstanceCount).toBe(1);
expect(result.kibana.totalUniqueFullyMigratedCount).toBe(0);
@@ -173,7 +185,7 @@ describe('getCollectionStatus', () => {
},
});
- const result = await getCollectionStatus(req, getIndexPatterns(req.server));
+ const result = await getCollectionStatus(req, getIndexPatterns(req.server.config));
expect(result.kibana.totalUniqueInstanceCount).toBe(1);
expect(result.kibana.totalUniqueFullyMigratedCount).toBe(1);
@@ -229,7 +241,7 @@ describe('getCollectionStatus', () => {
},
});
- const result = await getCollectionStatus(req, getIndexPatterns(req.server));
+ const result = await getCollectionStatus(req, getIndexPatterns(req.server.config));
expect(result.kibana.totalUniqueInstanceCount).toBe(2);
expect(result.kibana.totalUniqueFullyMigratedCount).toBe(1);
@@ -251,7 +263,11 @@ describe('getCollectionStatus', () => {
it('should detect products based on other indices', async () => {
const req = mockReq({ hits: { total: { value: 1 } } });
- const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid);
+ const result = await getCollectionStatus(
+ req,
+ getIndexPatterns(req.server.config),
+ liveClusterUuid
+ );
expect(result.kibana.detected.doesExist).toBe(true);
expect(result.elasticsearch.detected.doesExist).toBe(true);
@@ -261,13 +277,21 @@ describe('getCollectionStatus', () => {
it('should work properly when security is disabled', async () => {
const req = mockReq({ hits: { total: { value: 1 } } }, false);
- const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid);
+ const result = await getCollectionStatus(
+ req,
+ getIndexPatterns(req.server.config),
+ liveClusterUuid
+ );
expect(result.kibana.detected.doesExist).toBe(true);
});
it('should work properly with an unknown security message', async () => {
const req = mockReq({ hits: { total: { value: 1 } } }, true, true, 'foobar');
- const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid);
+ const result = await getCollectionStatus(
+ req,
+ getIndexPatterns(req.server.config),
+ liveClusterUuid
+ );
expect(result._meta.hasPermissions).toBe(false);
});
@@ -278,7 +302,11 @@ describe('getCollectionStatus', () => {
true,
'no handler found for uri [/_security/user/_has_privileges] and method [POST]'
);
- const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid);
+ const result = await getCollectionStatus(
+ req,
+ getIndexPatterns(req.server.config),
+ liveClusterUuid
+ );
expect(result.kibana.detected.doesExist).toBe(true);
});
@@ -289,13 +317,21 @@ describe('getCollectionStatus', () => {
true,
'Invalid index name [_security]'
);
- const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid);
+ const result = await getCollectionStatus(
+ req,
+ getIndexPatterns(req.server.config),
+ liveClusterUuid
+ );
expect(result.kibana.detected.doesExist).toBe(true);
});
it('should not work if the user does not have the necessary permissions', async () => {
const req = mockReq({ hits: { total: { value: 1 } } }, true, false);
- const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid);
+ const result = await getCollectionStatus(
+ req,
+ getIndexPatterns(req.server.config),
+ liveClusterUuid
+ );
expect(result._meta.hasPermissions).toBe(false);
});
});
diff --git a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.ts b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.ts
index b06b74fd255f4..568b8bbaef567 100644
--- a/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.ts
+++ b/x-pack/plugins/monitoring/server/lib/setup/collection/get_collection_status.ts
@@ -5,17 +5,18 @@
* 2.0.
*/
-import { get, uniq } from 'lodash';
import { CollectorFetchContext, UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
+import { get, uniq } from 'lodash';
import {
- METRICBEAT_INDEX_NAME_UNIQUE_TOKEN,
- ELASTICSEARCH_SYSTEM_ID,
APM_SYSTEM_ID,
- KIBANA_SYSTEM_ID,
BEATS_SYSTEM_ID,
- LOGSTASH_SYSTEM_ID,
+ ELASTICSEARCH_SYSTEM_ID,
KIBANA_STATS_TYPE_MONITORING,
+ KIBANA_SYSTEM_ID,
+ LOGSTASH_SYSTEM_ID,
+ METRICBEAT_INDEX_NAME_UNIQUE_TOKEN,
} from '../../../../common/constants';
+import { TimeRange } from '../../../../common/http_api/shared';
import { LegacyRequest } from '../../../types';
import { getLivesNodes } from '../../elasticsearch/nodes/get_nodes/get_live_nodes';
@@ -31,7 +32,7 @@ interface Bucket {
const NUMBER_OF_SECONDS_AGO_TO_LOOK = 30;
const getRecentMonitoringDocuments = async (
- req: LegacyRequest,
+ req: LegacyRequest,
indexPatterns: Record,
clusterUuid?: string,
nodeUuid?: string,
@@ -300,7 +301,7 @@ function isBeatFromAPM(bucket: Bucket) {
return get(beatType, 'buckets[0].key') === 'apm-server';
}
-async function hasNecessaryPermissions(req: LegacyRequest) {
+async function hasNecessaryPermissions(req: LegacyRequest) {
const licenseService = await req.server.plugins.monitoring.info.getLicenseService();
const securityFeature = licenseService.getSecurityFeature();
if (!securityFeature.isAvailable || !securityFeature.isEnabled) {
@@ -366,7 +367,7 @@ async function getLiveKibanaInstance(usageCollection?: UsageCollectionSetup) {
);
}
-async function getLiveElasticsearchClusterUuid(req: LegacyRequest) {
+async function getLiveElasticsearchClusterUuid(req: LegacyRequest) {
const params = {
path: '/_cluster/state/cluster_uuid',
method: 'GET',
@@ -377,7 +378,9 @@ async function getLiveElasticsearchClusterUuid(req: LegacyRequest) {
return clusterUuid;
}
-async function getLiveElasticsearchCollectionEnabled(req: LegacyRequest) {
+async function getLiveElasticsearchCollectionEnabled(
+ req: LegacyRequest
+) {
const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin');
const response = await callWithRequest(req, 'transport.request', {
method: 'GET',
@@ -425,7 +428,7 @@ async function getLiveElasticsearchCollectionEnabled(req: LegacyRequest) {
* @param {*} skipLiveData Optional and will not make any live api calls if set to true
*/
export const getCollectionStatus = async (
- req: LegacyRequest,
+ req: LegacyRequest,
indexPatterns: Record,
clusterUuid?: string,
nodeUuid?: string,
diff --git a/x-pack/plugins/monitoring/server/mocks.ts b/x-pack/plugins/monitoring/server/mocks.ts
new file mode 100644
index 0000000000000..5adeae22acfc0
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/mocks.ts
@@ -0,0 +1,25 @@
+/*
+ * 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 { ILicense } from '@kbn/licensing-plugin/server';
+import { Subject } from 'rxjs';
+import { MonitoringLicenseService } from './types';
+
+const createLicenseServiceMock = (): jest.Mocked => ({
+ refresh: jest.fn(),
+ license$: new Subject(),
+ getMessage: jest.fn(),
+ getWatcherFeature: jest.fn(),
+ getMonitoringFeature: jest.fn(),
+ getSecurityFeature: jest.fn(),
+ stop: jest.fn(),
+});
+
+// this might be incomplete and is added to as needed
+export const monitoringPluginMock = {
+ createLicenseServiceMock,
+};
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts
index 9188215137565..b773e25b81152 100644
--- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts
+++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts
@@ -8,15 +8,15 @@
// @ts-ignore
import { ActionResult } from '@kbn/actions-plugin/common';
import { RuleTypeParams, SanitizedRule } from '@kbn/alerting-plugin/common';
-import { handleError } from '../../../../lib/errors';
-import { AlertsFactory } from '../../../../alerts';
-import { LegacyServer, RouteDependencies } from '../../../../types';
import { ALERT_ACTION_TYPE_LOG } from '../../../../../common/constants';
+import { AlertsFactory } from '../../../../alerts';
import { disableWatcherClusterAlerts } from '../../../../lib/alerts/disable_watcher_cluster_alerts';
+import { handleError } from '../../../../lib/errors';
+import { MonitoringCore, RouteDependencies } from '../../../../types';
const DEFAULT_SERVER_LOG_NAME = 'Monitoring: Write to Kibana log';
-export function enableAlertsRoute(server: LegacyServer, npRoute: RouteDependencies) {
+export function enableAlertsRoute(server: MonitoringCore, npRoute: RouteDependencies) {
npRoute.router.post(
{
path: '/api/monitoring/v1/alerts/enable',
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/index.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/index.ts
index 11782c73d9b55..c2511e1d24c0a 100644
--- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/index.ts
+++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/index.ts
@@ -5,5 +5,11 @@
* 2.0.
*/
-export { enableAlertsRoute } from './enable';
-export { alertStatusRoute } from './status';
+import { MonitoringCore, RouteDependencies } from '../../../../types';
+import { enableAlertsRoute } from './enable';
+import { alertStatusRoute } from './status';
+
+export function registerV1AlertRoutes(server: MonitoringCore, npRoute: RouteDependencies) {
+ alertStatusRoute(npRoute);
+ enableAlertsRoute(server, npRoute);
+}
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts
index a145d92921634..a9efc14c8c458 100644
--- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts
+++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts
@@ -6,13 +6,12 @@
*/
import { schema } from '@kbn/config-schema';
-// @ts-ignore
+import { CommonAlertFilter } from '../../../../../common/types/alerts';
+import { fetchStatus } from '../../../../lib/alerts/fetch_status';
import { handleError } from '../../../../lib/errors';
import { RouteDependencies } from '../../../../types';
-import { fetchStatus } from '../../../../lib/alerts/fetch_status';
-import { CommonAlertFilter } from '../../../../../common/types/alerts';
-export function alertStatusRoute(server: any, npRoute: RouteDependencies) {
+export function alertStatusRoute(npRoute: RouteDependencies) {
npRoute.router.post(
{
path: '/api/monitoring/v1/alert/{clusterUuid}/status',
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/apm/index.ts b/x-pack/plugins/monitoring/server/routes/api/v1/apm/index.ts
index 0fb4dd78c9be6..97d9a2f9789d7 100644
--- a/x-pack/plugins/monitoring/server/routes/api/v1/apm/index.ts
+++ b/x-pack/plugins/monitoring/server/routes/api/v1/apm/index.ts
@@ -5,6 +5,13 @@
* 2.0.
*/
-export { apmInstanceRoute } from './instance';
-export { apmInstancesRoute } from './instances';
-export { apmOverviewRoute } from './overview';
+import { MonitoringCore } from '../../../../types';
+import { apmInstanceRoute } from './instance';
+import { apmInstancesRoute } from './instances';
+import { apmOverviewRoute } from './overview';
+
+export function registerV1ApmRoutes(server: MonitoringCore) {
+ apmInstanceRoute(server);
+ apmInstancesRoute(server);
+ apmOverviewRoute(server);
+}
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/beats/index.ts b/x-pack/plugins/monitoring/server/routes/api/v1/beats/index.ts
index 57423052760bf..935ca35c3a384 100644
--- a/x-pack/plugins/monitoring/server/routes/api/v1/beats/index.ts
+++ b/x-pack/plugins/monitoring/server/routes/api/v1/beats/index.ts
@@ -5,6 +5,13 @@
* 2.0.
*/
-export { beatsOverviewRoute } from './overview';
-export { beatsListingRoute } from './beats';
-export { beatsDetailRoute } from './beat_detail';
+import { MonitoringCore } from '../../../../types';
+import { beatsListingRoute } from './beats';
+import { beatsDetailRoute } from './beat_detail';
+import { beatsOverviewRoute } from './overview';
+
+export function registerV1BeatsRoutes(server: MonitoringCore) {
+ beatsDetailRoute(server);
+ beatsListingRoute(server);
+ beatsOverviewRoute(server);
+}
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/check_access/check_access.ts b/x-pack/plugins/monitoring/server/routes/api/v1/check_access/check_access.ts
index 450872049a3de..2db7481882b89 100644
--- a/x-pack/plugins/monitoring/server/routes/api/v1/check_access/check_access.ts
+++ b/x-pack/plugins/monitoring/server/routes/api/v1/check_access/check_access.ts
@@ -7,18 +7,19 @@
import { verifyMonitoringAuth } from '../../../../lib/elasticsearch/verify_monitoring_auth';
import { handleError } from '../../../../lib/errors';
-import { LegacyRequest, LegacyServer } from '../../../../types';
+import { LegacyRequest, MonitoringCore } from '../../../../types';
/*
* API for checking read privilege on Monitoring Data
* Used for the "Access Denied" page as something to auto-retry with.
*/
-// TODO: Replace this LegacyServer call with the "new platform" core Kibana route method
-export function checkAccessRoute(server: LegacyServer) {
+// TODO: Replace this legacy route registration with the "new platform" core Kibana route method
+export function checkAccessRoute(server: MonitoringCore) {
server.route({
- method: 'GET',
+ method: 'get',
path: '/api/monitoring/v1/check_access',
+ validate: {},
handler: async (req: LegacyRequest) => {
const response: { has_access?: boolean } = {};
try {
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/check_access/index.ts b/x-pack/plugins/monitoring/server/routes/api/v1/check_access/index.ts
index 0fb8228f82442..5209ec8b92e9a 100644
--- a/x-pack/plugins/monitoring/server/routes/api/v1/check_access/index.ts
+++ b/x-pack/plugins/monitoring/server/routes/api/v1/check_access/index.ts
@@ -5,4 +5,9 @@
* 2.0.
*/
-export { checkAccessRoute } from './check_access';
+import { MonitoringCore } from '../../../../types';
+import { checkAccessRoute } from './check_access';
+
+export function registerV1CheckAccessRoutes(server: MonitoringCore) {
+ checkAccessRoute(server);
+}
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.ts b/x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.ts
index 30749f2e95c9f..6bd0a19d79c5f 100644
--- a/x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.ts
+++ b/x-pack/plugins/monitoring/server/routes/api/v1/cluster/cluster.ts
@@ -5,39 +5,36 @@
* 2.0.
*/
-import { schema } from '@kbn/config-schema';
+import {
+ postClusterRequestParamsRT,
+ postClusterRequestPayloadRT,
+ postClusterResponsePayloadRT,
+} from '../../../../../common/http_api/cluster';
+import { createValidationFunction } from '../../../../lib/create_route_validation_function';
import { getClustersFromRequest } from '../../../../lib/cluster/get_clusters_from_request';
-// @ts-ignore
-import { handleError } from '../../../../lib/errors';
import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns';
-import { LegacyRequest, LegacyServer } from '../../../../types';
+import { handleError } from '../../../../lib/errors';
+import { MonitoringCore } from '../../../../types';
-export function clusterRoute(server: LegacyServer) {
+export function clusterRoute(server: MonitoringCore) {
/*
* Cluster Overview
*/
+
+ const validateParams = createValidationFunction(postClusterRequestParamsRT);
+ const validateBody = createValidationFunction(postClusterRequestPayloadRT);
+
server.route({
- method: 'POST',
+ method: 'post',
path: '/api/monitoring/v1/clusters/{clusterUuid}',
- config: {
- validate: {
- params: schema.object({
- clusterUuid: schema.string(),
- }),
- body: schema.object({
- ccs: schema.maybe(schema.string()),
- timeRange: schema.object({
- min: schema.string(),
- max: schema.string(),
- }),
- codePaths: schema.arrayOf(schema.string()),
- }),
- },
+ validate: {
+ params: validateParams,
+ body: validateBody,
},
- handler: async (req: LegacyRequest) => {
+ handler: async (req) => {
const config = server.config;
- const indexPatterns = getIndexPatterns(server, {
+ const indexPatterns = getIndexPatterns(config, {
filebeatIndexPattern: config.ui.logs.index,
});
const options = {
@@ -47,13 +44,12 @@ export function clusterRoute(server: LegacyServer) {
codePaths: req.payload.codePaths,
};
- let clusters = [];
try {
- clusters = await getClustersFromRequest(req, indexPatterns, options);
+ const clusters = await getClustersFromRequest(req, indexPatterns, options);
+ return postClusterResponsePayloadRT.encode(clusters);
} catch (err) {
throw handleError(err, req);
}
- return clusters;
},
});
}
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/cluster/clusters.ts b/x-pack/plugins/monitoring/server/routes/api/v1/cluster/clusters.ts
index 81acd0e53f319..9591dda205487 100644
--- a/x-pack/plugins/monitoring/server/routes/api/v1/cluster/clusters.ts
+++ b/x-pack/plugins/monitoring/server/routes/api/v1/cluster/clusters.ts
@@ -5,36 +5,33 @@
* 2.0.
*/
-import { schema } from '@kbn/config-schema';
-import { LegacyRequest, LegacyServer } from '../../../../types';
+import {
+ postClustersRequestPayloadRT,
+ postClustersResponsePayloadRT,
+} from '../../../../../common/http_api/cluster';
import { getClustersFromRequest } from '../../../../lib/cluster/get_clusters_from_request';
+import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns';
+import { createValidationFunction } from '../../../../lib/create_route_validation_function';
import { verifyMonitoringAuth } from '../../../../lib/elasticsearch/verify_monitoring_auth';
import { handleError } from '../../../../lib/errors';
-import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns';
+import { MonitoringCore } from '../../../../types';
-export function clustersRoute(server: LegacyServer) {
+export function clustersRoute(server: MonitoringCore) {
/*
* Monitoring Home
* Route Init (for checking license and compatibility for multi-cluster monitoring
*/
+ const validateBody = createValidationFunction(postClustersRequestPayloadRT);
+
// TODO switch from the LegacyServer route() method to the "new platform" route methods
server.route({
- method: 'POST',
+ method: 'post',
path: '/api/monitoring/v1/clusters',
- config: {
- validate: {
- body: schema.object({
- timeRange: schema.object({
- min: schema.string(),
- max: schema.string(),
- }),
- codePaths: schema.arrayOf(schema.string()),
- }),
- },
+ validate: {
+ body: validateBody,
},
- handler: async (req: LegacyRequest) => {
- let clusters = [];
+ handler: async (req) => {
const config = server.config;
// NOTE using try/catch because checkMonitoringAuth is expected to throw
@@ -42,17 +39,16 @@ export function clustersRoute(server: LegacyServer) {
// the monitoring data. `try/catch` makes it a little more explicit.
try {
await verifyMonitoringAuth(req);
- const indexPatterns = getIndexPatterns(server, {
+ const indexPatterns = getIndexPatterns(config, {
filebeatIndexPattern: config.ui.logs.index,
});
- clusters = await getClustersFromRequest(req, indexPatterns, {
- codePaths: req.payload.codePaths as string[], // TODO remove this cast when we can properly type req by using the right route handler
+ const clusters = await getClustersFromRequest(req, indexPatterns, {
+ codePaths: req.payload.codePaths,
});
+ return postClustersResponsePayloadRT.encode(clusters);
} catch (err) {
throw handleError(err, req);
}
-
- return clusters;
},
});
}
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/cluster/index.ts b/x-pack/plugins/monitoring/server/routes/api/v1/cluster/index.ts
index 769f315480d9c..9534398db52c1 100644
--- a/x-pack/plugins/monitoring/server/routes/api/v1/cluster/index.ts
+++ b/x-pack/plugins/monitoring/server/routes/api/v1/cluster/index.ts
@@ -5,5 +5,11 @@
* 2.0.
*/
-export { clusterRoute } from './cluster';
-export { clustersRoute } from './clusters';
+import { clusterRoute } from './cluster';
+import { clustersRoute } from './clusters';
+import { MonitoringCore } from '../../../../types';
+
+export function registerV1ClusterRoutes(server: MonitoringCore) {
+ clusterRoute(server);
+ clustersRoute(server);
+}
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index.ts
index b2d432a5e35b5..e706dc61c0a41 100644
--- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index.ts
+++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/index.ts
@@ -5,11 +5,23 @@
* 2.0.
*/
-export { esIndexRoute } from './index_detail';
-export { esIndicesRoute } from './indices';
-export { esNodeRoute } from './node_detail';
-export { esNodesRoute } from './nodes';
-export { esOverviewRoute } from './overview';
-export { mlJobRoute } from './ml_jobs';
-export { ccrRoute } from './ccr';
-export { ccrShardRoute } from './ccr_shard';
+import { MonitoringCore } from '../../../../types';
+import { ccrRoute } from './ccr';
+import { ccrShardRoute } from './ccr_shard';
+import { esIndexRoute } from './index_detail';
+import { esIndicesRoute } from './indices';
+import { mlJobRoute } from './ml_jobs';
+import { esNodesRoute } from './nodes';
+import { esNodeRoute } from './node_detail';
+import { esOverviewRoute } from './overview';
+
+export function registerV1ElasticsearchRoutes(server: MonitoringCore) {
+ esIndexRoute(server);
+ esIndicesRoute(server);
+ esNodeRoute(server);
+ esNodesRoute(server);
+ esOverviewRoute(server);
+ mlJobRoute(server);
+ ccrRoute(server);
+ ccrShardRoute(server);
+}
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts
index 11e0eec3f08f0..f8742144b28f8 100644
--- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts
+++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts
@@ -19,7 +19,7 @@ import {
} from '../../../../../../common/http_api/elasticsearch_settings';
import { createValidationFunction } from '../../../../../lib/create_route_validation_function';
import { handleError } from '../../../../../lib/errors';
-import { LegacyServer, RouteDependencies } from '../../../../../types';
+import { MonitoringCore, RouteDependencies } from '../../../../../types';
const queryBody = {
size: 0,
@@ -72,7 +72,7 @@ const checkLatestMonitoringIsLegacy = async (context: RequestHandlerContext, ind
return counts;
};
-export function internalMonitoringCheckRoute(server: LegacyServer, npRoute: RouteDependencies) {
+export function internalMonitoringCheckRoute(server: MonitoringCore, npRoute: RouteDependencies) {
const validateBody = createValidationFunction(
postElasticsearchSettingsInternalMonitoringRequestPayloadRT
);
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/index.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/index.ts
index 61bb1ba804a5a..dfc68068bf80d 100644
--- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/index.ts
+++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/index.ts
@@ -5,8 +5,20 @@
* 2.0.
*/
-export { clusterSettingsCheckRoute } from './check/cluster';
-export { internalMonitoringCheckRoute } from './check/internal_monitoring';
-export { nodesSettingsCheckRoute } from './check/nodes';
-export { setCollectionEnabledRoute } from './set/collection_enabled';
-export { setCollectionIntervalRoute } from './set/collection_interval';
+import { MonitoringCore, RouteDependencies } from '../../../../types';
+import { clusterSettingsCheckRoute } from './check/cluster';
+import { internalMonitoringCheckRoute } from './check/internal_monitoring';
+import { nodesSettingsCheckRoute } from './check/nodes';
+import { setCollectionEnabledRoute } from './set/collection_enabled';
+import { setCollectionIntervalRoute } from './set/collection_interval';
+
+export function registerV1ElasticsearchSettingsRoutes(
+ server: MonitoringCore,
+ npRoute: RouteDependencies
+) {
+ clusterSettingsCheckRoute(server);
+ internalMonitoringCheckRoute(server, npRoute);
+ nodesSettingsCheckRoute(server);
+ setCollectionEnabledRoute(server);
+ setCollectionIntervalRoute(server);
+}
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/index.ts b/x-pack/plugins/monitoring/server/routes/api/v1/index.ts
new file mode 100644
index 0000000000000..e0f5e55c6c128
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/routes/api/v1/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { registerV1AlertRoutes } from './alerts';
+export { registerV1ApmRoutes } from './apm';
+export { registerV1BeatsRoutes } from './beats';
+export { registerV1CheckAccessRoutes } from './check_access';
+export { registerV1ClusterRoutes } from './cluster';
+export { registerV1ElasticsearchRoutes } from './elasticsearch';
+export { registerV1ElasticsearchSettingsRoutes } from './elasticsearch_settings';
+export { registerV1LogstashRoutes } from './logstash';
+export { registerV1SetupRoutes } from './setup';
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/index.ts b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/index.ts
index b267c17fc3346..a4975726cf0a1 100644
--- a/x-pack/plugins/monitoring/server/routes/api/v1/logstash/index.ts
+++ b/x-pack/plugins/monitoring/server/routes/api/v1/logstash/index.ts
@@ -5,10 +5,21 @@
* 2.0.
*/
-export { logstashNodesRoute } from './nodes';
-export { logstashNodeRoute } from './node';
-export { logstashOverviewRoute } from './overview';
-export { logstashPipelineRoute } from './pipeline';
-export { logstashNodePipelinesRoute } from './pipelines/node_pipelines';
-export { logstashClusterPipelinesRoute } from './pipelines/cluster_pipelines';
-export { logstashClusterPipelineIdsRoute } from './pipelines/cluster_pipeline_ids';
+import { MonitoringCore } from '../../../../types';
+import { logstashNodeRoute } from './node';
+import { logstashNodesRoute } from './nodes';
+import { logstashOverviewRoute } from './overview';
+import { logstashPipelineRoute } from './pipeline';
+import { logstashClusterPipelinesRoute } from './pipelines/cluster_pipelines';
+import { logstashClusterPipelineIdsRoute } from './pipelines/cluster_pipeline_ids';
+import { logstashNodePipelinesRoute } from './pipelines/node_pipelines';
+
+export function registerV1LogstashRoutes(server: MonitoringCore) {
+ logstashClusterPipelineIdsRoute(server);
+ logstashClusterPipelinesRoute(server);
+ logstashNodePipelinesRoute(server);
+ logstashNodeRoute(server);
+ logstashNodesRoute(server);
+ logstashOverviewRoute(server);
+ logstashPipelineRoute(server);
+}
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/setup/cluster_setup_status.js b/x-pack/plugins/monitoring/server/routes/api/v1/setup/cluster_setup_status.js
deleted file mode 100644
index bc8b722d22214..0000000000000
--- a/x-pack/plugins/monitoring/server/routes/api/v1/setup/cluster_setup_status.js
+++ /dev/null
@@ -1,72 +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 { schema } from '@kbn/config-schema';
-import { verifyMonitoringAuth } from '../../../../lib/elasticsearch/verify_monitoring_auth';
-import { handleError } from '../../../../lib/errors';
-import { getCollectionStatus } from '../../../../lib/setup/collection';
-import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns';
-
-export function clusterSetupStatusRoute(server) {
- /*
- * Monitoring Home
- * Route Init (for checking license and compatibility for multi-cluster monitoring
- */
- server.route({
- method: 'POST',
- path: '/api/monitoring/v1/setup/collection/cluster/{clusterUuid?}',
- config: {
- validate: {
- params: schema.object({
- clusterUuid: schema.maybe(schema.string()),
- }),
- query: schema.object({
- // This flag is not intended to be used in production. It was introduced
- // as a way to ensure consistent API testing - the typical data source
- // for API tests are archived data, where the cluster configuration and data
- // are consistent from environment to environment. However, this endpoint
- // also attempts to retrieve data from the running stack products (ES and Kibana)
- // which will vary from environment to environment making it difficult
- // to write tests against. Therefore, this flag exists and should only be used
- // in our testing environment.
- skipLiveData: schema.boolean({ defaultValue: false }),
- }),
- body: schema.nullable(
- schema.object({
- ccs: schema.maybe(schema.string()),
- timeRange: schema.object({
- min: schema.string({ defaultValue: '' }),
- max: schema.string({ defaultValue: '' }),
- }),
- })
- ),
- },
- },
- handler: async (req) => {
- let status = null;
-
- // NOTE using try/catch because checkMonitoringAuth is expected to throw
- // an error when current logged-in user doesn't have permission to read
- // the monitoring data. `try/catch` makes it a little more explicit.
- try {
- await verifyMonitoringAuth(req);
- const indexPatterns = getIndexPatterns(server, {}, req.payload.ccs);
- status = await getCollectionStatus(
- req,
- indexPatterns,
- req.params.clusterUuid,
- null,
- req.query.skipLiveData
- );
- } catch (err) {
- throw handleError(err, req);
- }
-
- return status;
- },
- });
-}
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/setup/cluster_setup_status.ts b/x-pack/plugins/monitoring/server/routes/api/v1/setup/cluster_setup_status.ts
new file mode 100644
index 0000000000000..370947df46b42
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/routes/api/v1/setup/cluster_setup_status.ts
@@ -0,0 +1,62 @@
+/*
+ * 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 {
+ postClusterSetupStatusRequestParamsRT,
+ postClusterSetupStatusRequestPayloadRT,
+ postClusterSetupStatusRequestQueryRT,
+ postClusterSetupStatusResponsePayloadRT,
+} from '../../../../../common/http_api/setup';
+import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns';
+import { createValidationFunction } from '../../../../lib/create_route_validation_function';
+import { verifyMonitoringAuth } from '../../../../lib/elasticsearch/verify_monitoring_auth';
+import { handleError } from '../../../../lib/errors';
+import { getCollectionStatus } from '../../../../lib/setup/collection';
+import { MonitoringCore } from '../../../../types';
+
+export function clusterSetupStatusRoute(server: MonitoringCore) {
+ /*
+ * Monitoring Home
+ * Route Init (for checking license and compatibility for multi-cluster monitoring
+ */
+
+ const validateParams = createValidationFunction(postClusterSetupStatusRequestParamsRT);
+ const validateQuery = createValidationFunction(postClusterSetupStatusRequestQueryRT);
+ const validateBody = createValidationFunction(postClusterSetupStatusRequestPayloadRT);
+
+ server.route({
+ method: 'post',
+ path: '/api/monitoring/v1/setup/collection/cluster/{clusterUuid?}',
+ validate: {
+ params: validateParams,
+ query: validateQuery,
+ body: validateBody,
+ },
+ handler: async (req) => {
+ const clusterUuid = req.params.clusterUuid;
+ const skipLiveData = req.query.skipLiveData;
+
+ // NOTE using try/catch because checkMonitoringAuth is expected to throw
+ // an error when current logged-in user doesn't have permission to read
+ // the monitoring data. `try/catch` makes it a little more explicit.
+ try {
+ await verifyMonitoringAuth(req);
+ const indexPatterns = getIndexPatterns(server.config, {}, req.payload.ccs);
+ const status = await getCollectionStatus(
+ req,
+ indexPatterns,
+ clusterUuid,
+ undefined,
+ skipLiveData
+ );
+ return postClusterSetupStatusResponsePayloadRT.encode(status);
+ } catch (err) {
+ throw handleError(err, req);
+ }
+ },
+ });
+}
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/setup/disable_elasticsearch_internal_collection.js b/x-pack/plugins/monitoring/server/routes/api/v1/setup/disable_elasticsearch_internal_collection.ts
similarity index 74%
rename from x-pack/plugins/monitoring/server/routes/api/v1/setup/disable_elasticsearch_internal_collection.js
rename to x-pack/plugins/monitoring/server/routes/api/v1/setup/disable_elasticsearch_internal_collection.ts
index 9590d91c357ee..cdecf346bae9d 100644
--- a/x-pack/plugins/monitoring/server/routes/api/v1/setup/disable_elasticsearch_internal_collection.js
+++ b/x-pack/plugins/monitoring/server/routes/api/v1/setup/disable_elasticsearch_internal_collection.ts
@@ -5,21 +5,19 @@
* 2.0.
*/
-import { schema } from '@kbn/config-schema';
+import { postDisableInternalCollectionRequestParamsRT } from '../../../../../common/http_api/setup';
+import { createValidationFunction } from '../../../../lib/create_route_validation_function';
import { verifyMonitoringAuth } from '../../../../lib/elasticsearch/verify_monitoring_auth';
-import { handleError } from '../../../../lib/errors';
import { setCollectionDisabled } from '../../../../lib/elasticsearch_settings/set/collection_disabled';
+import { handleError } from '../../../../lib/errors';
+import { MonitoringCore } from '../../../../types';
-export function disableElasticsearchInternalCollectionRoute(server) {
+export function disableElasticsearchInternalCollectionRoute(server: MonitoringCore) {
server.route({
- method: 'POST',
+ method: 'post',
path: '/api/monitoring/v1/setup/collection/{clusterUuid}/disable_internal_collection',
- config: {
- validate: {
- params: schema.object({
- clusterUuid: schema.string(),
- }),
- },
+ validate: {
+ params: createValidationFunction(postDisableInternalCollectionRequestParamsRT),
},
handler: async (req) => {
// NOTE using try/catch because checkMonitoringAuth is expected to throw
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/setup/index.ts b/x-pack/plugins/monitoring/server/routes/api/v1/setup/index.ts
new file mode 100644
index 0000000000000..6a8ecac8597a8
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/routes/api/v1/setup/index.ts
@@ -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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { MonitoringCore } from '../../../../types';
+import { clusterSetupStatusRoute } from './cluster_setup_status';
+import { disableElasticsearchInternalCollectionRoute } from './disable_elasticsearch_internal_collection';
+import { nodeSetupStatusRoute } from './node_setup_status';
+
+export function registerV1SetupRoutes(server: MonitoringCore) {
+ clusterSetupStatusRoute(server);
+ disableElasticsearchInternalCollectionRoute(server);
+ nodeSetupStatusRoute(server);
+}
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/setup/node_setup_status.js b/x-pack/plugins/monitoring/server/routes/api/v1/setup/node_setup_status.js
deleted file mode 100644
index 1f93e92843ea8..0000000000000
--- a/x-pack/plugins/monitoring/server/routes/api/v1/setup/node_setup_status.js
+++ /dev/null
@@ -1,74 +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 { schema } from '@kbn/config-schema';
-import { verifyMonitoringAuth } from '../../../../lib/elasticsearch/verify_monitoring_auth';
-import { handleError } from '../../../../lib/errors';
-import { getCollectionStatus } from '../../../../lib/setup/collection';
-import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns';
-
-export function nodeSetupStatusRoute(server) {
- /*
- * Monitoring Home
- * Route Init (for checking license and compatibility for multi-cluster monitoring
- */
- server.route({
- method: 'POST',
- path: '/api/monitoring/v1/setup/collection/node/{nodeUuid}',
- config: {
- validate: {
- params: schema.object({
- nodeUuid: schema.string(),
- }),
- query: schema.object({
- // This flag is not intended to be used in production. It was introduced
- // as a way to ensure consistent API testing - the typical data source
- // for API tests are archived data, where the cluster configuration and data
- // are consistent from environment to environment. However, this endpoint
- // also attempts to retrieve data from the running stack products (ES and Kibana)
- // which will vary from environment to environment making it difficult
- // to write tests against. Therefore, this flag exists and should only be used
- // in our testing environment.
- skipLiveData: schema.boolean({ defaultValue: false }),
- }),
- body: schema.nullable(
- schema.object({
- ccs: schema.maybe(schema.string()),
- timeRange: schema.maybe(
- schema.object({
- min: schema.string(),
- max: schema.string(),
- })
- ),
- })
- ),
- },
- },
- handler: async (req) => {
- let status = null;
-
- // NOTE using try/catch because checkMonitoringAuth is expected to throw
- // an error when current logged-in user doesn't have permission to read
- // the monitoring data. `try/catch` makes it a little more explicit.
- try {
- await verifyMonitoringAuth(req);
- const indexPatterns = getIndexPatterns(server, {}, req.payload.ccs);
- status = await getCollectionStatus(
- req,
- indexPatterns,
- null,
- req.params.nodeUuid,
- req.query.skipLiveData
- );
- } catch (err) {
- throw handleError(err, req);
- }
-
- return status;
- },
- });
-}
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/setup/node_setup_status.ts b/x-pack/plugins/monitoring/server/routes/api/v1/setup/node_setup_status.ts
new file mode 100644
index 0000000000000..327b741a0e64a
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/routes/api/v1/setup/node_setup_status.ts
@@ -0,0 +1,64 @@
+/*
+ * 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 {
+ postNodeSetupStatusRequestParamsRT,
+ postNodeSetupStatusRequestPayloadRT,
+ postNodeSetupStatusRequestQueryRT,
+ postNodeSetupStatusResponsePayloadRT,
+} from '../../../../../common/http_api/setup';
+import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns';
+import { createValidationFunction } from '../../../../lib/create_route_validation_function';
+import { verifyMonitoringAuth } from '../../../../lib/elasticsearch/verify_monitoring_auth';
+import { handleError } from '../../../../lib/errors';
+import { getCollectionStatus } from '../../../../lib/setup/collection';
+import { MonitoringCore } from '../../../../types';
+
+export function nodeSetupStatusRoute(server: MonitoringCore) {
+ /*
+ * Monitoring Home
+ * Route Init (for checking license and compatibility for multi-cluster monitoring
+ */
+
+ const validateParams = createValidationFunction(postNodeSetupStatusRequestParamsRT);
+ const validateQuery = createValidationFunction(postNodeSetupStatusRequestQueryRT);
+ const validateBody = createValidationFunction(postNodeSetupStatusRequestPayloadRT);
+
+ server.route({
+ method: 'post',
+ path: '/api/monitoring/v1/setup/collection/node/{nodeUuid}',
+ validate: {
+ params: validateParams,
+ query: validateQuery,
+ body: validateBody,
+ },
+ handler: async (req) => {
+ const nodeUuid = req.params.nodeUuid;
+ const skipLiveData = req.query.skipLiveData;
+ const ccs = req.payload.ccs;
+
+ // NOTE using try/catch because checkMonitoringAuth is expected to throw
+ // an error when current logged-in user doesn't have permission to read
+ // the monitoring data. `try/catch` makes it a little more explicit.
+ try {
+ await verifyMonitoringAuth(req);
+ const indexPatterns = getIndexPatterns(server.config, {}, ccs);
+ const status = await getCollectionStatus(
+ req,
+ indexPatterns,
+ undefined,
+ nodeUuid,
+ skipLiveData
+ );
+
+ return postNodeSetupStatusResponsePayloadRT.encode(status);
+ } catch (err) {
+ throw handleError(err, req);
+ }
+ },
+ });
+}
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/ui.js b/x-pack/plugins/monitoring/server/routes/api/v1/ui.js
deleted file mode 100644
index 618d12afedef7..0000000000000
--- a/x-pack/plugins/monitoring/server/routes/api/v1/ui.js
+++ /dev/null
@@ -1,42 +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.
- */
-
-// all routes for the app
-export { checkAccessRoute } from './check_access';
-export * from './alerts';
-export { beatsDetailRoute, beatsListingRoute, beatsOverviewRoute } from './beats';
-export { clusterRoute, clustersRoute } from './cluster';
-export {
- esIndexRoute,
- esIndicesRoute,
- esNodeRoute,
- esNodesRoute,
- esOverviewRoute,
- mlJobRoute,
- ccrRoute,
- ccrShardRoute,
-} from './elasticsearch';
-export {
- internalMonitoringCheckRoute,
- clusterSettingsCheckRoute,
- nodesSettingsCheckRoute,
- setCollectionEnabledRoute,
- setCollectionIntervalRoute,
-} from './elasticsearch_settings';
-export { kibanaInstanceRoute, kibanaInstancesRoute, kibanaOverviewRoute } from './kibana';
-export { apmInstanceRoute, apmInstancesRoute, apmOverviewRoute } from './apm';
-export {
- logstashClusterPipelinesRoute,
- logstashNodePipelinesRoute,
- logstashNodeRoute,
- logstashNodesRoute,
- logstashOverviewRoute,
- logstashPipelineRoute,
- logstashClusterPipelineIdsRoute,
-} from './logstash';
-export { entSearchOverviewRoute } from './enterprise_search';
-export * from './setup';
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/ui.ts b/x-pack/plugins/monitoring/server/routes/api/v1/ui.ts
new file mode 100644
index 0000000000000..7aaa6591e868e
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/routes/api/v1/ui.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+// these are the remaining routes not yet converted to TypeScript
+// all others are registered through index.ts
+
+// @ts-expect-error
+export { kibanaInstanceRoute, kibanaInstancesRoute, kibanaOverviewRoute } from './kibana';
+// @ts-expect-error
+export { entSearchOverviewRoute } from './enterprise_search';
diff --git a/x-pack/plugins/monitoring/server/routes/index.ts b/x-pack/plugins/monitoring/server/routes/index.ts
index 05a8de96b4c07..f38612d5a42da 100644
--- a/x-pack/plugins/monitoring/server/routes/index.ts
+++ b/x-pack/plugins/monitoring/server/routes/index.ts
@@ -8,22 +8,43 @@
import { MonitoringConfig } from '../config';
import { decorateDebugServer } from '../debug_logger';
-import { RouteDependencies } from '../types';
-// @ts-ignore
-import * as uiRoutes from './api/v1/ui'; // namespace import
+import { MonitoringCore, RouteDependencies } from '../types';
+import {
+ registerV1AlertRoutes,
+ registerV1ApmRoutes,
+ registerV1BeatsRoutes,
+ registerV1CheckAccessRoutes,
+ registerV1ClusterRoutes,
+ registerV1ElasticsearchRoutes,
+ registerV1ElasticsearchSettingsRoutes,
+ registerV1LogstashRoutes,
+ registerV1SetupRoutes,
+} from './api/v1';
+import * as uiRoutes from './api/v1/ui';
export function requireUIRoutes(
- _server: any,
+ server: MonitoringCore,
config: MonitoringConfig,
npRoute: RouteDependencies
) {
const routes = Object.keys(uiRoutes);
- const server = config.ui.debug_mode
- ? decorateDebugServer(_server, config, npRoute.logger)
- : _server;
+ const decoratedServer = config.ui.debug_mode
+ ? decorateDebugServer(server, config, npRoute.logger)
+ : server;
routes.forEach((route) => {
+ // @ts-expect-error
const registerRoute = uiRoutes[route]; // computed reference to module objects imported via namespace
registerRoute(server, npRoute);
});
+
+ registerV1AlertRoutes(decoratedServer, npRoute);
+ registerV1ApmRoutes(server);
+ registerV1BeatsRoutes(server);
+ registerV1CheckAccessRoutes(server);
+ registerV1ClusterRoutes(server);
+ registerV1ElasticsearchRoutes(server);
+ registerV1ElasticsearchSettingsRoutes(server, npRoute);
+ registerV1LogstashRoutes(server);
+ registerV1SetupRoutes(server);
}
diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts
index 86447a24fdf04..64931f5888514 100644
--- a/x-pack/plugins/monitoring/server/types.ts
+++ b/x-pack/plugins/monitoring/server/types.ts
@@ -34,7 +34,7 @@ import { LicensingPluginStart } from '@kbn/licensing-plugin/server';
import { PluginSetupContract as FeaturesPluginSetupContract } from '@kbn/features-plugin/server';
import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server';
import { CloudSetup } from '@kbn/cloud-plugin/server';
-import { RouteConfig, RouteMethod } from '@kbn/core/server';
+import { RouteConfig, RouteMethod, Headers } from '@kbn/core/server';
import { ElasticsearchModifiedSource } from '../common/types/es';
import { RulesByType } from '../common/types/alerts';
import { configSchema, MonitoringConfig } from './config';
@@ -124,6 +124,7 @@ export interface LegacyRequest {
payload: Body;
params: Params;
query: Query;
+ headers: Headers;
getKibanaStatsCollector: () => any;
getUiSettingsService: () => any;
getActionTypeRegistry: () => any;
From 6fc2fff3f2dfc263f767bbc54f46eb4946438e4c Mon Sep 17 00:00:00 2001
From: Lisa Cawley
Date: Fri, 20 May 2022 10:48:15 -0700
Subject: [PATCH 26/39] [ML] Minor edits in prebuilt job descriptions (#132633)
---
.../modules/security_auth/ml/auth_high_count_logon_events.json | 2 +-
.../ml/auth_high_count_logon_events_for_a_source_ip.json | 2 +-
.../modules/security_auth/ml/auth_high_count_logon_fails.json | 2 +-
.../models/data_recognizer/modules/security_linux/manifest.json | 2 +-
.../security_network/ml/high_count_by_destination_country.json | 2 +-
.../modules/security_network/ml/high_count_network_denies.json | 2 +-
.../modules/security_network/ml/high_count_network_events.json | 2 +-
.../data_recognizer/modules/security_windows/manifest.json | 2 +-
8 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json
index 35fc14e23624f..fa87299dfb464 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json
@@ -1,6 +1,6 @@
{
"job_type": "anomaly_detector",
- "description": "Security: Authentication - looks for an unusually large spike in successful authentication events. This can be due to password spraying, user enumeration or brute force activity.",
+ "description": "Security: Authentication - Looks for an unusually large spike in successful authentication events. This can be due to password spraying, user enumeration, or brute force activity.",
"groups": [
"security",
"authentication"
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json
index cdf219152c7fd..9f2f10973a35b 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json
@@ -1,6 +1,6 @@
{
"job_type": "anomaly_detector",
- "description": "Security: Authentication - looks for an unusually large spike in successful authentication events from a particular source IP address. This can be due to password spraying, user enumeration or brute force activity.",
+ "description": "Security: Authentication - Looks for an unusually large spike in successful authentication events from a particular source IP address. This can be due to password spraying, user enumeration, or brute force activity.",
"groups": [
"security",
"authentication"
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json
index cde52bf7d33cc..c74dff5257864 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json
@@ -1,6 +1,6 @@
{
"job_type": "anomaly_detector",
- "description": "Security: Authentication - looks for an unusually large spike in authentication failure events. This can be due to password spraying, user enumeration or brute force activity and may be a precursor to account takeover or credentialed access.",
+ "description": "Security: Authentication - Looks for an unusually large spike in authentication failure events. This can be due to password spraying, user enumeration, or brute force activity and may be a precursor to account takeover or credentialed access.",
"groups": [
"security",
"authentication"
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/manifest.json
index efed4a3c9e9b1..cfa9f45c5d1ac 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/manifest.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/manifest.json
@@ -1,7 +1,7 @@
{
"id": "security_linux_v3",
"title": "Security: Linux",
- "description": "Anomaly detection jobs for Linux host based threat hunting and detection.",
+ "description": "Anomaly detection jobs for Linux host-based threat hunting and detection.",
"type": "linux data",
"logoFile": "logo.json",
"defaultIndexPattern": "auditbeat-*,logs-*",
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json
index 2360233937c2b..45375ad939f36 100755
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json
@@ -1,6 +1,6 @@
{
"job_type": "anomaly_detector",
- "description": "Security: Network - looks for an unusually large spike in network activity to one destination country in the network logs. This could be due to unusually large amounts of reconnaissance or enumeration traffic. Data exfiltration activity may also produce such a surge in traffic to a destination country which does not normally appear in network traffic or business work-flows. Malware instances and persistence mechanisms may communicate with command-and-control (C2) infrastructure in their country of origin, which may be an unusual destination country for the source network.",
+ "description": "Security: Network - Looks for an unusually large spike in network activity to one destination country in the network logs. This could be due to unusually large amounts of reconnaissance or enumeration traffic. Data exfiltration activity may also produce such a surge in traffic to a destination country which does not normally appear in network traffic or business work-flows. Malware instances and persistence mechanisms may communicate with command-and-control (C2) infrastructure in their country of origin, which may be an unusual destination country for the source network.",
"groups": [
"security",
"network"
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json
index 2a3b4b0100183..45c22599f37d2 100755
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json
@@ -1,6 +1,6 @@
{
"job_type": "anomaly_detector",
- "description": "Security: Network - looks for an unusually large spike in network traffic that was denied by network ACLs or firewall rules. Such a burst of denied traffic is usually either 1) a misconfigured application or firewall or 2) suspicious or malicious activity. Unsuccessful attempts at network transit, in order to connect to command-and-control (C2), or engage in data exfiltration, may produce a burst of failed connections. This could also be due to unusually large amounts of reconnaissance or enumeration traffic. Denial-of-service attacks or traffic floods may also produce such a surge in traffic.",
+ "description": "Security: Network - Looks for an unusually large spike in network traffic that was denied by network ACLs or firewall rules. Such a burst of denied traffic is usually either 1) a misconfigured application or firewall or 2) suspicious or malicious activity. Unsuccessful attempts at network transit, in order to connect to command-and-control (C2), or engage in data exfiltration, may produce a burst of failed connections. This could also be due to unusually large amounts of reconnaissance or enumeration traffic. Denial-of-service attacks or traffic floods may also produce such a surge in traffic.",
"groups": [
"security",
"network"
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json
index 792d7f2513985..a3bb734ad9bdc 100755
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json
@@ -1,6 +1,6 @@
{
"job_type": "anomaly_detector",
- "description": "Security: Network - looks for an unusually large spike in network traffic. Such a burst of traffic, if not caused by a surge in business activity, can be due to suspicious or malicious activity. Large-scale data exfiltration may produce a burst of network traffic; this could also be due to unusually large amounts of reconnaissance or enumeration traffic. Denial-of-service attacks or traffic floods may also produce such a surge in traffic.",
+ "description": "Security: Network - Looks for an unusually large spike in network traffic. Such a burst of traffic, if not caused by a surge in business activity, can be due to suspicious or malicious activity. Large-scale data exfiltration may produce a burst of network traffic; this could also be due to unusually large amounts of reconnaissance or enumeration traffic. Denial-of-service attacks or traffic floods may also produce such a surge in traffic.",
"groups": [
"security",
"network"
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/manifest.json
index bf39cd7ec7902..8d01d0d91e0c2 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/manifest.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/manifest.json
@@ -1,7 +1,7 @@
{
"id": "security_windows_v3",
"title": "Security: Windows",
- "description": "Anomaly detection jobs for Windows host based threat hunting and detection.",
+ "description": "Anomaly detection jobs for Windows host-based threat hunting and detection.",
"type": "windows data",
"logoFile": "logo.json",
"defaultIndexPattern": "winlogbeat-*,logs-*",
From e857b30f8a9b4b1ccaa9527b3809875b892dec01 Mon Sep 17 00:00:00 2001
From: Vadim Kibana <82822460+vadimkibana@users.noreply.github.com>
Date: Fri, 20 May 2022 20:36:59 +0200
Subject: [PATCH 27/39] remove human-readable automatic slug generation
(#132593)
* remove human-readable automatic slug generation
* make change non-breaking
* [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix'
* remove test
Co-authored-by: streamich
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
src/plugins/share/README.mdx | 13 -------------
.../share/common/url_service/short_urls/types.ts | 6 ------
.../short_urls/short_url_client.test.ts | 2 --
.../url_service/short_urls/short_url_client.ts | 3 ---
.../http/short_urls/register_create_route.ts | 12 ++++++++++--
.../short_urls/short_url_client.test.ts | 13 -------------
.../url_service/short_urls/short_url_client.ts | 4 +---
.../apis/short_url/create_short_url/main.ts | 16 ----------------
8 files changed, 11 insertions(+), 58 deletions(-)
diff --git a/src/plugins/share/README.mdx b/src/plugins/share/README.mdx
index 1a1e2e721c2ab..1a1fef0587812 100644
--- a/src/plugins/share/README.mdx
+++ b/src/plugins/share/README.mdx
@@ -215,19 +215,6 @@ const url = await shortUrls.create({
});
```
-You can make the short URL slug human-readable by specifying the
-`humanReadableSlug` flag:
-
-```ts
-const url = await shortUrls.create({
- locator,
- params: {
- dashboardId: '123',
- },
- humanReadableSlug: true,
-});
-```
-
Or you can manually specify the slug for the short URL using the `slug` option:
```ts
diff --git a/src/plugins/share/common/url_service/short_urls/types.ts b/src/plugins/share/common/url_service/short_urls/types.ts
index 44e11a0610a66..a7c4d106873f9 100644
--- a/src/plugins/share/common/url_service/short_urls/types.ts
+++ b/src/plugins/share/common/url_service/short_urls/types.ts
@@ -79,12 +79,6 @@ export interface ShortUrlCreateParams {
* URL. This part will be visible to the user, it can have user-friendly text.
*/
slug?: string;
-
- /**
- * Whether to generate a slug automatically. If `true`, the slug will be
- * a human-readable text consisting of three worlds: "--".
- */
- humanReadableSlug?: boolean;
}
/**
diff --git a/src/plugins/share/public/url_service/short_urls/short_url_client.test.ts b/src/plugins/share/public/url_service/short_urls/short_url_client.test.ts
index 8a125206d1c80..693d06538e63e 100644
--- a/src/plugins/share/public/url_service/short_urls/short_url_client.test.ts
+++ b/src/plugins/share/public/url_service/short_urls/short_url_client.test.ts
@@ -88,7 +88,6 @@ describe('create()', () => {
body: expect.any(String),
});
expect(JSON.parse(fetchSpy.mock.calls[0][1].body)).toStrictEqual({
- humanReadableSlug: false,
locatorId: LEGACY_SHORT_URL_LOCATOR_ID,
params: {
url: 'https://example.com/foo/bar',
@@ -173,7 +172,6 @@ describe('createFromLongUrl()', () => {
body: expect.any(String),
});
expect(JSON.parse(fetchSpy.mock.calls[0][1].body)).toStrictEqual({
- humanReadableSlug: true,
locatorId: LEGACY_SHORT_URL_LOCATOR_ID,
params: {
url: '/a/b/c',
diff --git a/src/plugins/share/public/url_service/short_urls/short_url_client.ts b/src/plugins/share/public/url_service/short_urls/short_url_client.ts
index 63dcdc0b78718..4a9dbf3909288 100644
--- a/src/plugins/share/public/url_service/short_urls/short_url_client.ts
+++ b/src/plugins/share/public/url_service/short_urls/short_url_client.ts
@@ -59,7 +59,6 @@ export class BrowserShortUrlClient implements IShortUrlClient {
locator,
params,
slug = undefined,
- humanReadableSlug = false,
}: ShortUrlCreateParams): Promise> {
const { http } = this.dependencies;
const data = await http.fetch>('/api/short_url', {
@@ -67,7 +66,6 @@ export class BrowserShortUrlClient implements IShortUrlClient {
body: JSON.stringify({
locatorId: locator.id,
slug,
- humanReadableSlug,
params,
}),
});
@@ -113,7 +111,6 @@ export class BrowserShortUrlClient implements IShortUrlClient {
const result = await this.createWithLocator({
locator,
- humanReadableSlug: true,
params: {
url: relativeUrl,
},
diff --git a/src/plugins/share/server/url_service/http/short_urls/register_create_route.ts b/src/plugins/share/server/url_service/http/short_urls/register_create_route.ts
index 1208f6fda4d1e..97594837f0720 100644
--- a/src/plugins/share/server/url_service/http/short_urls/register_create_route.ts
+++ b/src/plugins/share/server/url_service/http/short_urls/register_create_route.ts
@@ -26,6 +26,15 @@ export const registerCreateRoute = (router: IRouter, url: ServerUrlService) => {
minLength: 3,
maxLength: 255,
}),
+ /**
+ * @deprecated
+ *
+ * This field is deprecated as the API does not support automatic
+ * human-readable slug generation.
+ *
+ * @todo This field will be removed in a future version. It is left
+ * here for backwards compatibility.
+ */
humanReadableSlug: schema.boolean({
defaultValue: false,
}),
@@ -36,7 +45,7 @@ export const registerCreateRoute = (router: IRouter, url: ServerUrlService) => {
router.handleLegacyErrors(async (ctx, req, res) => {
const savedObjects = (await ctx.core).savedObjects.client;
const shortUrls = url.shortUrls.get({ savedObjects });
- const { locatorId, params, slug, humanReadableSlug } = req.body;
+ const { locatorId, params, slug } = req.body;
const locator = url.locators.get(locatorId);
if (!locator) {
@@ -51,7 +60,6 @@ export const registerCreateRoute = (router: IRouter, url: ServerUrlService) => {
locator,
params,
slug,
- humanReadableSlug,
});
return res.ok({
diff --git a/src/plugins/share/server/url_service/short_urls/short_url_client.test.ts b/src/plugins/share/server/url_service/short_urls/short_url_client.test.ts
index 5fc108cdbf56c..fe6365d498628 100644
--- a/src/plugins/share/server/url_service/short_urls/short_url_client.test.ts
+++ b/src/plugins/share/server/url_service/short_urls/short_url_client.test.ts
@@ -128,19 +128,6 @@ describe('ServerShortUrlClient', () => {
})
).rejects.toThrowError(new UrlServiceError(`Slug "lala" already exists.`, 'SLUG_EXISTS'));
});
-
- test('can automatically generate human-readable slug', async () => {
- const { client, locator } = setup();
- const shortUrl = await client.create({
- locator,
- humanReadableSlug: true,
- params: {
- url: '/app/test#foo/bar/baz',
- },
- });
-
- expect(shortUrl.data.slug.split('-').length).toBe(3);
- });
});
describe('.get()', () => {
diff --git a/src/plugins/share/server/url_service/short_urls/short_url_client.ts b/src/plugins/share/server/url_service/short_urls/short_url_client.ts
index 762ded11bf8ee..cecc4c3127135 100644
--- a/src/plugins/share/server/url_service/short_urls/short_url_client.ts
+++ b/src/plugins/share/server/url_service/short_urls/short_url_client.ts
@@ -8,7 +8,6 @@
import type { SerializableRecord } from '@kbn/utility-types';
import { SavedObjectReference } from '@kbn/core/server';
-import { generateSlug } from 'random-word-slugs';
import { ShortUrlRecord } from '.';
import type {
IShortUrlClient,
@@ -60,14 +59,13 @@ export class ServerShortUrlClient implements IShortUrlClient {
locator,
params,
slug = '',
- humanReadableSlug = false,
}: ShortUrlCreateParams): Promise> {
if (slug) {
validateSlug(slug);
}
if (!slug) {
- slug = humanReadableSlug ? generateSlug() : randomStr(4);
+ slug = randomStr(5);
}
const { storage, currentVersion } = this.dependencies;
diff --git a/test/api_integration/apis/short_url/create_short_url/main.ts b/test/api_integration/apis/short_url/create_short_url/main.ts
index 4eb6fa489b725..d0b57a9873135 100644
--- a/test/api_integration/apis/short_url/create_short_url/main.ts
+++ b/test/api_integration/apis/short_url/create_short_url/main.ts
@@ -70,22 +70,6 @@ export default function ({ getService }: FtrProviderContext) {
expect(response.body.url).to.be('');
});
- it('can generate a human-readable slug, composed of three words', async () => {
- const response = await supertest.post('/api/short_url').send({
- locatorId: 'LEGACY_SHORT_URL_LOCATOR',
- params: {},
- humanReadableSlug: true,
- });
-
- expect(response.status).to.be(200);
- expect(typeof response.body.slug).to.be('string');
- const words = response.body.slug.split('-');
- expect(words.length).to.be(3);
- for (const word of words) {
- expect(word.length > 0).to.be(true);
- }
- });
-
it('can create a short URL with custom slug', async () => {
const rnd = Math.round(Math.random() * 1e6) + 1;
const slug = 'test-slug-' + Date.now() + '-' + rnd;
From 46cd72911c5e96b8dc8df6e12d47df004efba381 Mon Sep 17 00:00:00 2001
From: Jan Monschke
Date: Fri, 20 May 2022 22:02:00 +0200
Subject: [PATCH 28/39] [SecuritySolution] Disable agent status filters and
timeline interaction (#132586)
* fix: disable drag-ability and hover actions for agent statuses
The agent fields cannot be queried with ECS and therefore should not provide Filter In/Out functionality nor should users be able to add their representative fields to timeline investigations. Therefore users should not be able to add them to a timeline query by dragging them.
* chore: make code more readable
---
.../table/summary_value_cell.tsx | 46 +++++++++++--------
.../body/renderers/agent_statuses.tsx | 38 +++------------
2 files changed, 32 insertions(+), 52 deletions(-)
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_value_cell.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_value_cell.tsx
index 1c9c0292ed912..d4677d22485b4 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_value_cell.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_value_cell.tsx
@@ -16,6 +16,8 @@ import { AGENT_STATUS_FIELD_NAME } from '../../../../timelines/components/timeli
const FIELDS_WITHOUT_ACTIONS: { [field: string]: boolean } = { [AGENT_STATUS_FIELD_NAME]: true };
+const style = { flexGrow: 0 };
+
export const SummaryValueCell: React.FC = ({
data,
eventId,
@@ -25,32 +27,36 @@ export const SummaryValueCell: React.FC = ({
timelineId,
values,
isReadOnly,
-}) => (
- <>
-
- {timelineId !== TimelineId.active && !isReadOnly && !FIELDS_WITHOUT_ACTIONS[data.field] && (
- {
+ const hoverActionsEnabled = !FIELDS_WITHOUT_ACTIONS[data.field];
+
+ return (
+ <>
+
- )}
- >
-);
+ {timelineId !== TimelineId.active && !isReadOnly && hoverActionsEnabled && (
+
+ )}
+ >
+ );
+};
SummaryValueCell.displayName = 'SummaryValueCell';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx
index edc8faff1b5fc..c459a9f05a678 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx
@@ -7,7 +7,6 @@
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
-import { DefaultDraggable } from '../../../../../common/components/draggables';
import { EndpointHostIsolationStatus } from '../../../../../common/components/endpoint/host_isolation';
import { useHostIsolationStatus } from '../../../../../detections/containers/detection_engine/alerts/use_host_isolation_status';
import { AgentStatus } from '../../../../../common/components/endpoint/agent_status';
@@ -33,26 +32,11 @@ export const AgentStatuses = React.memo(
}) => {
const { isIsolated, agentStatus, pendingIsolation, pendingUnisolation } =
useHostIsolationStatus({ agentId: value });
- const isolationFieldName = 'host.isolation';
return (
{agentStatus !== undefined ? (
- {isDraggable ? (
-
-
-
- ) : (
-
- )}
+
) : (
@@ -60,21 +44,11 @@ export const AgentStatuses = React.memo(
)}
-
-
-
+
);
From e55bf409976a22c429d3f48f4a3c198e2591cbe3 Mon Sep 17 00:00:00 2001
From: Nathan Reese
Date: Fri, 20 May 2022 14:15:00 -0600
Subject: [PATCH 29/39] [Maps] create MVT_VECTOR when using choropleth wizard
(#132648)
---
.../create_choropleth_layer_descriptor.ts | 61 +++++++++++--------
1 file changed, 36 insertions(+), 25 deletions(-)
diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/wizards/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts
index 92045f5911176..36e07d7383d18 100644
--- a/x-pack/plugins/maps/public/classes/layers/wizards/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts
+++ b/x-pack/plugins/maps/public/classes/layers/wizards/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts
@@ -10,6 +10,7 @@ import {
AGG_TYPE,
COLOR_MAP_TYPE,
FIELD_ORIGIN,
+ LAYER_TYPE,
SCALING_TYPES,
SOURCE_TYPES,
STYLE_TYPE,
@@ -21,10 +22,11 @@ import {
CountAggDescriptor,
EMSFileSourceDescriptor,
ESSearchSourceDescriptor,
+ JoinDescriptor,
VectorStylePropertiesDescriptor,
} from '../../../../../common/descriptor_types';
import { VectorStyle } from '../../../styles/vector/vector_style';
-import { GeoJsonVectorLayer } from '../../vector_layer';
+import { GeoJsonVectorLayer, MvtVectorLayer } from '../../vector_layer';
import { EMSFileSource } from '../../../sources/ems_file_source';
// @ts-ignore
import { ESSearchSource } from '../../../sources/es_search_source';
@@ -38,14 +40,14 @@ function createChoroplethLayerDescriptor({
rightIndexPatternId,
rightIndexPatternTitle,
rightTermField,
- setLabelStyle,
+ layerType,
}: {
sourceDescriptor: EMSFileSourceDescriptor | ESSearchSourceDescriptor;
leftField: string;
rightIndexPatternId: string;
rightIndexPatternTitle: string;
rightTermField: string;
- setLabelStyle: boolean;
+ layerType: LAYER_TYPE.GEOJSON_VECTOR | LAYER_TYPE.MVT_VECTOR;
}) {
const metricsDescriptor: CountAggDescriptor = { type: AGG_TYPE.COUNT };
const joinId = uuid();
@@ -75,7 +77,8 @@ function createChoroplethLayerDescriptor({
},
},
};
- if (setLabelStyle) {
+ // Styling label by join metric with MVT is not supported
+ if (layerType === LAYER_TYPE.GEOJSON_VECTOR) {
styleProperties[VECTOR_STYLES.LABEL_TEXT] = {
type: STYLE_TYPE.DYNAMIC,
options: {
@@ -88,26 +91,34 @@ function createChoroplethLayerDescriptor({
};
}
- return GeoJsonVectorLayer.createDescriptor({
- joins: [
- {
- leftField,
- right: {
- type: SOURCE_TYPES.ES_TERM_SOURCE,
- id: joinId,
- indexPatternId: rightIndexPatternId,
- indexPatternTitle: rightIndexPatternTitle,
- term: rightTermField,
- metrics: [metricsDescriptor],
- applyGlobalQuery: true,
- applyGlobalTime: true,
- applyForceRefresh: true,
- },
+ const joins = [
+ {
+ leftField,
+ right: {
+ type: SOURCE_TYPES.ES_TERM_SOURCE,
+ id: joinId,
+ indexPatternId: rightIndexPatternId,
+ indexPatternTitle: rightIndexPatternTitle,
+ term: rightTermField,
+ metrics: [metricsDescriptor],
+ applyGlobalQuery: true,
+ applyGlobalTime: true,
+ applyForceRefresh: true,
},
- ],
- sourceDescriptor,
- style: VectorStyle.createDescriptor(styleProperties),
- });
+ } as JoinDescriptor,
+ ];
+
+ return layerType === LAYER_TYPE.MVT_VECTOR
+ ? MvtVectorLayer.createDescriptor({
+ joins,
+ sourceDescriptor,
+ style: VectorStyle.createDescriptor(styleProperties),
+ })
+ : GeoJsonVectorLayer.createDescriptor({
+ joins,
+ sourceDescriptor,
+ style: VectorStyle.createDescriptor(styleProperties),
+ });
}
export function createEmsChoroplethLayerDescriptor({
@@ -132,7 +143,7 @@ export function createEmsChoroplethLayerDescriptor({
rightIndexPatternId,
rightIndexPatternTitle,
rightTermField,
- setLabelStyle: true,
+ layerType: LAYER_TYPE.GEOJSON_VECTOR,
});
}
@@ -165,6 +176,6 @@ export function createEsChoroplethLayerDescriptor({
rightIndexPatternId,
rightIndexPatternTitle,
rightTermField,
- setLabelStyle: false, // Styling label by join metric with MVT is not supported
+ layerType: LAYER_TYPE.MVT_VECTOR,
});
}
From 791ebfad8c589b91555a7e252d99ea1841f75906 Mon Sep 17 00:00:00 2001
From: debadair
Date: Fri, 20 May 2022 13:34:04 -0700
Subject: [PATCH 30/39] [DOCS] Remove obsolete license expiration info
(#131474)
* [DOCS] Remove obsolete license expiration info
As of https://github.com/elastic/elasticsearch/pull/79671, Elasticsearch does a more stringent license check
rather than operating in a semi-degraded mode.
Closes #127845
Closes #125702
* Update docs/management/managing-licenses.asciidoc
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
docs/management/managing-licenses.asciidoc | 192 +++------------------
1 file changed, 22 insertions(+), 170 deletions(-)
diff --git a/docs/management/managing-licenses.asciidoc b/docs/management/managing-licenses.asciidoc
index cf501518ea534..837a83f0aae38 100644
--- a/docs/management/managing-licenses.asciidoc
+++ b/docs/management/managing-licenses.asciidoc
@@ -1,191 +1,43 @@
[[managing-licenses]]
== License Management
-When you install the default distribution of {kib}, you receive free features
-with no expiration date. For the full list of features, refer to
-{subscriptions}.
+By default, new installations have a Basic license that never expires.
+For the full list of features available at the Free and Open Basic subscription level,
+refer to {subscriptions}.
-If you want to try out the full set of features, you can activate a free 30-day
-trial. To view the status of your license, start a trial, or install a new
-license, open the main menu, then click *Stack Management > License Management*.
-
-NOTE: You can start a trial only if your cluster has not already activated a
-trial license for the current major product version. For example, if you have
-already activated a trial for 6.0, you cannot start a new trial until
-7.0. You can, however, request an extended trial at {extendtrial}.
-
-When you activate a new license level, new features appear in *Stack Management*.
-
-[role="screenshot"]
-image::images/management-license.png[]
+To explore all of the available solutions and features, start a 30-day free trial.
+You can activate a trial subscription once per major product version.
+If you need more than 30 days to complete your evaluation,
+request an extended trial at {extendtrial}.
-At the end of the trial period, some features operate in a
-<>. You can revert to Basic, extend the trial,
-or purchase a subscription.
-
-TIP: If {security-features} are enabled, unless you have a trial license,
-you must configure Transport Layer Security (TLS) in {es}.
-See {ref}/encrypting-communications.html[Encrypting communications].
-{kib} and the {ref}/start-basic.html[start basic API] provide a list of all of
-the features that will no longer be supported if you revert to a basic license.
+To view the status of your license, start a trial, or install a new
+license, open the main menu, then click *Stack Management > License Management*.
-[float]
+[discrete]
=== Required permissions
The `manage` cluster privilege is required to access *License Management*.
To add the privilege, open the main menu, then click *Stack Management > Roles*.
-[discrete]
-[[update-license]]
-=== Update your license
-
-You can update your license at runtime without shutting down your {es} nodes.
-License updates take effect immediately. The license is provided as a _JSON_
-file that you install in {kib} or by using the
-{ref}/update-license.html[update license API].
-
-TIP: If you are using a basic or trial license, {security-features} are disabled
-by default. In all other licenses, {security-features} are enabled by default;
-you must secure the {stack} or disable the {security-features}.
-
[discrete]
[[license-expiration]]
=== License expiration
-Your license is time based and expires at a future date. If you're using
-{monitor-features} and your license will expire within 30 days, a license
-expiration warning is displayed prominently. Warnings are also displayed on
-startup and written to the {es} log starting 30 days from the expiration date.
-These error messages tell you when the license expires and what features will be
-disabled if you do not update the license.
-
-IMPORTANT: You should update your license as soon as possible. You are
-essentially flying blind when running with an expired license. Access to the
-cluster health and stats APIs is critical for monitoring and managing an {es}
-cluster.
-
-[discrete]
-[[expiration-beats]]
-==== Beats
-
-* Beats will continue to poll centrally-managed configuration.
-
-[discrete]
-[[expiration-elasticsearch]]
-==== {es}
-
-// Upgrade API is disabled
-* The deprecation API is disabled.
-* SQL support is disabled.
-* Aggregations provided by the analytics plugin are no longer usable.
-* All searchable snapshots indices are unassigned and cannot be searched.
-
-[discrete]
-[[expiration-watcher]]
-==== {stack} {alert-features}
-
-* The PUT and GET watch APIs are disabled. The DELETE watch API continues to work.
-* Watches execute and write to the history.
-* The actions of the watches do not execute.
-
-[discrete]
-[[expiration-graph]]
-==== {stack} {graph-features}
-
-* Graph explore APIs are disabled.
-
-[discrete]
-[[expiration-ml]]
-==== {stack} {ml-features}
+Licenses are valid for a specific time period.
+30 days before the license expiration date, {es} starts logging expiration warnings.
+If monitoring is enabled, expiration warnings are displayed prominently in {kib}.
-* APIs to create {anomaly-jobs}, open jobs, send data to jobs, create {dfeeds},
-and start {dfeeds} are disabled.
-* All started {dfeeds} are stopped.
-* All open {anomaly-jobs} are closed.
-* APIs to create and start {dfanalytics-jobs} are disabled.
-* Existing {anomaly-job} and {dfanalytics-job} results continue to be available
-by using {kib} or APIs.
+If your license expires, your subscription level reverts to Basic and
+you will no longer be able to use https://www.elastic.co/subscriptions[Platinum or Enterprise features].
[discrete]
-[[expiration-monitoring]]
-==== {stack} {monitor-features}
-
-* The agent stops collecting cluster and indices metrics.
-* The agent stops automatically cleaning indices older than
-`xpack.monitoring.history.duration`.
-
-[discrete]
-[[expiration-security]]
-==== {stack} {security-features}
-
-* Cluster health, cluster stats, and indices stats operations are blocked.
-* All data operations (read and write) continue to work.
-
-Once the license expires, calls to the cluster health, cluster stats, and index
-stats APIs fail with a `security_exception` and return a 403 HTTP status code.
-
-[source,sh]
------------------------------------------------------
-{
- "error": {
- "root_cause": [
- {
- "type": "security_exception",
- "reason": "current license is non-compliant for [security]",
- "license.expired.feature": "security"
- }
- ],
- "type": "security_exception",
- "reason": "current license is non-compliant for [security]",
- "license.expired.feature": "security"
- },
- "status": 403
-}
------------------------------------------------------
-
-This message enables automatic monitoring systems to easily detect the license
-failure without immediately impacting other users.
-
-[discrete]
-[[expiration-logstash]]
-==== {ls} pipeline management
-
-* Cannot create new pipelines or edit or delete existing pipelines from the UI.
-* Cannot list or view existing pipelines from the UI.
-* Cannot run Logstash instances which are registered to listen to existing pipelines.
-//TBD: * Logstash will continue to poll centrally-managed pipelines
-
-[discrete]
-[[expiration-kibana]]
-==== {kib}
-
-* Users can still log into {kib}.
-* {kib} works for data exploration and visualization, but some features
-are disabled.
-* The license management UI is available to easily upgrade your license. See
-<> and <>.
-
-[discrete]
-[[expiration-reporting]]
-==== {kib} {report-features}
-
-* Reporting is no longer available in {kib}.
-* Report generation URLs stop working.
-* Existing reports are no longer accessible.
-
-[discrete]
-[[expiration-rollups]]
-==== {rollups-cap}
-
-* {rollup-jobs-cap} cannot be created or started.
-* Existing {rollup-jobs} can be stopped and deleted.
-* The get rollup caps and rollup search APIs continue to function.
+[[update-license]]
+=== Update your license
-[discrete]
-[[expiration-transforms]]
-==== {transforms-cap}
+Licenses are provided as a _JSON_ file and have an effective date and an expiration date.
+You cannot install a new license before its effective date.
+License updates take effect immediately and do not require restarting {es}.
-* {transforms-cap} cannot be created, previewed, started, or updated.
-* Existing {transforms} can be stopped and deleted.
-* Existing {transform} results continue to be available.
+You can update your license from *Stack Management > License Management* or through the
+{ref}/update-license.html[update license API].
From 41635e288f790a8e79e8294f65d43212fa479366 Mon Sep 17 00:00:00 2001
From: Karl Godard
Date: Fri, 20 May 2022 13:35:30 -0700
Subject: [PATCH 31/39] fixed search highlighting. was only showing highlighted
text w/o context (#132650)
Co-authored-by: mitodrummer
---
.../public/components/process_tree_node/index.test.tsx | 10 ++++++++--
.../public/components/process_tree_node/index.tsx | 2 +-
.../public/components/process_tree_node/styles.ts | 2 +-
3 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx
index 1316313427c5e..cff05c5c1003b 100644
--- a/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx
+++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx
@@ -295,13 +295,19 @@ describe('ProcessTreeNode component', () => {
describe('Search', () => {
it('highlights text within the process node line item if it matches the searchQuery', () => {
// set a mock search matched indicator for the process (typically done by ProcessTree/helpers.ts)
- processMock.searchMatched = '/vagrant';
+ processMock.searchMatched = '/vagr';
renderResult = mockedContext.render();
expect(
renderResult.getByTestId('sessionView:processNodeSearchHighlight').textContent
- ).toEqual('/vagrant');
+ ).toEqual('/vagr');
+
+ // ensures we are showing the rest of the info, and not replacing it with just the match.
+ const { process } = props.process.getDetails();
+ expect(renderResult.container.textContent).toContain(
+ process?.working_directory + '\xA0' + (process?.args && process.args.join(' '))
+ );
});
});
});
diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx
index 4d6074497af5a..f65cb0f25530a 100644
--- a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx
+++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx
@@ -146,7 +146,7 @@ export function ProcessTreeNode({
});
// eslint-disable-next-line no-unsanitized/property
- textRef.current.innerHTML = html;
+ textRef.current.innerHTML = '' + html + '';
}
}
}, [searchMatched, styles.searchHighlight]);
diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts b/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts
index b68df480064b3..54dbdb1bc4565 100644
--- a/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts
+++ b/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts
@@ -117,7 +117,6 @@ export const useStyles = ({
fontSize: FONT_SIZE,
lineHeight: LINE_HEIGHT,
verticalAlign: 'middle',
- display: 'inline-block',
},
};
@@ -165,6 +164,7 @@ export const useStyles = ({
paddingLeft: size.xxl,
position: 'relative',
lineHeight: LINE_HEIGHT,
+ marginTop: '1px',
};
const alertDetails: CSSObject = {
From e0ea600d54ec68159fc6fa89eb761b36988cc1a6 Mon Sep 17 00:00:00 2001
From: Hannah Mudge
Date: Fri, 20 May 2022 14:55:31 -0600
Subject: [PATCH 32/39] Add group 6 to FTR config (#132655)
---
.buildkite/ftr_configs.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml
index e070baa844ea9..4a59641e29af2 100644
--- a/.buildkite/ftr_configs.yml
+++ b/.buildkite/ftr_configs.yml
@@ -68,6 +68,7 @@ enabled:
- test/functional/apps/dashboard/group3/config.ts
- test/functional/apps/dashboard/group4/config.ts
- test/functional/apps/dashboard/group5/config.ts
+ - test/functional/apps/dashboard/group6/config.ts
- test/functional/apps/discover/config.ts
- test/functional/apps/getting_started/config.ts
- test/functional/apps/home/config.ts
From eb6a061a930a0c48fa4a28b66197b7681d3fd5cf Mon Sep 17 00:00:00 2001
From: Brian Seeders
Date: Fri, 20 May 2022 16:57:49 -0400
Subject: [PATCH 33/39] [docs] Add 'yarn dev-docs' for managing and starting
dev docs (#132647)
---
package.json | 1 +
scripts/dev_docs.sh | 103 ++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 104 insertions(+)
create mode 100755 scripts/dev_docs.sh
diff --git a/package.json b/package.json
index 9b01ec9decdcb..36a1cd9a5ffad 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,7 @@
"cover:report": "nyc report --temp-dir target/kibana-coverage/functional --report-dir target/coverage/report --reporter=lcov && open ./target/coverage/report/lcov-report/index.html",
"debug": "node --nolazy --inspect scripts/kibana --dev",
"debug-break": "node --nolazy --inspect-brk scripts/kibana --dev",
+ "dev-docs": "scripts/dev_docs.sh",
"docs:acceptApiChanges": "node --max-old-space-size=6144 scripts/check_published_api_changes.js --accept",
"es": "node scripts/es",
"preinstall": "node ./preinstall_check",
diff --git a/scripts/dev_docs.sh b/scripts/dev_docs.sh
new file mode 100755
index 0000000000000..55d8f4d51e8dc
--- /dev/null
+++ b/scripts/dev_docs.sh
@@ -0,0 +1,103 @@
+#!/bin/bash
+set -euo pipefail
+
+KIBANA_DIR=$(cd "$(dirname "$0")"/.. && pwd)
+WORKSPACE=$(cd "$KIBANA_DIR/.." && pwd)/kibana-docs
+export NVM_DIR="$WORKSPACE/.nvm"
+
+DOCS_DIR="$WORKSPACE/docs.elastic.dev"
+
+# These are the other repos with docs currently required to build the docs in this repo and not get errors
+# For example, kibana docs link to docs in these repos, and if they aren't built, you'll get errors
+DEV_DIR="$WORKSPACE/dev"
+TEAM_DIR="$WORKSPACE/kibana-team"
+
+cd "$KIBANA_DIR"
+origin=$(git remote get-url origin || true)
+GIT_PREFIX="git@github.com:"
+if [[ "$origin" == "https"* ]]; then
+ GIT_PREFIX="https://github.com/"
+fi
+
+mkdir -p "$WORKSPACE"
+cd "$WORKSPACE"
+
+if [[ ! -d "$NVM_DIR" ]]; then
+ echo "Installing a separate copy of nvm"
+ git clone https://github.com/nvm-sh/nvm.git "$NVM_DIR"
+ cd "$NVM_DIR"
+ git checkout "$(git describe --abbrev=0 --tags --match "v[0-9]*" "$(git rev-list --tags --max-count=1)")"
+ cd "$WORKSPACE"
+fi
+source "$NVM_DIR/nvm.sh"
+
+if [[ ! -d "$DOCS_DIR" ]]; then
+ echo "Cloning docs.elastic.dev repo..."
+ git clone --depth 1 "${GIT_PREFIX}elastic/docs.elastic.dev.git"
+else
+ cd "$DOCS_DIR"
+ git pull
+ cd "$WORKSPACE"
+fi
+
+if [[ ! -d "$DEV_DIR" ]]; then
+ echo "Cloning dev repo..."
+ git clone --depth 1 "${GIT_PREFIX}elastic/dev.git"
+else
+ cd "$DEV_DIR"
+ git pull
+ cd "$WORKSPACE"
+fi
+
+if [[ ! -d "$TEAM_DIR" ]]; then
+ echo "Cloning kibana-team repo..."
+ git clone --depth 1 "${GIT_PREFIX}elastic/kibana-team.git"
+else
+ cd "$TEAM_DIR"
+ git pull
+ cd "$WORKSPACE"
+fi
+
+# The minimum sources required to build kibana docs
+cat << EOF > "$DOCS_DIR/sources-dev.json"
+{
+ "sources": [
+ {
+ "type": "file",
+ "location": "$KIBANA_DIR"
+ },
+ {
+ "type": "file",
+ "location": "$DEV_DIR"
+ },
+ {
+ "type": "file",
+ "location": "$TEAM_DIR"
+ }
+ ]
+}
+EOF
+
+cd "$DOCS_DIR"
+nvm install
+
+if ! which yarn; then
+ npm install -g yarn
+fi
+
+yarn
+
+if [[ ! -d .docsmobile ]]; then
+ yarn init-docs
+fi
+
+echo ""
+echo "The docs.elastic.dev project is located at:"
+echo "$DOCS_DIR"
+echo ""
+
+if [[ "${1:-}" ]]; then
+ yarn "$@"
+else
+ yarn dev
+fi
From 642290b0f11a6647aef0170648b1a24da712ba56 Mon Sep 17 00:00:00 2001
From: Nathan Reese
Date: Fri, 20 May 2022 15:11:15 -0600
Subject: [PATCH 34/39] [maps] convert ESPewPewSource to typescript (#132656)
* [maps] convert ESPewPewSource to typescript
* move @ts-expect-error moved by fix
---
.../security/create_layer_descriptors.ts | 2 -
...ew_pew_source.js => es_pew_pew_source.tsx} | 102 ++++++++++++------
.../es_pew_pew_source/{index.js => index.ts} | 0
.../point_2_point_layer_wizard.tsx | 9 +-
4 files changed, 73 insertions(+), 40 deletions(-)
rename x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/{es_pew_pew_source.js => es_pew_pew_source.tsx} (67%)
rename x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/{index.js => index.ts} (100%)
diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/security/create_layer_descriptors.ts b/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/security/create_layer_descriptors.ts
index 5792d861f6f5c..f295464126c96 100644
--- a/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/security/create_layer_descriptors.ts
+++ b/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/security/create_layer_descriptors.ts
@@ -24,9 +24,7 @@ import {
} from '../../../../../../common/constants';
import { GeoJsonVectorLayer } from '../../../vector_layer';
import { VectorStyle } from '../../../../styles/vector/vector_style';
-// @ts-ignore
import { ESSearchSource } from '../../../../sources/es_search_source';
-// @ts-ignore
import { ESPewPewSource } from '../../../../sources/es_pew_pew_source';
import { getDefaultDynamicProperties } from '../../../../styles/vector/vector_style_defaults';
import { APM_INDEX_PATTERN_TITLE } from '../observability';
diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.tsx
similarity index 67%
rename from x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js
rename to x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.tsx
index a38c769205304..910181d6a2868 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js
+++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.tsx
@@ -8,17 +8,35 @@
import React from 'react';
import turfBbox from '@turf/bbox';
import { multiPoint } from '@turf/helpers';
+import { Adapters } from '@kbn/inspector-plugin/common/adapters';
+import { type Filter, buildExistsFilter } from '@kbn/es-query';
+import { lastValueFrom } from 'rxjs';
+import type {
+ AggregationsGeoBoundsAggregate,
+ LatLonGeoLocation,
+ TopLeftBottomRightGeoBounds,
+} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
-import { UpdateSourceEditor } from './update_source_editor';
import { i18n } from '@kbn/i18n';
+// @ts-expect-error
+import { UpdateSourceEditor } from './update_source_editor';
import { SOURCE_TYPES, VECTOR_SHAPE_TYPE } from '../../../../common/constants';
import { getDataSourceLabel, getDataViewLabel } from '../../../../common/i18n_getters';
+// @ts-expect-error
import { convertToLines } from './convert_to_lines';
import { AbstractESAggSource } from '../es_agg_source';
import { registerSource } from '../source_registry';
import { turfBboxToBounds } from '../../../../common/elasticsearch_util';
import { DataRequestAbortError } from '../../util/data_request';
import { makePublicExecutionContext } from '../../../util';
+import { SourceEditorArgs } from '../source';
+import {
+ ESPewPewSourceDescriptor,
+ MapExtent,
+ VectorSourceRequestMeta,
+} from '../../../../common/descriptor_types';
+import { isValidStringConfig } from '../../util/valid_string_config';
+import { BoundsRequestMeta, GeoJsonWithMeta } from '../vector_source';
const MAX_GEOTILE_LEVEL = 29;
@@ -27,20 +45,30 @@ export const sourceTitle = i18n.translate('xpack.maps.source.pewPewTitle', {
});
export class ESPewPewSource extends AbstractESAggSource {
- static type = SOURCE_TYPES.ES_PEW_PEW;
+ readonly _descriptor: ESPewPewSourceDescriptor;
- static createDescriptor(descriptor) {
+ static createDescriptor(descriptor: Partial): ESPewPewSourceDescriptor {
const normalizedDescriptor = AbstractESAggSource.createDescriptor(descriptor);
+ if (!isValidStringConfig(descriptor.sourceGeoField)) {
+ throw new Error('Cannot create ESPewPewSourceDescriptor, sourceGeoField is not provided');
+ }
+ if (!isValidStringConfig(descriptor.destGeoField)) {
+ throw new Error('Cannot create ESPewPewSourceDescriptor, destGeoField is not provided');
+ }
return {
...normalizedDescriptor,
- type: ESPewPewSource.type,
- indexPatternId: descriptor.indexPatternId,
- sourceGeoField: descriptor.sourceGeoField,
- destGeoField: descriptor.destGeoField,
+ type: SOURCE_TYPES.ES_PEW_PEW,
+ sourceGeoField: descriptor.sourceGeoField!,
+ destGeoField: descriptor.destGeoField!,
};
}
- renderSourceSettingsEditor({ onChange }) {
+ constructor(descriptor: ESPewPewSourceDescriptor) {
+ super(descriptor);
+ this._descriptor = descriptor;
+ }
+
+ renderSourceSettingsEditor({ onChange }: SourceEditorArgs) {
return (
void) => void,
+ isRequestStillActive: () => boolean,
+ inspectorAdapters: Adapters
+ ): Promise {
const indexPattern = await this.getIndexPattern();
const searchSource = await this.makeSearchSource(searchFilters, 0);
searchSource.setField('trackTotalHits', false);
@@ -151,14 +179,10 @@ export class ESPewPewSource extends AbstractESAggSource {
// Some underlying indices may not contain geo fields
// Filter out documents without geo fields to avoid shard failures for those indices
searchSource.setField('filter', [
- ...searchSource.getField('filter'),
+ ...(searchSource.getField('filter') as Filter[]),
// destGeoField exists ensured by buffer filter
// so only need additional check for sourceGeoField
- {
- exists: {
- field: this._descriptor.sourceGeoField,
- },
- },
+ buildExistsFilter({ name: this._descriptor.sourceGeoField, type: 'geo_point' }, indexPattern),
]);
const esResponse = await this._runEsQuery({
@@ -188,7 +212,10 @@ export class ESPewPewSource extends AbstractESAggSource {
return this._descriptor.destGeoField;
}
- async getBoundsForFilters(boundsFilters, registerCancelCallback) {
+ async getBoundsForFilters(
+ boundsFilters: BoundsRequestMeta,
+ registerCancelCallback: (callback: () => void) => void
+ ): Promise {
const searchSource = await this.makeSearchSource(boundsFilters, 0);
searchSource.setField('trackTotalHits', false);
searchSource.setField('aggs', {
@@ -208,31 +235,36 @@ export class ESPewPewSource extends AbstractESAggSource {
try {
const abortController = new AbortController();
registerCancelCallback(() => abortController.abort());
- const { rawResponse: esResp } = await searchSource
- .fetch$({
+ const { rawResponse: esResp } = await lastValueFrom(
+ searchSource.fetch$({
abortSignal: abortController.signal,
legacyHitsTotal: false,
executionContext: makePublicExecutionContext('es_pew_pew_source:bounds'),
})
- .toPromise();
- if (esResp.aggregations.destFitToBounds.bounds) {
+ );
+ const destBounds = (esResp.aggregations?.destFitToBounds as AggregationsGeoBoundsAggregate)
+ .bounds as TopLeftBottomRightGeoBounds;
+ if (destBounds) {
corners.push([
- esResp.aggregations.destFitToBounds.bounds.top_left.lon,
- esResp.aggregations.destFitToBounds.bounds.top_left.lat,
+ (destBounds.top_left as LatLonGeoLocation).lon,
+ (destBounds.top_left as LatLonGeoLocation).lat,
]);
corners.push([
- esResp.aggregations.destFitToBounds.bounds.bottom_right.lon,
- esResp.aggregations.destFitToBounds.bounds.bottom_right.lat,
+ (destBounds.bottom_right as LatLonGeoLocation).lon,
+ (destBounds.bottom_right as LatLonGeoLocation).lat,
]);
}
- if (esResp.aggregations.sourceFitToBounds.bounds) {
+ const sourceBounds = (
+ esResp.aggregations?.sourceFitToBounds as AggregationsGeoBoundsAggregate
+ ).bounds as TopLeftBottomRightGeoBounds;
+ if (sourceBounds) {
corners.push([
- esResp.aggregations.sourceFitToBounds.bounds.top_left.lon,
- esResp.aggregations.sourceFitToBounds.bounds.top_left.lat,
+ (sourceBounds.top_left as LatLonGeoLocation).lon,
+ (sourceBounds.top_left as LatLonGeoLocation).lat,
]);
corners.push([
- esResp.aggregations.sourceFitToBounds.bounds.bottom_right.lon,
- esResp.aggregations.sourceFitToBounds.bounds.bottom_right.lat,
+ (sourceBounds.bottom_right as LatLonGeoLocation).lon,
+ (sourceBounds.bottom_right as LatLonGeoLocation).lat,
]);
}
} catch (error) {
diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/index.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/index.ts
similarity index 100%
rename from x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/index.js
rename to x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/index.ts
diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx
index 37ecbfdebab11..aa128e3c7d8ff 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx
@@ -9,7 +9,6 @@ import React from 'react';
import { i18n } from '@kbn/i18n';
import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults';
import { GeoJsonVectorLayer } from '../../layers/vector_layer';
-// @ts-ignore
import { ESPewPewSource, sourceTitle } from './es_pew_pew_source';
import { VectorStyle } from '../../styles/vector/vector_style';
import {
@@ -24,7 +23,11 @@ import { NUMERICAL_COLOR_PALETTES } from '../../styles/color_palettes';
// @ts-ignore
import { CreateSourceEditor } from './create_source_editor';
import { LayerWizard, RenderWizardArguments } from '../../layers';
-import { ColorDynamicOptions, SizeDynamicOptions } from '../../../../common/descriptor_types';
+import {
+ ColorDynamicOptions,
+ ESPewPewSourceDescriptor,
+ SizeDynamicOptions,
+} from '../../../../common/descriptor_types';
import { Point2PointLayerIcon } from '../../layers/wizards/icons/point_2_point_layer_icon';
export const point2PointLayerWizardConfig: LayerWizard = {
@@ -36,7 +39,7 @@ export const point2PointLayerWizardConfig: LayerWizard = {
}),
icon: Point2PointLayerIcon,
renderWizard: ({ previewLayers }: RenderWizardArguments) => {
- const onSourceConfigChange = (sourceConfig: unknown) => {
+ const onSourceConfigChange = (sourceConfig: Partial) => {
if (!sourceConfig) {
previewLayers([]);
return;
From 51ae0208dc381c82d8ba224f155b7ced2ba73d1b Mon Sep 17 00:00:00 2001
From: Constance
Date: Fri, 20 May 2022 14:30:36 -0700
Subject: [PATCH 35/39] Upgrade EUI to v55.1.3 (#132451)
* Upgrade EUI to 55.1.3 backport
* [Deprecation] Remove `watchedItemProps` from EuiContextMenu usage - should no longer be necessary
* Update snapshots with new data-popover attr
* Fix failing FTR test
- Now that EuiContextMenu focus is restored correctly, there is a tooltip around the popover toggle that's blocking an above item that the test wants to click
- swapping the order so that the tooltip does not block the clicked item should work
* Fix 2nd maps FTR test with blocking tooltip
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
package.json | 2 +-
src/dev/license_checker/config.ts | 2 +-
.../public/components/field_picker/field_search.tsx | 1 -
.../saved_objects/public/finder/saved_object_finder.tsx | 6 +-----
.../footer/settings/__snapshots__/settings.test.tsx.snap | 6 +++++-
.../markdown_editor/plugins/lens/saved_objects_finder.tsx | 6 +-----
.../lens/public/indexpattern_datasource/datapanel.tsx | 1 -
.../application/components/anomalies_table/links_menu.tsx | 6 +-----
.../edit_role/spaces_popover_list/spaces_popover_list.tsx | 1 -
.../spaces/public/nav_control/components/spaces_menu.tsx | 1 -
.../test/functional/apps/maps/group1/layer_visibility.js | 2 ++
x-pack/test/functional/apps/maps/group1/sample_data.js | 2 +-
yarn.lock | 8 ++++----
13 files changed, 17 insertions(+), 27 deletions(-)
diff --git a/package.json b/package.json
index 36a1cd9a5ffad..e5fffb5b3a394 100644
--- a/package.json
+++ b/package.json
@@ -110,7 +110,7 @@
"@elastic/datemath": "5.0.3",
"@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@8.2.0-canary.2",
"@elastic/ems-client": "8.3.2",
- "@elastic/eui": "55.1.2",
+ "@elastic/eui": "55.1.3",
"@elastic/filesaver": "1.1.2",
"@elastic/node-crypto": "1.2.1",
"@elastic/numeral": "^2.5.1",
diff --git a/src/dev/license_checker/config.ts b/src/dev/license_checker/config.ts
index f10fb0231352d..66e2664b2e8b4 100644
--- a/src/dev/license_checker/config.ts
+++ b/src/dev/license_checker/config.ts
@@ -77,6 +77,6 @@ export const LICENSE_OVERRIDES = {
'jsts@1.6.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts
'@mapbox/jsonlint-lines-primitives@2.0.2': ['MIT'], // license in readme https://github.com/tmcw/jsonlint
'@elastic/ems-client@8.3.2': ['Elastic License 2.0'],
- '@elastic/eui@55.1.2': ['SSPL-1.0 OR Elastic License 2.0'],
+ '@elastic/eui@55.1.3': ['SSPL-1.0 OR Elastic License 2.0'],
'language-subtag-registry@0.3.21': ['CC-BY-4.0'], // retired ODC‑By license https://github.com/mattcg/language-subtag-registry
};
diff --git a/src/plugins/presentation_util/public/components/field_picker/field_search.tsx b/src/plugins/presentation_util/public/components/field_picker/field_search.tsx
index 3f3dcfdef5c8b..d3307f71988f1 100644
--- a/src/plugins/presentation_util/public/components/field_picker/field_search.tsx
+++ b/src/plugins/presentation_util/public/components/field_picker/field_search.tsx
@@ -103,7 +103,6 @@ export function FieldSearch({
})}
(
}
>
-
+
{this.props.showFilter && (
(
can navigate Autoplay Settings 1`] = `
aria-live="off"
aria-modal="true"
class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--noShadow euiPopover__panel euiPopover__panel--top"
+ data-popover-panel="true"
role="dialog"
style="top: -16px; left: -22px; will-change: transform, opacity; z-index: 2000;"
tabindex="0"
@@ -108,6 +109,7 @@ exports[` can navigate Autoplay Settings 2`] = `
aria-live="off"
aria-modal="true"
class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--noShadow euiPopover__panel euiPopover__panel--top euiPopover__panel-isOpen"
+ data-popover-panel="true"
role="dialog"
style="top: -16px; left: -22px; z-index: 2000;"
tabindex="0"
@@ -359,6 +361,7 @@ exports[` can navigate Toolbar Settings, closes when activated 1`] =
aria-live="off"
aria-modal="true"
class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--noShadow euiPopover__panel euiPopover__panel--top"
+ data-popover-panel="true"
role="dialog"
style="top: -16px; left: -22px; will-change: transform, opacity; z-index: 2000;"
tabindex="0"
@@ -457,6 +460,7 @@ exports[` can navigate Toolbar Settings, closes when activated 2`] =
aria-live="off"
aria-modal="true"
class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--noShadow euiPopover__panel euiPopover__panel--top euiPopover__panel-isOpen"
+ data-popover-panel="true"
role="dialog"
style="top: -16px; left: -22px; z-index: 2000;"
tabindex="0"
@@ -631,4 +635,4 @@ exports[` can navigate Toolbar Settings, closes when activated 2`] =
`;
-exports[` can navigate Toolbar Settings, closes when activated 3`] = `"You are in a dialog. To close this dialog, hit escape.
"`;
+exports[` can navigate Toolbar Settings, closes when activated 3`] = `"You are in a dialog. To close this dialog, hit escape.
"`;
diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/saved_objects_finder.tsx b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/saved_objects_finder.tsx
index 7d7ce5d638489..3f2b3c2420629 100644
--- a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/saved_objects_finder.tsx
+++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/saved_objects_finder.tsx
@@ -394,10 +394,7 @@ export class SavedObjectFinderUi extends React.Component<
}
>
-
+
{this.props.showFilter && (
(
(
{
]);
return (
-
+
);
};
diff --git a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx
index 7e1c5eb545a28..9ddc698ef2c2b 100644
--- a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx
+++ b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx
@@ -73,7 +73,6 @@ export class SpacesPopoverList extends Component {
title: i18n.translate('xpack.security.management.editRole.spacesPopoverList.popoverTitle', {
defaultMessage: 'Spaces',
}),
- watchedItemProps: ['data-search-term'],
};
if (this.props.spaces.length >= SPACE_SEARCH_COUNT_THRESHOLD) {
diff --git a/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.tsx b/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.tsx
index 6e268d4711bb5..6f5158423ca51 100644
--- a/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.tsx
+++ b/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.tsx
@@ -69,7 +69,6 @@ class SpacesMenuUI extends Component {
id: 'xpack.spaces.navControl.spacesMenu.changeCurrentSpaceTitle',
defaultMessage: 'Change current space',
}),
- watchedItemProps: ['data-search-term'],
};
if (this.props.spaces.length >= SPACE_SEARCH_COUNT_THRESHOLD) {
diff --git a/x-pack/test/functional/apps/maps/group1/layer_visibility.js b/x-pack/test/functional/apps/maps/group1/layer_visibility.js
index cf6051cde8be7..a9bbefbff86ca 100644
--- a/x-pack/test/functional/apps/maps/group1/layer_visibility.js
+++ b/x-pack/test/functional/apps/maps/group1/layer_visibility.js
@@ -10,6 +10,7 @@ import expect from '@kbn/expect';
export default function ({ getPageObjects, getService }) {
const PageObjects = getPageObjects(['maps']);
const inspector = getService('inspector');
+ const testSubjects = getService('testSubjects');
const security = getService('security');
describe('layer visibility', () => {
@@ -31,6 +32,7 @@ export default function ({ getPageObjects, getService }) {
it('should fetch layer data when layer is made visible', async () => {
await PageObjects.maps.toggleLayerVisibility('logstash');
+ await testSubjects.click('mapLayerTOC'); // Tooltip blocks clicks otherwise
const hits = await PageObjects.maps.getHits();
expect(hits).to.equal('5');
});
diff --git a/x-pack/test/functional/apps/maps/group1/sample_data.js b/x-pack/test/functional/apps/maps/group1/sample_data.js
index cf8bd4c85cf26..62df1d3859a45 100644
--- a/x-pack/test/functional/apps/maps/group1/sample_data.js
+++ b/x-pack/test/functional/apps/maps/group1/sample_data.js
@@ -165,8 +165,8 @@ export default function ({ getPageObjects, getService, updateBaselines }) {
describe('web logs', () => {
before(async () => {
await PageObjects.maps.loadSavedMap('[Logs] Total Requests and Bytes');
- await PageObjects.maps.toggleLayerVisibility('Road map - desaturated');
await PageObjects.maps.toggleLayerVisibility('Total Requests by Destination');
+ await PageObjects.maps.toggleLayerVisibility('Road map - desaturated');
await PageObjects.timePicker.setCommonlyUsedTime('sample_data range');
await PageObjects.maps.enterFullScreen();
await PageObjects.maps.closeLegend();
diff --git a/yarn.lock b/yarn.lock
index 88a23a226d0e8..35c60d9444f32 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1503,10 +1503,10 @@
resolved "https://registry.yarnpkg.com/@elastic/eslint-plugin-eui/-/eslint-plugin-eui-0.0.2.tgz#56b9ef03984a05cc213772ae3713ea8ef47b0314"
integrity sha512-IoxURM5zraoQ7C8f+mJb9HYSENiZGgRVcG4tLQxE61yHNNRDXtGDWTZh8N1KIHcsqN1CEPETjuzBXkJYF/fDiQ==
-"@elastic/eui@55.1.2":
- version "55.1.2"
- resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-55.1.2.tgz#dd0b42f5b26c5800d6a9cb2d4c2fe1afce9d3f07"
- integrity sha512-wwZz5KxMIMFlqEsoCRiQBJDc4CrluS1d0sCOmQ5lhIzKhYc91MdxnqCk2i6YkhL4sSDf2Y9KAEuMXa+uweOWUA==
+"@elastic/eui@55.1.3":
+ version "55.1.3"
+ resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-55.1.3.tgz#976142b88156caab2ce896102a1e35fecdaa2647"
+ integrity sha512-Hf6eN9YKOKAQMMS9OV5pHLUkzpKKAxGYNVSfc/KK7hN9BlhlHH4OaZIQP3Psgf5GKoqhZrldT/N65hujk3rlLA==
dependencies:
"@types/chroma-js" "^2.0.0"
"@types/lodash" "^4.14.160"
From 788dd2e718bb4af112c43319cffcb900281d7073 Mon Sep 17 00:00:00 2001
From: Andrew Goldstein
Date: Fri, 20 May 2022 16:02:05 -0600
Subject: [PATCH 36/39] [Security Solution] Fixes sorting and tooltips on
columns for non-ECS fields that are only one level deep (#132570)
## [Security Solution] Fixes sorting and tooltips on columns for non-ECS fields that are only one level deep
This PR fixes , an issue where Timeline columns for non-ECS fields that are only one level deep couldn't be sorted, and displayed incomplete metadata in the column's tooltip.
### Before
![test_field_1_actual_tooltip](https://user-images.githubusercontent.com/4459398/169208299-51d9296a-15e1-4eb0-bc31-a0df6a63f0c5.png)
_Before: The column is **not** sortable, and the tooltip displays incomplete metadata_
### After
![after](https://user-images.githubusercontent.com/4459398/169414767-7274a795-015f-4805-8c3f-b233ead994ea.png)
_After: The column is sortable, and the tooltip displays the expected metadata_
### Desk testing
See the _Steps to reproduce_ section of for testing details.
---
.../body/column_headers/helpers.test.ts | 232 +++++++++++++++++-
.../timeline/body/column_headers/helpers.ts | 27 +-
.../components/t_grid/body/helpers.test.tsx | 2 +-
3 files changed, 251 insertions(+), 10 deletions(-)
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.test.ts
index 84cc6e60d928c..2a23b5e993637 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.test.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.test.ts
@@ -6,11 +6,12 @@
*/
import { mockBrowserFields } from '../../../../../common/containers/source/mock';
-
-import { defaultHeaders } from './default_headers';
-import { getColumnWidthFromType, getColumnHeaders } from './helpers';
-import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants';
import '../../../../../common/mock/match_media';
+import { BrowserFields } from '../../../../../../common/search_strategy';
+import { ColumnHeaderOptions } from '../../../../../../common/types';
+import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants';
+import { defaultHeaders } from './default_headers';
+import { getColumnWidthFromType, getColumnHeaders, getRootCategory } from './helpers';
describe('helpers', () => {
describe('getColumnWidthFromType', () => {
@@ -23,6 +24,32 @@ describe('helpers', () => {
});
});
+ describe('getRootCategory', () => {
+ const baseFields = ['@timestamp', '_id', 'message'];
+
+ baseFields.forEach((field) => {
+ test(`it returns the 'base' category for the ${field} field`, () => {
+ expect(
+ getRootCategory({
+ field,
+ browserFields: mockBrowserFields,
+ })
+ ).toEqual('base');
+ });
+ });
+
+ test(`it echos the field name for a field that's NOT in the base category`, () => {
+ const field = 'test_field_1';
+
+ expect(
+ getRootCategory({
+ field,
+ browserFields: mockBrowserFields,
+ })
+ ).toEqual(field);
+ });
+ });
+
describe('getColumnHeaders', () => {
test('should return a full object of ColumnHeader from the default header', () => {
const expectedData = [
@@ -80,5 +107,202 @@ describe('helpers', () => {
);
expect(getColumnHeaders(mockHeader, mockBrowserFields)).toEqual(expectedData);
});
+
+ test('it should return the expected metadata for the `_id` field, which is one level deep, and belongs to the `base` category', () => {
+ const headers: ColumnHeaderOptions[] = [
+ {
+ columnHeaderType: 'not-filtered',
+ id: '_id',
+ initialWidth: 180,
+ },
+ ];
+
+ expect(getColumnHeaders(headers, mockBrowserFields)).toEqual([
+ {
+ aggregatable: false,
+ category: 'base',
+ columnHeaderType: 'not-filtered',
+ description: 'Each document has an _id that uniquely identifies it',
+ esTypes: [],
+ example: 'Y-6TfmcB0WOhS6qyMv3s',
+ id: '_id',
+ indexes: ['auditbeat', 'filebeat', 'packetbeat'],
+ initialWidth: 180,
+ name: '_id',
+ searchable: true,
+ type: 'string',
+ },
+ ]);
+ });
+
+ test('it should return the expected metadata for a field one level deep that does NOT belong to the `base` category', () => {
+ const headers: ColumnHeaderOptions[] = [
+ {
+ columnHeaderType: 'not-filtered',
+ id: 'test_field_1', // one level deep, but does NOT belong to the `base` category
+ initialWidth: 180,
+ },
+ ];
+
+ const oneLevelDeep: BrowserFields = {
+ test_field_1: {
+ fields: {
+ test_field_1: {
+ aggregatable: true,
+ category: 'test_field_1',
+ esTypes: ['keyword'],
+ format: 'string',
+ indexes: [
+ '-*elastic-cloud-logs-*',
+ '.alerts-security.alerts-default',
+ 'apm-*-transaction*',
+ 'auditbeat-*',
+ 'endgame-*',
+ 'filebeat-*',
+ 'logs-*',
+ 'packetbeat-*',
+ 'traces-apm*',
+ 'winlogbeat-*',
+ ],
+ name: 'test_field_1',
+ readFromDocValues: true,
+ searchable: true,
+ type: 'string',
+ },
+ },
+ },
+ };
+
+ expect(getColumnHeaders(headers, oneLevelDeep)).toEqual([
+ {
+ aggregatable: true,
+ category: 'test_field_1',
+ columnHeaderType: 'not-filtered',
+ esTypes: ['keyword'],
+ format: 'string',
+ id: 'test_field_1',
+ indexes: [
+ '-*elastic-cloud-logs-*',
+ '.alerts-security.alerts-default',
+ 'apm-*-transaction*',
+ 'auditbeat-*',
+ 'endgame-*',
+ 'filebeat-*',
+ 'logs-*',
+ 'packetbeat-*',
+ 'traces-apm*',
+ 'winlogbeat-*',
+ ],
+ initialWidth: 180,
+ name: 'test_field_1',
+ readFromDocValues: true,
+ searchable: true,
+ type: 'string',
+ },
+ ]);
+ });
+
+ test('it should return the expected metadata for a field that is more than one level deep', () => {
+ const headers: ColumnHeaderOptions[] = [
+ {
+ columnHeaderType: 'not-filtered',
+ id: 'foo.bar', // two levels deep
+ initialWidth: 180,
+ },
+ ];
+
+ const twoLevelsDeep: BrowserFields = {
+ foo: {
+ fields: {
+ 'foo.bar': {
+ aggregatable: true,
+ category: 'foo',
+ esTypes: ['keyword'],
+ format: 'string',
+ indexes: [
+ '-*elastic-cloud-logs-*',
+ '.alerts-security.alerts-default',
+ 'apm-*-transaction*',
+ 'auditbeat-*',
+ 'endgame-*',
+ 'filebeat-*',
+ 'logs-*',
+ 'packetbeat-*',
+ 'traces-apm*',
+ 'winlogbeat-*',
+ ],
+ name: 'foo.bar',
+ readFromDocValues: true,
+ searchable: true,
+ type: 'string',
+ },
+ },
+ },
+ };
+
+ expect(getColumnHeaders(headers, twoLevelsDeep)).toEqual([
+ {
+ aggregatable: true,
+ category: 'foo',
+ columnHeaderType: 'not-filtered',
+ esTypes: ['keyword'],
+ format: 'string',
+ id: 'foo.bar',
+ indexes: [
+ '-*elastic-cloud-logs-*',
+ '.alerts-security.alerts-default',
+ 'apm-*-transaction*',
+ 'auditbeat-*',
+ 'endgame-*',
+ 'filebeat-*',
+ 'logs-*',
+ 'packetbeat-*',
+ 'traces-apm*',
+ 'winlogbeat-*',
+ ],
+ initialWidth: 180,
+ name: 'foo.bar',
+ readFromDocValues: true,
+ searchable: true,
+ type: 'string',
+ },
+ ]);
+ });
+
+ test('it should return the expected metadata for an UNKNOWN field one level deep', () => {
+ const headers: ColumnHeaderOptions[] = [
+ {
+ columnHeaderType: 'not-filtered',
+ id: 'unknown', // one level deep, but not contained in the `BrowserFields`
+ initialWidth: 180,
+ },
+ ];
+
+ expect(getColumnHeaders(headers, mockBrowserFields)).toEqual([
+ {
+ columnHeaderType: 'not-filtered',
+ id: 'unknown',
+ initialWidth: 180,
+ },
+ ]);
+ });
+
+ test('it should return the expected metadata for an UNKNOWN field that is more than one level deep', () => {
+ const headers: ColumnHeaderOptions[] = [
+ {
+ columnHeaderType: 'not-filtered',
+ id: 'unknown.more.than.one.level', // more than one level deep, and not contained in the `BrowserFields`
+ initialWidth: 180,
+ },
+ ];
+
+ expect(getColumnHeaders(headers, mockBrowserFields)).toEqual([
+ {
+ columnHeaderType: 'not-filtered',
+ id: 'unknown.more.than.one.level',
+ initialWidth: 180,
+ },
+ ]);
+ });
});
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts
index b1ea4899615a6..1779c39ce7b31 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts
@@ -5,12 +5,28 @@
* 2.0.
*/
-import { get } from 'lodash/fp';
+import { has, get } from 'lodash/fp';
import { ColumnHeaderOptions } from '../../../../../../common/types';
import { BrowserFields } from '../../../../../common/containers/source';
import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants';
+/**
+ * Returns the root category for fields that are only one level, e.g. `_id` or `test_field_1`
+ *
+ * The `base` category will be returned for fields that are members of `base`,
+ * e.g. the `@timestamp`, `_id`, and `message` fields.
+ *
+ * The field name will be echoed-back for all other fields, e.g. `test_field_1`
+ */
+export const getRootCategory = ({
+ browserFields,
+ field,
+}: {
+ browserFields: BrowserFields;
+ field: string;
+}): string => (has(`base.fields.${field}`, browserFields) ? 'base' : field);
+
/** Enriches the column headers with field details from the specified browserFields */
export const getColumnHeaders = (
headers: ColumnHeaderOptions[],
@@ -19,13 +35,14 @@ export const getColumnHeaders = (
return headers
? headers.map((header) => {
const splitHeader = header.id.split('.'); // source.geo.city_name -> [source, geo, city_name]
+ const category =
+ splitHeader.length > 1
+ ? splitHeader[0]
+ : getRootCategory({ field: header.id, browserFields });
return {
...header,
- ...get(
- [splitHeader.length > 1 ? splitHeader[0] : 'base', 'fields', header.id],
- browserFields
- ),
+ ...get([category, 'fields', header.id], browserFields),
};
})
: [];
diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx
index 444ba878d6709..253c3ca78b487 100644
--- a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx
@@ -138,7 +138,7 @@ describe('helpers', () => {
]);
});
- test('it defaults to a `columnType` of empty string when a column does NOT has a corresponding entry in `columnHeaders`', () => {
+ test('it defaults to a `columnType` of empty string when a column does NOT have a corresponding entry in `columnHeaders`', () => {
const withUnknownColumn: Array<{
id: string;
direction: 'asc' | 'desc';
From fb1eeb0945e25928a2516133055f048ff098166f Mon Sep 17 00:00:00 2001
From: Georgii Gorbachev
Date: Sat, 21 May 2022 00:21:53 +0200
Subject: [PATCH 37/39] [Security Solution][Detections] Add new fields to the
rule model: Related Integrations, Required Fields, and Setup (#132409)
**Addresses partially:** https://github.com/elastic/security-team/issues/2083, https://github.com/elastic/security-team/issues/558, https://github.com/elastic/security-team/issues/2856, https://github.com/elastic/security-team/issues/1801 (internal tickets)
## Summary
**TL;DR:** With this PR, it's now possible to specify `related_integrations`, `required_fields`, and `setup` fields in prebuilt rules in https://github.com/elastic/detection-rules. They are returned within rules in the API responses.
This PR:
- Adds 3 new fields to the model of Security detection rules. These fields are common to all of the rule types we have.
- **Related Integrations**. It's a list of Fleet integrations associated with a given rule. It's assumed that if the user installs them, the rule might start to work properly because it will start receiving source events potentially matching the rule's query.
- **Required Fields**. It's a list of event fields that must be present in the source indices of a given rule.
- **Setup Guide**. It's any instructions for the user for setting up their environment in order to start receiving source events for a given rule. It's a text. Markdown is supported. It's similar to the Investigation Guide that we show on the Details page.
- Adjusts API endpoints accordingly:
- These fields are for prebuilt rules only and are supposed to be read-only in the UI.
- Specifying these fields in the request parameters of the create/update/patch rule API endpoints is not supported.
- These fields are returned in all responses that contain rules. If they are missing in a rule, default values are returned (empty array, empty string).
- When duplicating a prebuilt rule, these fields are being reset to their default value (empty array, empty string).
- Export/Import is supported. Edge case / supported hack: it's possible to specify these fields manually in a ndjson doc and import with a rule.
- The fields are being copied to `kibana.alert.rule.parameters` field of an alert document, which is mapped as a flattened field type. No special handling for the new fields was needed there.
- Adjusts tests accordingly.
## Related Integrations
Example (part of a rule returned from the API):
```json
{
"related_integrations": [
{
"package": "windows",
"version": "1.5.x"
},
{
"package": "azure",
"integration": "activitylogs",
"version": "~1.1.6"
}
],
}
```
Schema:
```ts
/**
* Related integration is a potential dependency of a rule. It's assumed that if the user installs
* one of the related integrations of a rule, the rule might start to work properly because it will
* have source events (generated by this integration) potentially matching the rule's query.
*
* NOTE: Proper work is not guaranteed, because a related integration, if installed, can be
* configured differently or generate data that is not necessarily relevant for this rule.
*
* Related integration is a combination of a Fleet package and (optionally) one of the
* package's "integrations" that this package contains. It is represented by 3 properties:
*
* - `package`: name of the package (required, unique id)
* - `version`: version of the package (required, semver-compatible)
* - `integration`: name of the integration of this package (optional, id within the package)
*
* There are Fleet packages like `windows` that contain only one integration; in this case,
* `integration` should be unspecified. There are also packages like `aws` and `azure` that contain
* several integrations; in this case, `integration` should be specified.
*
* @example
* const x: RelatedIntegration = {
* package: 'windows',
* version: '1.5.x',
* };
*
* @example
* const x: RelatedIntegration = {
* package: 'azure',
* version: '~1.1.6',
* integration: 'activitylogs',
* };
*/
export type RelatedIntegration = t.TypeOf;
export const RelatedIntegration = t.exact(
t.intersection([
t.type({
package: NonEmptyString,
version: NonEmptyString,
}),
t.partial({
integration: NonEmptyString,
}),
])
);
```
## Required Fields
Example (part of a rule returned from the API):
```json
{
"required_fields": [
{
"name": "event.action",
"type": "keyword",
"ecs": true
},
{
"name": "event.code",
"type": "keyword",
"ecs": true
},
{
"name": "winlog.event_data.AttributeLDAPDisplayName",
"type": "keyword",
"ecs": false
}
],
}
```
Schema:
```ts
/**
* Almost all types of Security rules check source event documents for a match to some kind of
* query or filter. If a document has certain field with certain values, then it's a match and
* the rule will generate an alert.
*
* Required field is an event field that must be present in the source indices of a given rule.
*
* @example
* const standardEcsField: RequiredField = {
* name: 'event.action',
* type: 'keyword',
* ecs: true,
* };
*
* @example
* const nonEcsField: RequiredField = {
* name: 'winlog.event_data.AttributeLDAPDisplayName',
* type: 'keyword',
* ecs: false,
* };
*/
export type RequiredField = t.TypeOf;
export const RequiredField = t.exact(
t.type({
name: NonEmptyString,
type: NonEmptyString,
ecs: t.boolean,
})
);
```
## Setup Guide
Example (part of a rule returned from the API):
```json
{
"setup": "## Config\n\nThe 'PowerShell Script Block Logging' logging policy must be enabled.\nSteps to implement the logging policy with with Advanced Audit Configuration:\n\n```\nComputer Configuration > \nAdministrative Templates > \nWindows PowerShell > \nTurn on PowerShell Script Block Logging (Enable)\n```\n\nSteps to implement the logging policy via registry:\n\n```\nreg add \"hklm\\SOFTWARE\\Policies\\Microsoft\\Windows\\PowerShell\\ScriptBlockLogging\" /v EnableScriptBlockLogging /t REG_DWORD /d 1\n```\n",
}
```
Schema:
```ts
/**
* Any instructions for the user for setting up their environment in order to start receiving
* source events for a given rule.
*
* It's a multiline text. Markdown is supported.
*/
export type SetupGuide = t.TypeOf;
export const SetupGuide = t.string;
```
## Details on the schema
This PR adjusts all the 6 rule schemas we have:
1. Alerting Framework rule `params` schema:
- `security_solution/server/lib/detection_engine/schemas/rule_schemas.ts`
- `security_solution/server/lib/detection_engine/schemas/rule_converters.ts`
2. HTTP API main old schema:
- `security_solution/common/detection_engine/schemas/response/rules_schema.ts`
3. HTTP API main new schema:
- `security_solution/common/detection_engine/schemas/request/rule_schemas.ts`
4. Prebuilt rule schema:
- `security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts`
5. Import rule schema:
- `security_solution/common/detection_engine/schemas/request/import_rules_schema.ts`
6. Rule schema used on the frontend side:
- `security_solution/public/detections/containers/detection_engine/rules/types.ts`
Names of the fields on the HTTP API level:
- `related_integrations`
- `required_fields`
- `setup`
Names of the fields on the Alerting Framework level:
- `params.relatedIntegrations`
- `params.requiredFields`
- `params.setup`
## Next steps
- Create a new endpoint for returning installed Fleet integrations (gonna be a separate PR).
- Rebase https://github.com/elastic/kibana/pull/131475 on top of this PR after merge.
- Cover the new fields with dedicated tests (gonna be a separate PR).
- Update API docs (gonna be a separate PR).
- Address the tech debt of having 6 different schemas (gonna create a ticket for that).
### Checklist
- [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials
- [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
---
.../detection_engine/schemas/common/index.ts | 1 +
.../schemas/common/rule_params.ts | 146 +++++++
.../request/add_prepackaged_rules_schema.ts | 8 +-
.../schemas/request/import_rules_schema.ts | 8 +-
.../schemas/request/patch_rules_schema.ts | 2 +-
.../schemas/request/rule_schemas.ts | 11 +
.../schemas/response/rules_schema.mocks.ts | 6 +
.../schemas/response/rules_schema.ts | 6 +
.../security_solution/cypress/objects/rule.ts | 9 +-
.../containers/detection_engine/rules/mock.ts | 9 +
.../detection_engine/rules/types.ts | 6 +
.../detection_engine/rules/use_rule.test.tsx | 3 +
.../rules/use_rule_with_fallback.test.tsx | 3 +
.../rules/all/__mocks__/mock.ts | 6 +
.../schedule_notification_actions.test.ts | 3 +
...dule_throttle_notification_actions.test.ts | 3 +
.../routes/__mocks__/utils.ts | 3 +
.../routes/rules/utils/import_rules_utils.ts | 9 +
.../routes/rules/validate.test.ts | 3 +
.../factories/utils/build_alert.test.ts | 6 +
.../rules/create_rules.mock.ts | 9 +
.../detection_engine/rules/create_rules.ts | 6 +
.../rules/duplicate_rule.test.ts | 391 +++++++++++++-----
.../detection_engine/rules/duplicate_rule.ts | 16 +-
.../rules/get_export_all.test.ts | 3 +
.../rules/get_export_by_object_ids.test.ts | 6 +
.../rules/install_prepacked_rules.ts | 6 +
.../lib/detection_engine/rules/patch_rules.ts | 9 +
.../lib/detection_engine/rules/types.ts | 9 +
.../rules/update_prepacked_rules.ts | 9 +
.../detection_engine/rules/update_rules.ts | 3 +
.../lib/detection_engine/rules/utils.test.ts | 9 +
.../lib/detection_engine/rules/utils.ts | 8 +-
.../schemas/rule_converters.ts | 6 +
.../schemas/rule_schemas.mock.ts | 3 +
.../detection_engine/schemas/rule_schemas.ts | 8 +-
.../signals/__mocks__/es_results.ts | 9 +
.../basic/tests/create_rules.ts | 3 +
.../group1/create_rules.ts | 3 +
.../group6/alerts/alerts_compatibility.ts | 6 +
.../utils/get_complex_rule_output.ts | 3 +
.../utils/get_simple_rule_output.ts | 3 +
42 files changed, 660 insertions(+), 119 deletions(-)
create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/common/rule_params.ts
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/index.ts
index 4ef5d6178d5a5..615eb3f05876e 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/index.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/index.ts
@@ -6,4 +6,5 @@
*/
export * from './rule_monitoring';
+export * from './rule_params';
export * from './schemas';
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/rule_params.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/rule_params.ts
new file mode 100644
index 0000000000000..b9588a26bb35b
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/rule_params.ts
@@ -0,0 +1,146 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import * as t from 'io-ts';
+import { NonEmptyString } from '@kbn/securitysolution-io-ts-types';
+
+// -------------------------------------------------------------------------------------------------
+// Related integrations
+
+/**
+ * Related integration is a potential dependency of a rule. It's assumed that if the user installs
+ * one of the related integrations of a rule, the rule might start to work properly because it will
+ * have source events (generated by this integration) potentially matching the rule's query.
+ *
+ * NOTE: Proper work is not guaranteed, because a related integration, if installed, can be
+ * configured differently or generate data that is not necessarily relevant for this rule.
+ *
+ * Related integration is a combination of a Fleet package and (optionally) one of the
+ * package's "integrations" that this package contains. It is represented by 3 properties:
+ *
+ * - `package`: name of the package (required, unique id)
+ * - `version`: version of the package (required, semver-compatible)
+ * - `integration`: name of the integration of this package (optional, id within the package)
+ *
+ * There are Fleet packages like `windows` that contain only one integration; in this case,
+ * `integration` should be unspecified. There are also packages like `aws` and `azure` that contain
+ * several integrations; in this case, `integration` should be specified.
+ *
+ * @example
+ * const x: RelatedIntegration = {
+ * package: 'windows',
+ * version: '1.5.x',
+ * };
+ *
+ * @example
+ * const x: RelatedIntegration = {
+ * package: 'azure',
+ * version: '~1.1.6',
+ * integration: 'activitylogs',
+ * };
+ */
+export type RelatedIntegration = t.TypeOf;
+export const RelatedIntegration = t.exact(
+ t.intersection([
+ t.type({
+ package: NonEmptyString,
+ version: NonEmptyString,
+ }),
+ t.partial({
+ integration: NonEmptyString,
+ }),
+ ])
+);
+
+/**
+ * Array of related integrations.
+ *
+ * @example
+ * const x: RelatedIntegrationArray = [
+ * {
+ * package: 'windows',
+ * version: '1.5.x',
+ * },
+ * {
+ * package: 'azure',
+ * version: '~1.1.6',
+ * integration: 'activitylogs',
+ * },
+ * ];
+ */
+export type RelatedIntegrationArray = t.TypeOf;
+export const RelatedIntegrationArray = t.array(RelatedIntegration);
+
+// -------------------------------------------------------------------------------------------------
+// Required fields
+
+/**
+ * Almost all types of Security rules check source event documents for a match to some kind of
+ * query or filter. If a document has certain field with certain values, then it's a match and
+ * the rule will generate an alert.
+ *
+ * Required field is an event field that must be present in the source indices of a given rule.
+ *
+ * @example
+ * const standardEcsField: RequiredField = {
+ * name: 'event.action',
+ * type: 'keyword',
+ * ecs: true,
+ * };
+ *
+ * @example
+ * const nonEcsField: RequiredField = {
+ * name: 'winlog.event_data.AttributeLDAPDisplayName',
+ * type: 'keyword',
+ * ecs: false,
+ * };
+ */
+export type RequiredField = t.TypeOf;
+export const RequiredField = t.exact(
+ t.type({
+ name: NonEmptyString,
+ type: NonEmptyString,
+ ecs: t.boolean,
+ })
+);
+
+/**
+ * Array of event fields that must be present in the source indices of a given rule.
+ *
+ * @example
+ * const x: RequiredFieldArray = [
+ * {
+ * name: 'event.action',
+ * type: 'keyword',
+ * ecs: true,
+ * },
+ * {
+ * name: 'event.code',
+ * type: 'keyword',
+ * ecs: true,
+ * },
+ * {
+ * name: 'winlog.event_data.AttributeLDAPDisplayName',
+ * type: 'keyword',
+ * ecs: false,
+ * },
+ * ];
+ */
+export type RequiredFieldArray = t.TypeOf;
+export const RequiredFieldArray = t.array(RequiredField);
+
+// -------------------------------------------------------------------------------------------------
+// Setup guide
+
+/**
+ * Any instructions for the user for setting up their environment in order to start receiving
+ * source events for a given rule.
+ *
+ * It's a multiline text. Markdown is supported.
+ */
+export type SetupGuide = t.TypeOf;
+export const SetupGuide = t.string;
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts
index 618aee3379316..27ebf9a608ffa 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts
@@ -72,7 +72,10 @@ import {
Author,
event_category_override,
namespace,
-} from '../common/schemas';
+ RelatedIntegrationArray,
+ RequiredFieldArray,
+ SetupGuide,
+} from '../common';
/**
* Big differences between this schema and the createRulesSchema
@@ -117,8 +120,11 @@ export const addPrepackagedRulesSchema = t.intersection([
meta, // defaults to "undefined" if not set during decode
machine_learning_job_id, // defaults to "undefined" if not set during decode
max_signals: DefaultMaxSignalsNumber, // defaults to DEFAULT_MAX_SIGNALS (100) if not set during decode
+ related_integrations: RelatedIntegrationArray, // defaults to "undefined" if not set during decode
+ required_fields: RequiredFieldArray, // defaults to "undefined" if not set during decode
risk_score_mapping: DefaultRiskScoreMappingArray, // defaults to empty risk score mapping array if not set during decode
rule_name_override, // defaults to "undefined" if not set during decode
+ setup: SetupGuide, // defaults to "undefined" if not set during decode
severity_mapping: DefaultSeverityMappingArray, // defaults to empty actions array if not set during decode
tags: DefaultStringArray, // defaults to empty string array if not set during decode
to: DefaultToString, // defaults to "now" if not set during decode
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts
index 63c41e45e42d0..8cee4183d6ee7 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts
@@ -80,7 +80,10 @@ import {
timestamp_override,
Author,
event_category_override,
-} from '../common/schemas';
+ RelatedIntegrationArray,
+ RequiredFieldArray,
+ SetupGuide,
+} from '../common';
/**
* Differences from this and the createRulesSchema are
@@ -129,8 +132,11 @@ export const importRulesSchema = t.intersection([
meta, // defaults to "undefined" if not set during decode
machine_learning_job_id, // defaults to "undefined" if not set during decode
max_signals: DefaultMaxSignalsNumber, // defaults to DEFAULT_MAX_SIGNALS (100) if not set during decode
+ related_integrations: RelatedIntegrationArray, // defaults to "undefined" if not set during decode
+ required_fields: RequiredFieldArray, // defaults to "undefined" if not set during decode
risk_score_mapping: DefaultRiskScoreMappingArray, // defaults to empty risk score mapping array if not set during decode
rule_name_override, // defaults to "undefined" if not set during decode
+ setup: SetupGuide, // defaults to "undefined" if not set during decode
severity_mapping: DefaultSeverityMappingArray, // defaults to empty actions array if not set during decode
tags: DefaultStringArray, // defaults to empty string array if not set during decode
to: DefaultToString, // defaults to "now" if not set during decode
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts
index 8c801e75af08c..6678681471b38 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts
@@ -61,7 +61,7 @@ import {
rule_name_override,
timestamp_override,
event_category_override,
-} from '../common/schemas';
+} from '../common';
/**
* All of the patch elements should default to undefined if not set
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts
index 69a748c3bd95c..9aef9ac8f2651 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts
@@ -67,6 +67,9 @@ import {
created_by,
namespace,
ruleExecutionSummary,
+ RelatedIntegrationArray,
+ RequiredFieldArray,
+ SetupGuide,
} from '../common';
export const createSchema = <
@@ -412,6 +415,14 @@ const responseRequiredFields = {
updated_by,
created_at,
created_by,
+
+ // NOTE: For now, Related Integrations, Required Fields and Setup Guide are supported for prebuilt
+ // rules only. We don't want to allow users to edit these 3 fields via the API. If we added them
+ // to baseParams.defaultable, they would become a part of the request schema as optional fields.
+ // This is why we add them here, in order to add them only to the response schema.
+ related_integrations: RelatedIntegrationArray,
+ required_fields: RequiredFieldArray,
+ setup: SetupGuide,
};
const responseOptionalFields = {
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts
index 0642481b62a6a..eeaab6dc50021 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts
@@ -68,6 +68,9 @@ export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchem
rule_id: 'query-rule-id',
interval: '5m',
exceptions_list: getListArrayMock(),
+ related_integrations: [],
+ required_fields: [],
+ setup: '',
});
export const getRulesMlSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchema => {
@@ -132,6 +135,9 @@ export const getThreatMatchingSchemaPartialMock = (enabled = false): Partial;
diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts
index 32c55e22ae7c9..de2de9bd78160 100644
--- a/x-pack/plugins/security_solution/cypress/objects/rule.ts
+++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts
@@ -442,7 +442,9 @@ export const expectedExportedRule = (ruleResponse: Cypress.Response
severity,
query,
} = ruleResponse.body;
- const rule = {
+
+ // NOTE: Order of the properties in this object matters for the tests to work.
+ const rule: RulesSchema = {
id,
updated_at: updatedAt,
updated_by: updatedBy,
@@ -469,6 +471,9 @@ export const expectedExportedRule = (ruleResponse: Cypress.Response
version: 1,
exceptions_list: [],
immutable: false,
+ related_integrations: [],
+ required_fields: [],
+ setup: '',
type: 'query',
language: 'kuery',
index: getIndexPatterns(),
@@ -476,6 +481,8 @@ export const expectedExportedRule = (ruleResponse: Cypress.Response
throttle: 'no_actions',
actions: [],
};
+
+ // NOTE: Order of the properties in this object matters for the tests to work.
const details = {
exported_count: 1,
exported_rules_count: 1,
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts
index 8c1737a4519a7..8a23cbf9e4318 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts
@@ -38,6 +38,9 @@ export const savedRuleMock: Rule = {
max_signals: 100,
query: "user.email: 'root@elastic.co'",
references: [],
+ related_integrations: [],
+ required_fields: [],
+ setup: '',
severity: 'high',
severity_mapping: [],
tags: ['APM'],
@@ -80,6 +83,9 @@ export const rulesMock: FetchRulesResponse = {
'event.kind:alert and event.module:endgame and event.action:cred_theft_event and endgame.metadata.type:detection',
filters: [],
references: [],
+ related_integrations: [],
+ required_fields: [],
+ setup: '',
severity: 'high',
severity_mapping: [],
updated_by: 'elastic',
@@ -115,6 +121,9 @@ export const rulesMock: FetchRulesResponse = {
query: 'event.kind:alert and event.module:endgame and event.action:rules_engine_event',
filters: [],
references: [],
+ related_integrations: [],
+ required_fields: [],
+ setup: '',
severity: 'medium',
severity_mapping: [],
updated_by: 'elastic',
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts
index ddd65674274be..d6e278599d62d 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts
@@ -34,6 +34,9 @@ import {
BulkAction,
BulkActionEditPayload,
ruleExecutionSummary,
+ RelatedIntegrationArray,
+ RequiredFieldArray,
+ SetupGuide,
} from '../../../../../common/detection_engine/schemas/common';
import {
@@ -102,11 +105,14 @@ export const RuleSchema = t.intersection([
name: t.string,
max_signals: t.number,
references: t.array(t.string),
+ related_integrations: RelatedIntegrationArray,
+ required_fields: RequiredFieldArray,
risk_score: t.number,
risk_score_mapping,
rule_id: t.string,
severity,
severity_mapping,
+ setup: SetupGuide,
tags: t.array(t.string),
type,
to: t.string,
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.test.tsx
index 096463872fc01..3ca18552a85ef 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule.test.tsx
@@ -67,9 +67,12 @@ describe('useRule', () => {
max_signals: 100,
query: "user.email: 'root@elastic.co'",
references: [],
+ related_integrations: [],
+ required_fields: [],
risk_score: 75,
risk_score_mapping: [],
rule_id: 'bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf',
+ setup: '',
severity: 'high',
severity_mapping: [],
tags: ['APM'],
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx
index d7c4ad8772bd2..1816fd4c5a7af 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx
@@ -78,9 +78,12 @@ describe('useRuleWithFallback', () => {
"name": "Test rule",
"query": "user.email: 'root@elastic.co'",
"references": Array [],
+ "related_integrations": Array [],
+ "required_fields": Array [],
"risk_score": 75,
"risk_score_mapping": Array [],
"rule_id": "bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf",
+ "setup": "",
"severity": "high",
"severity_mapping": Array [],
"tags": Array [
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts
index 77de8902be33a..d9f16242a544a 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts
@@ -70,6 +70,9 @@ export const mockRule = (id: string): Rule => ({
timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2',
timeline_title: 'Untitled timeline',
meta: { from: '0m' },
+ related_integrations: [],
+ required_fields: [],
+ setup: '',
severity: 'low',
severity_mapping: [],
updated_by: 'elastic',
@@ -133,6 +136,9 @@ export const mockRuleWithEverything = (id: string): Rule => ({
timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2',
timeline_title: 'Titled timeline',
meta: { from: '0m' },
+ related_integrations: [],
+ required_fields: [],
+ setup: '',
severity: 'low',
severity_mapping: [],
updated_by: 'elastic',
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.test.ts
index d97eff43aeb8d..04e8f2130e88f 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.test.ts
@@ -51,6 +51,9 @@ describe('schedule_notification_actions', () => {
note: '# sample markdown',
version: 1,
exceptionsList: [],
+ relatedIntegrations: [],
+ requiredFields: [],
+ setup: '',
};
it('Should schedule actions with unflatted and legacy context', () => {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts
index d7293275c9c49..72ddb96301c47 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts
@@ -59,6 +59,9 @@ describe('schedule_throttle_notification_actions', () => {
note: '# sample markdown',
version: 1,
exceptionsList: [],
+ relatedIntegrations: [],
+ requiredFields: [],
+ setup: '',
};
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts
index 2622493a51dc1..54bf6133f9e37 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts
@@ -90,4 +90,7 @@ export const getOutputRuleAlertForRest = (): Omit<
note: '# Investigative notes',
version: 1,
execution_summary: undefined,
+ related_integrations: [],
+ required_fields: [],
+ setup: '',
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/import_rules_utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/import_rules_utils.ts
index d603784fc7081..8f87c1cdc0467 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/import_rules_utils.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/import_rules_utils.ts
@@ -117,10 +117,13 @@ export const importRules = async ({
index,
interval,
max_signals: maxSignals,
+ related_integrations: relatedIntegrations,
+ required_fields: requiredFields,
risk_score: riskScore,
risk_score_mapping: riskScoreMapping,
rule_name_override: ruleNameOverride,
name,
+ setup,
severity,
severity_mapping: severityMapping,
tags,
@@ -192,9 +195,12 @@ export const importRules = async ({
interval,
maxSignals,
name,
+ relatedIntegrations,
+ requiredFields,
riskScore,
riskScoreMapping,
ruleNameOverride,
+ setup,
severity,
severityMapping,
tags,
@@ -250,10 +256,13 @@ export const importRules = async ({
index,
interval,
maxSignals,
+ relatedIntegrations,
+ requiredFields,
riskScore,
riskScoreMapping,
ruleNameOverride,
name,
+ setup,
severity,
severityMapping,
tags,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts
index 0b8c49cdb4d17..833361e7e22bf 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts
@@ -63,6 +63,9 @@ export const ruleOutput = (): RulesSchema => ({
note: '# Investigative notes',
timeline_title: 'some-timeline-title',
timeline_id: 'some-timeline-id',
+ related_integrations: [],
+ required_fields: [],
+ setup: '',
});
describe('validate', () => {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts
index 5768306999f79..083f495366480 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts
@@ -126,6 +126,9 @@ describe('buildAlert', () => {
],
to: 'now',
references: ['http://example.com', 'https://example.com'],
+ related_integrations: [],
+ required_fields: [],
+ setup: '',
version: 1,
exceptions_list: [
{
@@ -303,6 +306,9 @@ describe('buildAlert', () => {
],
to: 'now',
references: ['http://example.com', 'https://example.com'],
+ related_integrations: [],
+ required_fields: [],
+ setup: '',
version: 1,
exceptions_list: [
{
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts
index 1a41adb4f6da5..3c7acccae703a 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts
@@ -32,11 +32,14 @@ export const getCreateRulesOptionsMock = (): CreateRulesOptions => ({
index: ['index-123'],
interval: '5m',
maxSignals: 100,
+ relatedIntegrations: undefined,
+ requiredFields: undefined,
riskScore: 80,
riskScoreMapping: [],
ruleNameOverride: undefined,
outputIndex: 'output-1',
name: 'Query with a rule id',
+ setup: undefined,
severity: 'high',
severityMapping: [],
tags: [],
@@ -85,11 +88,14 @@ export const getCreateMlRulesOptionsMock = (): CreateRulesOptions => ({
index: ['index-123'],
interval: '5m',
maxSignals: 100,
+ relatedIntegrations: undefined,
+ requiredFields: undefined,
riskScore: 80,
riskScoreMapping: [],
ruleNameOverride: undefined,
outputIndex: 'output-1',
name: 'Machine Learning Job',
+ setup: undefined,
severity: 'high',
severityMapping: [],
tags: [],
@@ -141,12 +147,15 @@ export const getCreateThreatMatchRulesOptionsMock = (): CreateRulesOptions => ({
outputIndex: 'output-1',
query: 'user.name: root or user.name: admin',
references: ['http://www.example.com'],
+ relatedIntegrations: undefined,
+ requiredFields: undefined,
riskScore: 80,
riskScoreMapping: [],
ruleId: 'rule-1',
ruleNameOverride: undefined,
rulesClient: rulesClientMock.create(),
savedId: 'savedId-123',
+ setup: undefined,
severity: 'high',
severityMapping: [],
tags: [],
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts
index 24017adc20626..726964cdf3596 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts
@@ -46,11 +46,14 @@ export const createRules = async ({
index,
interval,
maxSignals,
+ relatedIntegrations,
+ requiredFields,
riskScore,
riskScoreMapping,
ruleNameOverride,
outputIndex,
name,
+ setup,
severity,
severityMapping,
tags,
@@ -109,9 +112,12 @@ export const createRules = async ({
: undefined,
filters,
maxSignals,
+ relatedIntegrations,
+ requiredFields,
riskScore,
riskScoreMapping,
ruleNameOverride,
+ setup,
severity,
severityMapping,
threat,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.test.ts
index 04d8e66a076fb..cab22e136f529 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.test.ts
@@ -6,6 +6,8 @@
*/
import uuid from 'uuid';
+import { SanitizedRule } from '@kbn/alerting-plugin/common';
+import { RuleParams } from '../schemas/rule_schemas';
import { duplicateRule } from './duplicate_rule';
jest.mock('uuid', () => ({
@@ -13,120 +15,287 @@ jest.mock('uuid', () => ({
}));
describe('duplicateRule', () => {
- it('should return a copy of rule with new ruleId', () => {
- (uuid.v4 as jest.Mock).mockReturnValue('newId');
-
- expect(
- duplicateRule({
- id: 'oldTestRuleId',
- notifyWhen: 'onActiveAlert',
- name: 'test',
- tags: ['test'],
- alertTypeId: 'siem.signals',
- consumer: 'siem',
- params: {
- savedId: undefined,
- author: [],
- description: 'test',
- ruleId: 'oldTestRuleId',
- falsePositives: [],
- from: 'now-360s',
- immutable: false,
- license: '',
- outputIndex: '.siem-signals-default',
- meta: undefined,
- maxSignals: 100,
- riskScore: 42,
- riskScoreMapping: [],
- severity: 'low',
- severityMapping: [],
- threat: [],
- to: 'now',
- references: [],
- version: 1,
- exceptionsList: [],
- type: 'query',
- language: 'kuery',
- index: [],
- query: 'process.args : "chmod"',
- filters: [],
- buildingBlockType: undefined,
- namespace: undefined,
- note: undefined,
- timelineId: undefined,
- timelineTitle: undefined,
- ruleNameOverride: undefined,
- timestampOverride: undefined,
- },
- schedule: {
- interval: '5m',
- },
+ const createTestRule = (): SanitizedRule => ({
+ id: 'some id',
+ notifyWhen: 'onActiveAlert',
+ name: 'Some rule',
+ tags: ['some tag'],
+ alertTypeId: 'siem.queryRule',
+ consumer: 'siem',
+ params: {
+ savedId: undefined,
+ author: [],
+ description: 'Some description.',
+ ruleId: 'some ruleId',
+ falsePositives: [],
+ from: 'now-360s',
+ immutable: false,
+ license: '',
+ outputIndex: '.siem-signals-default',
+ meta: undefined,
+ maxSignals: 100,
+ relatedIntegrations: [],
+ requiredFields: [],
+ riskScore: 42,
+ riskScoreMapping: [],
+ severity: 'low',
+ severityMapping: [],
+ setup: 'Some setup guide.',
+ threat: [],
+ to: 'now',
+ references: [],
+ version: 1,
+ exceptionsList: [],
+ type: 'query',
+ language: 'kuery',
+ index: [],
+ query: 'process.args : "chmod"',
+ filters: [],
+ buildingBlockType: undefined,
+ namespace: undefined,
+ note: undefined,
+ timelineId: undefined,
+ timelineTitle: undefined,
+ ruleNameOverride: undefined,
+ timestampOverride: undefined,
+ },
+ schedule: {
+ interval: '5m',
+ },
+ enabled: false,
+ actions: [],
+ throttle: null,
+ apiKeyOwner: 'kibana',
+ createdBy: 'kibana',
+ updatedBy: 'kibana',
+ muteAll: false,
+ mutedInstanceIds: [],
+ updatedAt: new Date(2021, 0),
+ createdAt: new Date(2021, 0),
+ scheduledTaskId: undefined,
+ executionStatus: {
+ lastExecutionDate: new Date(2021, 0),
+ status: 'ok',
+ },
+ });
+
+ beforeAll(() => {
+ (uuid.v4 as jest.Mock).mockReturnValue('new ruleId');
+ });
+
+ afterAll(() => {
+ jest.clearAllMocks();
+ });
+
+ it('returns an object with fields copied from a given rule', () => {
+ const rule = createTestRule();
+ const result = duplicateRule(rule);
+
+ expect(result).toEqual({
+ name: expect.anything(), // covered in a separate test
+ params: {
+ ...rule.params,
+ ruleId: expect.anything(), // covered in a separate test
+ },
+ tags: rule.tags,
+ alertTypeId: rule.alertTypeId,
+ consumer: rule.consumer,
+ schedule: rule.schedule,
+ actions: rule.actions,
+ throttle: null, // TODO: fix?
+ notifyWhen: null, // TODO: fix?
+ enabled: false, // covered in a separate test
+ });
+ });
+
+ it('appends [Duplicate] to the name', () => {
+ const rule = createTestRule();
+ rule.name = 'PowerShell Keylogging Script';
+ const result = duplicateRule(rule);
+
+ expect(result).toEqual(
+ expect.objectContaining({
+ name: 'PowerShell Keylogging Script [Duplicate]',
+ })
+ );
+ });
+
+ it('generates a new ruleId', () => {
+ const rule = createTestRule();
+ const result = duplicateRule(rule);
+
+ expect(result).toEqual(
+ expect.objectContaining({
+ params: expect.objectContaining({
+ ruleId: 'new ruleId',
+ }),
+ })
+ );
+ });
+
+ it('makes sure the duplicated rule is disabled', () => {
+ const rule = createTestRule();
+ rule.enabled = true;
+ const result = duplicateRule(rule);
+
+ expect(result).toEqual(
+ expect.objectContaining({
enabled: false,
- actions: [],
- throttle: null,
- apiKeyOwner: 'kibana',
- createdBy: 'kibana',
- updatedBy: 'kibana',
- muteAll: false,
- mutedInstanceIds: [],
- updatedAt: new Date(2021, 0),
- createdAt: new Date(2021, 0),
- scheduledTaskId: undefined,
- executionStatus: {
- lastExecutionDate: new Date(2021, 0),
- status: 'ok',
- },
})
- ).toMatchInlineSnapshot(`
- Object {
- "actions": Array [],
- "alertTypeId": "siem.queryRule",
- "consumer": "siem",
- "enabled": false,
- "name": "test [Duplicate]",
- "notifyWhen": null,
- "params": Object {
- "author": Array [],
- "buildingBlockType": undefined,
- "description": "test",
- "exceptionsList": Array [],
- "falsePositives": Array [],
- "filters": Array [],
- "from": "now-360s",
- "immutable": false,
- "index": Array [],
- "language": "kuery",
- "license": "",
- "maxSignals": 100,
- "meta": undefined,
- "namespace": undefined,
- "note": undefined,
- "outputIndex": ".siem-signals-default",
- "query": "process.args : \\"chmod\\"",
- "references": Array [],
- "riskScore": 42,
- "riskScoreMapping": Array [],
- "ruleId": "newId",
- "ruleNameOverride": undefined,
- "savedId": undefined,
- "severity": "low",
- "severityMapping": Array [],
- "threat": Array [],
- "timelineId": undefined,
- "timelineTitle": undefined,
- "timestampOverride": undefined,
- "to": "now",
- "type": "query",
- "version": 1,
+ );
+ });
+
+ describe('when duplicating a prebuilt (immutable) rule', () => {
+ const createPrebuiltRule = () => {
+ const rule = createTestRule();
+ rule.params.immutable = true;
+ return rule;
+ };
+
+ it('transforms it to a custom (mutable) rule', () => {
+ const rule = createPrebuiltRule();
+ const result = duplicateRule(rule);
+
+ expect(result).toEqual(
+ expect.objectContaining({
+ params: expect.objectContaining({
+ immutable: false,
+ }),
+ })
+ );
+ });
+
+ it('resets related integrations to an empty array', () => {
+ const rule = createPrebuiltRule();
+ rule.params.relatedIntegrations = [
+ {
+ package: 'aws',
+ version: '~1.2.3',
+ integration: 'route53',
},
- "schedule": Object {
- "interval": "5m",
+ ];
+
+ const result = duplicateRule(rule);
+
+ expect(result).toEqual(
+ expect.objectContaining({
+ params: expect.objectContaining({
+ relatedIntegrations: [],
+ }),
+ })
+ );
+ });
+
+ it('resets required fields to an empty array', () => {
+ const rule = createPrebuiltRule();
+ rule.params.requiredFields = [
+ {
+ name: 'event.action',
+ type: 'keyword',
+ ecs: true,
},
- "tags": Array [
- "test",
- ],
- "throttle": null,
- }
- `);
+ ];
+
+ const result = duplicateRule(rule);
+
+ expect(result).toEqual(
+ expect.objectContaining({
+ params: expect.objectContaining({
+ requiredFields: [],
+ }),
+ })
+ );
+ });
+
+ it('resets setup guide to an empty string', () => {
+ const rule = createPrebuiltRule();
+ rule.params.setup = `## Config\n\nThe 'Audit Detailed File Share' audit policy must be configured...`;
+ const result = duplicateRule(rule);
+
+ expect(result).toEqual(
+ expect.objectContaining({
+ params: expect.objectContaining({
+ setup: '',
+ }),
+ })
+ );
+ });
+ });
+
+ describe('when duplicating a custom (mutable) rule', () => {
+ const createCustomRule = () => {
+ const rule = createTestRule();
+ rule.params.immutable = false;
+ return rule;
+ };
+
+ it('keeps it custom', () => {
+ const rule = createCustomRule();
+ const result = duplicateRule(rule);
+
+ expect(result).toEqual(
+ expect.objectContaining({
+ params: expect.objectContaining({
+ immutable: false,
+ }),
+ })
+ );
+ });
+
+ it('copies related integrations as is', () => {
+ const rule = createCustomRule();
+ rule.params.relatedIntegrations = [
+ {
+ package: 'aws',
+ version: '~1.2.3',
+ integration: 'route53',
+ },
+ ];
+
+ const result = duplicateRule(rule);
+
+ expect(result).toEqual(
+ expect.objectContaining({
+ params: expect.objectContaining({
+ relatedIntegrations: rule.params.relatedIntegrations,
+ }),
+ })
+ );
+ });
+
+ it('copies required fields as is', () => {
+ const rule = createCustomRule();
+ rule.params.requiredFields = [
+ {
+ name: 'event.action',
+ type: 'keyword',
+ ecs: true,
+ },
+ ];
+
+ const result = duplicateRule(rule);
+
+ expect(result).toEqual(
+ expect.objectContaining({
+ params: expect.objectContaining({
+ requiredFields: rule.params.requiredFields,
+ }),
+ })
+ );
+ });
+
+ it('copies setup guide as is', () => {
+ const rule = createCustomRule();
+ rule.params.setup = `## Config\n\nThe 'Audit Detailed File Share' audit policy must be configured...`;
+ const result = duplicateRule(rule);
+
+ expect(result).toEqual(
+ expect.objectContaining({
+ params: expect.objectContaining({
+ setup: rule.params.setup,
+ }),
+ })
+ );
+ });
});
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.ts
index 4ef21d0450517..81af1533498ee 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.ts
@@ -22,7 +22,16 @@ const DUPLICATE_TITLE = i18n.translate(
);
export const duplicateRule = (rule: SanitizedRule): InternalRuleCreate => {
- const newRuleId = uuid.v4();
+ // Generate a new static ruleId
+ const ruleId = uuid.v4();
+
+ // If it's a prebuilt rule, reset Related Integrations, Required Fields and Setup Guide.
+ // We do this because for now we don't allow the users to edit these fields for custom rules.
+ const isPrebuilt = rule.params.immutable;
+ const relatedIntegrations = isPrebuilt ? [] : rule.params.relatedIntegrations;
+ const requiredFields = isPrebuilt ? [] : rule.params.requiredFields;
+ const setup = isPrebuilt ? '' : rule.params.setup;
+
return {
name: `${rule.name} [${DUPLICATE_TITLE}]`,
tags: rule.tags,
@@ -31,7 +40,10 @@ export const duplicateRule = (rule: SanitizedRule): InternalRuleCrea
params: {
...rule.params,
immutable: false,
- ruleId: newRuleId,
+ ruleId,
+ relatedIntegrations,
+ requiredFields,
+ setup,
},
schedule: rule.schedule,
enabled: false,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts
index de80a8ba8c26b..68fad65a8ff7e 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts
@@ -85,6 +85,9 @@ describe('getExportAll', () => {
name: 'Detect Root/Admin Users',
query: 'user.name: root or user.name: admin',
references: ['http://example.com', 'https://example.com'],
+ related_integrations: [],
+ required_fields: [],
+ setup: '',
timeline_id: 'some-timeline-id',
timeline_title: 'some-timeline-title',
meta: { someMeta: 'someField' },
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts
index f297f375dda0b..e31c1444cd9fc 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts
@@ -82,6 +82,9 @@ describe('get_export_by_object_ids', () => {
name: 'Detect Root/Admin Users',
query: 'user.name: root or user.name: admin',
references: ['http://example.com', 'https://example.com'],
+ related_integrations: [],
+ required_fields: [],
+ setup: '',
timeline_id: 'some-timeline-id',
timeline_title: 'some-timeline-title',
meta: { someMeta: 'someField' },
@@ -191,6 +194,9 @@ describe('get_export_by_object_ids', () => {
name: 'Detect Root/Admin Users',
query: 'user.name: root or user.name: admin',
references: ['http://example.com', 'https://example.com'],
+ related_integrations: [],
+ required_fields: [],
+ setup: '',
timeline_id: 'some-timeline-id',
timeline_title: 'some-timeline-title',
meta: { someMeta: 'someField' },
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts
index bffa0bc39eb91..1ef4f14b17b6b 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts
@@ -39,10 +39,13 @@ export const installPrepackagedRules = (
index,
interval,
max_signals: maxSignals,
+ related_integrations: relatedIntegrations,
+ required_fields: requiredFields,
risk_score: riskScore,
risk_score_mapping: riskScoreMapping,
rule_name_override: ruleNameOverride,
name,
+ setup,
severity,
severity_mapping: severityMapping,
tags,
@@ -95,10 +98,13 @@ export const installPrepackagedRules = (
index,
interval,
maxSignals,
+ relatedIntegrations,
+ requiredFields,
riskScore,
riskScoreMapping,
ruleNameOverride,
name,
+ setup,
severity,
severityMapping,
tags,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts
index ad2443b34fa95..e5f87b7cdb2e2 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts
@@ -54,11 +54,14 @@ export const patchRules = async ({
index,
interval,
maxSignals,
+ relatedIntegrations,
+ requiredFields,
riskScore,
riskScoreMapping,
ruleNameOverride,
rule,
name,
+ setup,
severity,
severityMapping,
tags,
@@ -108,10 +111,13 @@ export const patchRules = async ({
index,
interval,
maxSignals,
+ relatedIntegrations,
+ requiredFields,
riskScore,
riskScoreMapping,
ruleNameOverride,
name,
+ setup,
severity,
severityMapping,
tags,
@@ -158,9 +164,12 @@ export const patchRules = async ({
filters,
index,
maxSignals,
+ relatedIntegrations,
+ requiredFields,
riskScore,
riskScoreMapping,
ruleNameOverride,
+ setup,
severity,
severityMapping,
threat,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts
index 8b560d0edea0f..eeb0e88e53d47 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts
@@ -93,6 +93,9 @@ import {
RuleNameOverrideOrUndefined,
EventCategoryOverrideOrUndefined,
NamespaceOrUndefined,
+ RelatedIntegrationArray,
+ RequiredFieldArray,
+ SetupGuide,
} from '../../../../common/detection_engine/schemas/common';
import { PartialFilter } from '../types';
@@ -161,11 +164,14 @@ export interface CreateRulesOptions {
interval: Interval;
license: LicenseOrUndefined;
maxSignals: MaxSignals;
+ relatedIntegrations: RelatedIntegrationArray | undefined;
+ requiredFields: RequiredFieldArray | undefined;
riskScore: RiskScore;
riskScoreMapping: RiskScoreMapping;
ruleNameOverride: RuleNameOverrideOrUndefined;
outputIndex: OutputIndex;
name: Name;
+ setup: SetupGuide | undefined;
severity: Severity;
severityMapping: SeverityMapping;
tags: Tags;
@@ -225,11 +231,14 @@ interface PatchRulesFieldsOptions {
interval: IntervalOrUndefined;
license: LicenseOrUndefined;
maxSignals: MaxSignalsOrUndefined;
+ relatedIntegrations: RelatedIntegrationArray | undefined;
+ requiredFields: RequiredFieldArray | undefined;
riskScore: RiskScoreOrUndefined;
riskScoreMapping: RiskScoreMappingOrUndefined;
ruleNameOverride: RuleNameOverrideOrUndefined;
outputIndex: OutputIndexOrUndefined;
name: NameOrUndefined;
+ setup: SetupGuide | undefined;
severity: SeverityOrUndefined;
severityMapping: SeverityMappingOrUndefined;
tags: TagsOrUndefined;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts
index ad35e11d35668..079af5b82d608 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts
@@ -83,10 +83,13 @@ export const createPromises = (
index,
interval,
max_signals: maxSignals,
+ related_integrations: relatedIntegrations,
+ required_fields: requiredFields,
risk_score: riskScore,
risk_score_mapping: riskScoreMapping,
rule_name_override: ruleNameOverride,
name,
+ setup,
severity,
severity_mapping: severityMapping,
tags,
@@ -169,10 +172,13 @@ export const createPromises = (
index,
interval,
maxSignals,
+ relatedIntegrations,
+ requiredFields,
riskScore,
riskScoreMapping,
ruleNameOverride,
name,
+ setup,
severity,
severityMapping,
tags,
@@ -220,10 +226,13 @@ export const createPromises = (
index,
interval,
maxSignals,
+ relatedIntegrations,
+ requiredFields,
riskScore,
riskScoreMapping,
ruleNameOverride,
name,
+ setup,
severity,
severityMapping,
tags,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts
index ba65b76f01c4a..7c981a5481ff9 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts
@@ -54,9 +54,12 @@ export const updateRules = async ({
timelineTitle: ruleUpdate.timeline_title,
meta: ruleUpdate.meta,
maxSignals: ruleUpdate.max_signals ?? DEFAULT_MAX_SIGNALS,
+ relatedIntegrations: existingRule.params.relatedIntegrations,
+ requiredFields: existingRule.params.requiredFields,
riskScore: ruleUpdate.risk_score,
riskScoreMapping: ruleUpdate.risk_score_mapping ?? [],
ruleNameOverride: ruleUpdate.rule_name_override,
+ setup: existingRule.params.setup,
severity: ruleUpdate.severity,
severityMapping: ruleUpdate.severity_mapping ?? [],
threat: ruleUpdate.threat ?? [],
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts
index 0952da3182e01..43ac38f447abc 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts
@@ -127,10 +127,13 @@ describe('utils', () => {
index: undefined,
interval: undefined,
maxSignals: undefined,
+ relatedIntegrations: undefined,
+ requiredFields: undefined,
riskScore: undefined,
riskScoreMapping: undefined,
ruleNameOverride: undefined,
name: undefined,
+ setup: undefined,
severity: undefined,
severityMapping: undefined,
tags: undefined,
@@ -179,10 +182,13 @@ describe('utils', () => {
index: undefined,
interval: undefined,
maxSignals: undefined,
+ relatedIntegrations: undefined,
+ requiredFields: undefined,
riskScore: undefined,
riskScoreMapping: undefined,
ruleNameOverride: undefined,
name: undefined,
+ setup: undefined,
severity: undefined,
severityMapping: undefined,
tags: undefined,
@@ -231,10 +237,13 @@ describe('utils', () => {
index: undefined,
interval: undefined,
maxSignals: undefined,
+ relatedIntegrations: undefined,
+ requiredFields: undefined,
riskScore: undefined,
riskScoreMapping: undefined,
ruleNameOverride: undefined,
name: undefined,
+ setup: undefined,
severity: undefined,
severityMapping: undefined,
tags: undefined,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts
index dd25676a758e4..4ac138e1629f3 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts
@@ -56,7 +56,10 @@ import {
TimestampOverrideOrUndefined,
EventCategoryOverrideOrUndefined,
NamespaceOrUndefined,
-} from '../../../../common/detection_engine/schemas/common/schemas';
+ RelatedIntegrationArray,
+ RequiredFieldArray,
+ SetupGuide,
+} from '../../../../common/detection_engine/schemas/common';
import { PartialFilter } from '../types';
import { RuleParams } from '../schemas/rule_schemas';
import {
@@ -107,11 +110,14 @@ export interface UpdateProperties {
index: IndexOrUndefined;
interval: IntervalOrUndefined;
maxSignals: MaxSignalsOrUndefined;
+ relatedIntegrations: RelatedIntegrationArray | undefined;
+ requiredFields: RequiredFieldArray | undefined;
riskScore: RiskScoreOrUndefined;
riskScoreMapping: RiskScoreMappingOrUndefined;
ruleNameOverride: RuleNameOverrideOrUndefined;
outputIndex: OutputIndexOrUndefined;
name: NameOrUndefined;
+ setup: SetupGuide | undefined;
severity: SeverityOrUndefined;
severityMapping: SeverityMappingOrUndefined;
tags: TagsOrUndefined;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts
index fd80bec1f6ad9..356436058b55c 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts
@@ -161,6 +161,9 @@ export const convertCreateAPIToInternalSchema = (
note: input.note,
version: input.version ?? 1,
exceptionsList: input.exceptions_list ?? [],
+ relatedIntegrations: [],
+ requiredFields: [],
+ setup: '',
...typeSpecificParams,
},
schedule: { interval: input.interval ?? '5m' },
@@ -276,6 +279,9 @@ export const commonParamsCamelToSnake = (params: BaseRuleParams) => {
version: params.version,
exceptions_list: params.exceptionsList,
immutable: params.immutable,
+ related_integrations: params.relatedIntegrations ?? [],
+ required_fields: params.requiredFields ?? [],
+ setup: params.setup ?? '',
};
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.mock.ts
index edaacf38d7712..9e3fa6a906da9 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.mock.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.mock.ts
@@ -51,6 +51,9 @@ const getBaseRuleParams = (): BaseRuleParams => {
threat: getThreatMock(),
version: 1,
exceptionsList: getListArrayMock(),
+ relatedIntegrations: [],
+ requiredFields: [],
+ setup: '',
};
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts
index 47e49e5f9c467..d1776136f6513 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts
@@ -72,7 +72,10 @@ import {
updatedByOrNull,
created_at,
updated_at,
-} from '../../../../common/detection_engine/schemas/common/schemas';
+ RelatedIntegrationArray,
+ RequiredFieldArray,
+ SetupGuide,
+} from '../../../../common/detection_engine/schemas/common';
import { SERVER_APP_ID } from '../../../../common/constants';
const nonEqlLanguages = t.keyof({ kuery: null, lucene: null });
@@ -105,6 +108,9 @@ export const baseRuleParams = t.exact(
references,
version,
exceptionsList: listArray,
+ relatedIntegrations: t.union([RelatedIntegrationArray, t.undefined]),
+ requiredFields: t.union([RequiredFieldArray, t.undefined]),
+ setup: t.union([SetupGuide, t.undefined]),
})
);
export type BaseRuleParams = t.TypeOf;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
index 9213d6c5b278c..03074b9560553 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
@@ -157,6 +157,9 @@ export const expectedRule = (): RulesSchema => {
timeline_id: 'some-timeline-id',
timeline_title: 'some-timeline-title',
exceptions_list: getListArrayMock(),
+ related_integrations: [],
+ required_fields: [],
+ setup: '',
};
};
@@ -624,6 +627,9 @@ export const sampleSignalHit = (): SignalHit => ({
rule_id: 'query-rule-id',
interval: '5m',
exceptions_list: getListArrayMock(),
+ related_integrations: [],
+ required_fields: [],
+ setup: '',
},
depth: 1,
},
@@ -685,6 +691,9 @@ export const sampleThresholdSignalHit = (): SignalHit => ({
rule_id: 'query-rule-id',
interval: '5m',
exceptions_list: getListArrayMock(),
+ related_integrations: [],
+ required_fields: [],
+ setup: '',
},
depth: 1,
},
diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts
index 7d32af43d1913..aff63d635c976 100644
--- a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts
+++ b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts
@@ -89,6 +89,9 @@ export default ({ getService }: FtrProviderContext) => {
name: 'Simple Rule Query',
query: 'user.name: root or user.name: admin',
references: [],
+ related_integrations: [],
+ required_fields: [],
+ setup: '',
severity: 'high',
severity_mapping: [],
updated_by: 'elastic',
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts
index 1b7e22fb21c57..966420c90b8d2 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts
@@ -171,6 +171,9 @@ export default ({ getService }: FtrProviderContext) => {
name: 'Simple Rule Query',
query: 'user.name: root or user.name: admin',
references: [],
+ related_integrations: [],
+ required_fields: [],
+ setup: '',
severity: 'high',
severity_mapping: [],
updated_by: 'elastic',
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/alerts_compatibility.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/alerts_compatibility.ts
index 865185387c57c..5382ba5fd18f4 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/alerts_compatibility.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/alerts_compatibility.ts
@@ -353,6 +353,9 @@ export default ({ getService }: FtrProviderContext) => {
language: 'kuery',
index: ['.siem-signals-*'],
query: '*:*',
+ related_integrations: [],
+ required_fields: [],
+ setup: '',
},
'kibana.alert.rule.actions': [],
'kibana.alert.rule.created_by': 'elastic',
@@ -518,6 +521,9 @@ export default ({ getService }: FtrProviderContext) => {
language: 'kuery',
index: ['.alerts-security.alerts-default'],
query: '*:*',
+ related_integrations: [],
+ required_fields: [],
+ setup: '',
},
'kibana.alert.rule.actions': [],
'kibana.alert.rule.created_by': 'elastic',
diff --git a/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts b/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts
index 98fdfa99cbd3c..81a169636605b 100644
--- a/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts
+++ b/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts
@@ -97,4 +97,7 @@ export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial =>
version: 1,
query: 'user.name: root or user.name: admin',
exceptions_list: [],
+ related_integrations: [],
+ required_fields: [],
+ setup: '',
});
diff --git a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts
index 30dc7eecb9256..ca8b04e66f3fc 100644
--- a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts
+++ b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts
@@ -26,11 +26,14 @@ export const getSimpleRuleOutput = (ruleId = 'rule-1', enabled = false): Partial
language: 'kuery',
output_index: '.siem-signals-default',
max_signals: 100,
+ related_integrations: [],
+ required_fields: [],
risk_score: 1,
risk_score_mapping: [],
name: 'Simple Rule Query',
query: 'user.name: root or user.name: admin',
references: [],
+ setup: '',
severity: 'high',
severity_mapping: [],
updated_by: 'elastic',
From 383239e77c165bfb77100a68c4a92c4ed7fb8fe2 Mon Sep 17 00:00:00 2001
From: Kfir Peled <61654899+kfirpeled@users.noreply.github.com>
Date: Sun, 22 May 2022 13:18:42 +0300
Subject: [PATCH 38/39] [Cloud Posture] Findings - Group by resource - Fixed
bug not showing results (#132529)
---
.../findings_by_resource_table.test.tsx | 30 ++++++++----
.../findings_by_resource_table.tsx | 48 +++++++++++++++----
.../use_findings_by_resource.ts | 34 +++++++++----
.../create_indices/latest_findings_mapping.ts | 26 +++++++---
4 files changed, 103 insertions(+), 35 deletions(-)
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.test.tsx
index a6b8f3b863401..9cc87d98e54f8 100644
--- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.test.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.test.tsx
@@ -21,14 +21,23 @@ import { TestProvider } from '../../../test/test_provider';
const chance = new Chance();
-const getFakeFindingsByResource = (): CspFindingsByResource => ({
- resource_id: chance.guid(),
- cis_sections: [chance.word(), chance.word()],
- failed_findings: {
- total: chance.integer(),
- normalized: chance.integer({ min: 0, max: 1 }),
- },
-});
+const getFakeFindingsByResource = (): CspFindingsByResource => {
+ const count = chance.integer();
+ const total = chance.integer() + count + 1;
+ const normalized = count / total;
+
+ return {
+ resource_id: chance.guid(),
+ resource_name: chance.word(),
+ resource_subtype: chance.word(),
+ cis_sections: [chance.word(), chance.word()],
+ failed_findings: {
+ count,
+ normalized,
+ total_findings: total,
+ },
+ };
+};
type TableProps = PropsOf;
@@ -74,8 +83,11 @@ describe('', () => {
);
expect(row).toBeInTheDocument();
expect(within(row).getByText(item.resource_id)).toBeInTheDocument();
+ if (item.resource_name) expect(within(row).getByText(item.resource_name)).toBeInTheDocument();
+ if (item.resource_subtype)
+ expect(within(row).getByText(item.resource_subtype)).toBeInTheDocument();
expect(within(row).getByText(item.cis_sections.join(', '))).toBeInTheDocument();
- expect(within(row).getByText(formatNumber(item.failed_findings.total))).toBeInTheDocument();
+ expect(within(row).getByText(formatNumber(item.failed_findings.count))).toBeInTheDocument();
expect(
within(row).getByText(new RegExp(numeral(item.failed_findings.normalized).format('0%')))
).toBeInTheDocument();
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.tsx
index 2e96306ad3a69..80da922225893 100644
--- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.tsx
@@ -9,12 +9,12 @@ import {
EuiEmptyPrompt,
EuiBasicTable,
EuiTextColor,
- EuiFlexGroup,
- EuiFlexItem,
type EuiTableFieldDataColumnType,
type CriteriaWithPagination,
type Pagination,
+ EuiToolTip,
} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import numeral from '@elastic/numeral';
import { Link, generatePath } from 'react-router-dom';
@@ -81,6 +81,26 @@ const columns: Array> = [
),
},
+ {
+ field: 'resource_subtype',
+ truncateText: true,
+ name: (
+
+ ),
+ },
+ {
+ field: 'resource_name',
+ truncateText: true,
+ name: (
+
+ ),
+ },
{
field: 'cis_sections',
truncateText: true,
@@ -102,14 +122,22 @@ const columns: Array> = [
/>
),
render: (failedFindings: CspFindingsByResource['failed_findings']) => (
-
-
- {formatNumber(failedFindings.total)}
-
-
- ({numeral(failedFindings.normalized).format('0%')})
-
-
+
+ <>
+
+ {formatNumber(failedFindings.count)}
+
+ ({numeral(failedFindings.normalized).format('0%')})
+ >
+
),
},
];
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/use_findings_by_resource.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/use_findings_by_resource.ts
index 880b2be868e6f..e2da77c8ba2a2 100644
--- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/use_findings_by_resource.ts
+++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/use_findings_by_resource.ts
@@ -14,7 +14,7 @@ import { showErrorToast } from '../latest_findings/use_latest_findings';
import type { FindingsBaseEsQuery, FindingsQueryResult } from '../types';
// a large number to probably get all the buckets
-const MAX_BUCKETS = 60 * 1000;
+const MAX_BUCKETS = 1000 * 1000;
interface UseResourceFindingsOptions extends FindingsBaseEsQuery {
from: NonNullable;
@@ -43,6 +43,8 @@ interface FindingsByResourceAggs {
interface FindingsAggBucket extends estypes.AggregationsStringRareTermsBucketKeys {
failed_findings: estypes.AggregationsMultiBucketBase;
+ name: estypes.AggregationsMultiBucketAggregateBase;
+ subtype: estypes.AggregationsMultiBucketAggregateBase;
cis_sections: estypes.AggregationsMultiBucketAggregateBase;
}
@@ -57,10 +59,16 @@ export const getFindingsByResourceAggQuery = ({
query,
size: 0,
aggs: {
- resource_total: { cardinality: { field: 'resource.id.keyword' } },
+ resource_total: { cardinality: { field: 'resource.id' } },
resources: {
- terms: { field: 'resource.id.keyword', size: MAX_BUCKETS },
+ terms: { field: 'resource.id', size: MAX_BUCKETS },
aggs: {
+ name: {
+ terms: { field: 'resource.name', size: 1 },
+ },
+ subtype: {
+ terms: { field: 'resource.sub_type', size: 1 },
+ },
cis_sections: {
terms: { field: 'rule.section.keyword' },
},
@@ -117,16 +125,24 @@ export const useFindingsByResource = ({ index, query, from, size }: UseResourceF
);
};
-const createFindingsByResource = (bucket: FindingsAggBucket) => {
- if (!Array.isArray(bucket.cis_sections.buckets))
+const createFindingsByResource = (resource: FindingsAggBucket) => {
+ if (
+ !Array.isArray(resource.cis_sections.buckets) ||
+ !Array.isArray(resource.name.buckets) ||
+ !Array.isArray(resource.subtype.buckets)
+ )
throw new Error('expected buckets to be an array');
return {
- resource_id: bucket.key,
- cis_sections: bucket.cis_sections.buckets.map((v) => v.key),
+ resource_id: resource.key,
+ resource_name: resource.name.buckets.map((v) => v.key).at(0),
+ resource_subtype: resource.subtype.buckets.map((v) => v.key).at(0),
+ cis_sections: resource.cis_sections.buckets.map((v) => v.key),
failed_findings: {
- total: bucket.failed_findings.doc_count,
- normalized: bucket.doc_count > 0 ? bucket.failed_findings.doc_count / bucket.doc_count : 0,
+ count: resource.failed_findings.doc_count,
+ normalized:
+ resource.doc_count > 0 ? resource.failed_findings.doc_count / resource.doc_count : 0,
+ total_findings: resource.doc_count,
},
};
};
diff --git a/x-pack/plugins/cloud_security_posture/server/create_indices/latest_findings_mapping.ts b/x-pack/plugins/cloud_security_posture/server/create_indices/latest_findings_mapping.ts
index 9ebe4c3cf4038..57305fd2df7c4 100644
--- a/x-pack/plugins/cloud_security_posture/server/create_indices/latest_findings_mapping.ts
+++ b/x-pack/plugins/cloud_security_posture/server/create_indices/latest_findings_mapping.ts
@@ -50,20 +50,32 @@ export const latestFindingsMapping: MappingTypeMapping = {
properties: {
type: {
type: 'keyword',
- ignore_above: 256,
+ ignore_above: 1024,
},
id: {
- type: 'text',
+ type: 'keyword',
+ ignore_above: 1024,
+ fields: {
+ text: {
+ type: 'text',
+ },
+ },
},
name: {
- type: 'text',
+ type: 'keyword',
+ ignore_above: 1024,
+ fields: {
+ text: {
+ type: 'text',
+ },
+ },
},
sub_type: {
- type: 'text',
+ ignore_above: 1024,
+ type: 'keyword',
fields: {
- keyword: {
- ignore_above: 1024,
- type: 'keyword',
+ text: {
+ type: 'text',
},
},
},
From fbaf0588d0ed72ba5f1f405252b93bb6584333f8 Mon Sep 17 00:00:00 2001
From: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com>
Date: Sun, 22 May 2022 17:14:23 -0700
Subject: [PATCH 39/39] [RAM] Add shareable rules list (#132437)
* Shareable rules list
* Hide snooze panel in rules list
* Address comments and added tests
* Fix tests
* Fix tests
* Fix lint
* Address design comments and fix tests
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../common/experimental_features.ts | 4 +-
.../hooks/use_load_rule_aggregations.test.ts | 111 +++
.../hooks/use_load_rule_aggregations.ts | 83 ++
.../application/hooks/use_load_rules.test.ts | 378 +++++++
.../application/hooks/use_load_rules.ts | 185 ++++
.../application/hooks/use_load_tags.test.ts | 54 +
.../public/application/hooks/use_load_tags.ts | 45 +
.../rule_event_log_list_sandbox.tsx | 3 +-
.../rules_list_sandbox.tsx | 16 +
.../shareable_components_sandbox.tsx | 2 +
.../application/lib/rule_api/aggregate.ts | 20 +-
.../public/application/lib/rule_api/index.ts | 2 +
.../public/application/lib/rule_api/rules.ts | 24 +-
.../public/application/sections/index.tsx | 3 +
.../components/action_type_filter.tsx | 89 +-
.../rule_execution_status_filter.tsx | 108 +-
.../components/rule_status_dropdown.tsx | 100 +-
.../components/rule_status_filter.test.tsx | 14 +-
.../components/rule_status_filter.tsx | 33 +-
.../rules_list/components/rule_tag_filter.tsx | 50 +-
.../rules_list/components/rules_list.test.tsx | 90 +-
.../rules_list/components/rules_list.tsx | 941 +++---------------
.../rules_list_auto_refresh.test.tsx | 87 ++
.../components/rules_list_auto_refresh.tsx | 122 +++
.../components/rules_list_notify_badge.tsx | 224 +++++
.../components/rules_list_table.tsx | 724 ++++++++++++++
.../rules_list/components/type_filter.tsx | 102 +-
.../public/common/get_rules_list.tsx | 13 +
.../triggers_actions_ui/public/mocks.ts | 4 +
.../triggers_actions_ui/public/plugin.ts | 5 +
.../apps/triggers_actions_ui/index.ts | 1 +
.../triggers_actions_ui/rule_tag_filter.ts | 20 -
.../apps/triggers_actions_ui/rules_list.ts | 34 +
33 files changed, 2624 insertions(+), 1067 deletions(-)
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations.test.ts
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations.ts
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules.test.ts
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules.ts
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_tags.test.ts
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_tags.ts
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rules_list_sandbox.tsx
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_auto_refresh.test.tsx
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_auto_refresh.tsx
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_notify_badge.tsx
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table.tsx
create mode 100644 x-pack/plugins/triggers_actions_ui/public/common/get_rules_list.tsx
create mode 100644 x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_list.ts
diff --git a/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts b/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts
index 33f5fdc44afcd..3265469bea640 100644
--- a/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts
+++ b/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts
@@ -15,8 +15,8 @@ export const allowedExperimentalValues = Object.freeze({
rulesListDatagrid: true,
internalAlertsTable: false,
internalShareableComponentsSandbox: false,
- ruleTagFilter: false,
- ruleStatusFilter: false,
+ ruleTagFilter: true,
+ ruleStatusFilter: true,
rulesDetailLogs: true,
});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations.test.ts
new file mode 100644
index 0000000000000..b00101da6be83
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations.test.ts
@@ -0,0 +1,111 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { renderHook, act } from '@testing-library/react-hooks';
+import { useLoadRuleAggregations } from './use_load_rule_aggregations';
+import { RuleStatus } from '../../types';
+
+const MOCK_TAGS = ['a', 'b', 'c'];
+
+const MOCK_AGGS = {
+ ruleEnabledStatus: { enabled: 2, disabled: 0 },
+ ruleExecutionStatus: { ok: 1, active: 2, error: 3, pending: 4, unknown: 5, warning: 6 },
+ ruleMutedStatus: { muted: 0, unmuted: 2 },
+ ruleTags: MOCK_TAGS,
+};
+
+jest.mock('../lib/rule_api', () => ({
+ loadRuleAggregations: jest.fn(),
+}));
+
+const { loadRuleAggregations } = jest.requireMock('../lib/rule_api');
+
+const onError = jest.fn();
+
+describe('useLoadRuleAggregations', () => {
+ beforeEach(() => {
+ loadRuleAggregations.mockResolvedValue(MOCK_AGGS);
+ jest.clearAllMocks();
+ });
+
+ it('should call loadRuleAggregations API and handle result', async () => {
+ const params = {
+ searchText: '',
+ typesFilter: [],
+ actionTypesFilter: [],
+ ruleExecutionStatusesFilter: [],
+ ruleStatusesFilter: [],
+ tagsFilter: [],
+ };
+
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useLoadRuleAggregations({
+ ...params,
+ onError,
+ })
+ );
+
+ await act(async () => {
+ result.current.loadRuleAggregations();
+ await waitForNextUpdate();
+ });
+
+ expect(loadRuleAggregations).toBeCalledWith(expect.objectContaining(params));
+ expect(result.current.rulesStatusesTotal).toEqual(MOCK_AGGS.ruleExecutionStatus);
+ });
+
+ it('should call loadRuleAggregation API with params and handle result', async () => {
+ const params = {
+ searchText: 'test',
+ typesFilter: ['type1', 'type2'],
+ actionTypesFilter: ['action1', 'action2'],
+ ruleExecutionStatusesFilter: ['status1', 'status2'],
+ ruleStatusesFilter: ['enabled', 'snoozed'] as RuleStatus[],
+ tagsFilter: ['tag1', 'tag2'],
+ };
+
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useLoadRuleAggregations({
+ ...params,
+ onError,
+ })
+ );
+
+ await act(async () => {
+ result.current.loadRuleAggregations();
+ await waitForNextUpdate();
+ });
+
+ expect(loadRuleAggregations).toBeCalledWith(expect.objectContaining(params));
+ expect(result.current.rulesStatusesTotal).toEqual(MOCK_AGGS.ruleExecutionStatus);
+ });
+
+ it('should call onError if API fails', async () => {
+ loadRuleAggregations.mockRejectedValue('');
+ const params = {
+ searchText: '',
+ typesFilter: [],
+ actionTypesFilter: [],
+ ruleExecutionStatusesFilter: [],
+ ruleStatusesFilter: [],
+ tagsFilter: [],
+ };
+
+ const { result } = renderHook(() =>
+ useLoadRuleAggregations({
+ ...params,
+ onError,
+ })
+ );
+
+ await act(async () => {
+ result.current.loadRuleAggregations();
+ });
+
+ expect(onError).toBeCalled();
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations.ts
new file mode 100644
index 0000000000000..75f9e18ec2328
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_aggregations.ts
@@ -0,0 +1,83 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { useState, useCallback, useMemo } from 'react';
+import { RuleExecutionStatusValues } from '@kbn/alerting-plugin/common';
+import { loadRuleAggregations, LoadRuleAggregationsProps } from '../lib/rule_api';
+import { useKibana } from '../../common/lib/kibana';
+
+type UseLoadRuleAggregationsProps = Omit & {
+ onError: (message: string) => void;
+};
+
+export function useLoadRuleAggregations({
+ searchText,
+ typesFilter,
+ actionTypesFilter,
+ ruleExecutionStatusesFilter,
+ ruleStatusesFilter,
+ tagsFilter,
+ onError,
+}: UseLoadRuleAggregationsProps) {
+ const { http } = useKibana().services;
+
+ const [rulesStatusesTotal, setRulesStatusesTotal] = useState>(
+ RuleExecutionStatusValues.reduce>(
+ (prev: Record, status: string) => ({
+ ...prev,
+ [status]: 0,
+ }),
+ {}
+ )
+ );
+
+ const internalLoadRuleAggregations = useCallback(async () => {
+ try {
+ const rulesAggs = await loadRuleAggregations({
+ http,
+ searchText,
+ typesFilter,
+ actionTypesFilter,
+ ruleExecutionStatusesFilter,
+ ruleStatusesFilter,
+ tagsFilter,
+ });
+ if (rulesAggs?.ruleExecutionStatus) {
+ setRulesStatusesTotal(rulesAggs.ruleExecutionStatus);
+ }
+ } catch (e) {
+ onError(
+ i18n.translate(
+ 'xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleStatusInfoMessage',
+ {
+ defaultMessage: 'Unable to load rule status info',
+ }
+ )
+ );
+ }
+ }, [
+ http,
+ searchText,
+ typesFilter,
+ actionTypesFilter,
+ ruleExecutionStatusesFilter,
+ ruleStatusesFilter,
+ tagsFilter,
+ onError,
+ setRulesStatusesTotal,
+ ]);
+
+ return useMemo(
+ () => ({
+ loadRuleAggregations: internalLoadRuleAggregations,
+ rulesStatusesTotal,
+ setRulesStatusesTotal,
+ }),
+ [internalLoadRuleAggregations, rulesStatusesTotal, setRulesStatusesTotal]
+ );
+}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules.test.ts
new file mode 100644
index 0000000000000..a309beeca58aa
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules.test.ts
@@ -0,0 +1,378 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { renderHook, act } from '@testing-library/react-hooks';
+import { useLoadRules } from './use_load_rules';
+import {
+ RuleExecutionStatusErrorReasons,
+ RuleExecutionStatusWarningReasons,
+} from '@kbn/alerting-plugin/common';
+import { RuleStatus } from '../../types';
+
+jest.mock('../lib/rule_api', () => ({
+ loadRules: jest.fn(),
+}));
+
+const { loadRules } = jest.requireMock('../lib/rule_api');
+
+const onError = jest.fn();
+const onPage = jest.fn();
+
+const mockedRulesData = [
+ {
+ id: '1',
+ name: 'test rule',
+ tags: ['tag1'],
+ enabled: true,
+ ruleTypeId: 'test_rule_type',
+ schedule: { interval: '1s' },
+ actions: [],
+ params: { name: 'test rule type name' },
+ scheduledTaskId: null,
+ createdBy: null,
+ updatedBy: null,
+ apiKeyOwner: null,
+ throttle: '1m',
+ muteAll: false,
+ mutedInstanceIds: [],
+ executionStatus: {
+ status: 'active',
+ lastDuration: 500,
+ lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
+ error: null,
+ },
+ monitoring: {
+ execution: {
+ history: [
+ {
+ success: true,
+ duration: 1000000,
+ },
+ {
+ success: true,
+ duration: 200000,
+ },
+ {
+ success: false,
+ duration: 300000,
+ },
+ ],
+ calculated_metrics: {
+ success_ratio: 0.66,
+ p50: 200000,
+ p95: 300000,
+ p99: 300000,
+ },
+ },
+ },
+ },
+ {
+ id: '2',
+ name: 'test rule ok',
+ tags: ['tag1'],
+ enabled: true,
+ ruleTypeId: 'test_rule_type',
+ schedule: { interval: '5d' },
+ actions: [],
+ params: { name: 'test rule type name' },
+ scheduledTaskId: null,
+ createdBy: null,
+ updatedBy: null,
+ apiKeyOwner: null,
+ throttle: '1m',
+ muteAll: false,
+ mutedInstanceIds: [],
+ executionStatus: {
+ status: 'ok',
+ lastDuration: 61000,
+ lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
+ error: null,
+ },
+ monitoring: {
+ execution: {
+ history: [
+ {
+ success: true,
+ duration: 100000,
+ },
+ {
+ success: true,
+ duration: 500000,
+ },
+ ],
+ calculated_metrics: {
+ success_ratio: 1,
+ p50: 0,
+ p95: 100000,
+ p99: 500000,
+ },
+ },
+ },
+ },
+ {
+ id: '3',
+ name: 'test rule pending',
+ tags: ['tag1'],
+ enabled: true,
+ ruleTypeId: 'test_rule_type',
+ schedule: { interval: '5d' },
+ actions: [],
+ params: { name: 'test rule type name' },
+ scheduledTaskId: null,
+ createdBy: null,
+ updatedBy: null,
+ apiKeyOwner: null,
+ throttle: '1m',
+ muteAll: false,
+ mutedInstanceIds: [],
+ executionStatus: {
+ status: 'pending',
+ lastDuration: 30234,
+ lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
+ error: null,
+ },
+ monitoring: {
+ execution: {
+ history: [{ success: false, duration: 100 }],
+ calculated_metrics: {
+ success_ratio: 0,
+ },
+ },
+ },
+ },
+ {
+ id: '4',
+ name: 'test rule error',
+ tags: ['tag1'],
+ enabled: true,
+ ruleTypeId: 'test_rule_type',
+ schedule: { interval: '5d' },
+ actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }],
+ params: { name: 'test rule type name' },
+ scheduledTaskId: null,
+ createdBy: null,
+ updatedBy: null,
+ apiKeyOwner: null,
+ throttle: '1m',
+ muteAll: false,
+ mutedInstanceIds: [],
+ executionStatus: {
+ status: 'error',
+ lastDuration: 122000,
+ lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
+ error: {
+ reason: RuleExecutionStatusErrorReasons.Unknown,
+ message: 'test',
+ },
+ },
+ },
+ {
+ id: '5',
+ name: 'test rule license error',
+ tags: [],
+ enabled: true,
+ ruleTypeId: 'test_rule_type',
+ schedule: { interval: '5d' },
+ actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }],
+ params: { name: 'test rule type name' },
+ scheduledTaskId: null,
+ createdBy: null,
+ updatedBy: null,
+ apiKeyOwner: null,
+ throttle: '1m',
+ muteAll: false,
+ mutedInstanceIds: [],
+ executionStatus: {
+ status: 'error',
+ lastDuration: 500,
+ lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
+ error: {
+ reason: RuleExecutionStatusErrorReasons.License,
+ message: 'test',
+ },
+ },
+ },
+ {
+ id: '6',
+ name: 'test rule warning',
+ tags: [],
+ enabled: true,
+ ruleTypeId: 'test_rule_type',
+ schedule: { interval: '5d' },
+ actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }],
+ params: { name: 'test rule type name' },
+ scheduledTaskId: null,
+ createdBy: null,
+ updatedBy: null,
+ apiKeyOwner: null,
+ throttle: '1m',
+ muteAll: false,
+ mutedInstanceIds: [],
+ executionStatus: {
+ status: 'warning',
+ lastDuration: 500,
+ lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
+ warning: {
+ reason: RuleExecutionStatusWarningReasons.MAX_EXECUTABLE_ACTIONS,
+ message: 'test',
+ },
+ },
+ },
+];
+
+const MOCK_RULE_DATA = {
+ page: 1,
+ perPage: 10000,
+ total: 4,
+ data: mockedRulesData,
+};
+
+describe('useLoadRules', () => {
+ beforeEach(() => {
+ loadRules.mockResolvedValue(MOCK_RULE_DATA);
+ jest.clearAllMocks();
+ });
+
+ it('should call loadRules API and handle result', async () => {
+ const params = {
+ page: {
+ index: 0,
+ size: 25,
+ },
+ searchText: '',
+ typesFilter: [],
+ actionTypesFilter: [],
+ ruleExecutionStatusesFilter: [],
+ ruleStatusesFilter: [],
+ tagsFilter: [],
+ };
+
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useLoadRules({
+ ...params,
+ onPage,
+ onError,
+ })
+ );
+
+ expect(result.current.initialLoad).toBeTruthy();
+ expect(result.current.noData).toBeTruthy();
+ expect(result.current.rulesState.isLoading).toBeFalsy();
+
+ await act(async () => {
+ result.current.loadRules();
+ await waitForNextUpdate();
+ });
+
+ expect(result.current.initialLoad).toBeFalsy();
+ expect(result.current.noData).toBeFalsy();
+ expect(result.current.rulesState.isLoading).toBeFalsy();
+
+ expect(onPage).toBeCalledTimes(0);
+ expect(loadRules).toBeCalledWith(expect.objectContaining(params));
+ expect(result.current.rulesState.data).toEqual(expect.arrayContaining(MOCK_RULE_DATA.data));
+ expect(result.current.rulesState.totalItemCount).toEqual(MOCK_RULE_DATA.total);
+ });
+
+ it('should call loadRules API with params and handle result', async () => {
+ const params = {
+ page: {
+ index: 0,
+ size: 25,
+ },
+ searchText: 'test',
+ typesFilter: ['type1', 'type2'],
+ actionTypesFilter: ['action1', 'action2'],
+ ruleExecutionStatusesFilter: ['status1', 'status2'],
+ ruleStatusesFilter: ['enabled', 'snoozed'] as RuleStatus[],
+ tagsFilter: ['tag1', 'tag2'],
+ };
+
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useLoadRules({
+ ...params,
+ onPage,
+ onError,
+ })
+ );
+
+ await act(async () => {
+ result.current.loadRules();
+ await waitForNextUpdate();
+ });
+
+ expect(loadRules).toBeCalledWith(expect.objectContaining(params));
+ });
+
+ it('should reset the page if the data is fetched while paged', async () => {
+ loadRules.mockResolvedValue({
+ ...MOCK_RULE_DATA,
+ data: [],
+ });
+
+ const params = {
+ page: {
+ index: 1,
+ size: 25,
+ },
+ searchText: '',
+ typesFilter: [],
+ actionTypesFilter: [],
+ ruleExecutionStatusesFilter: [],
+ ruleStatusesFilter: [],
+ tagsFilter: [],
+ };
+
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useLoadRules({
+ ...params,
+ onPage,
+ onError,
+ })
+ );
+
+ await act(async () => {
+ result.current.loadRules();
+ await waitForNextUpdate();
+ });
+
+ expect(onPage).toHaveBeenCalledWith({
+ index: 0,
+ size: 25,
+ });
+ });
+
+ it('should call onError if API fails', async () => {
+ loadRules.mockRejectedValue('');
+ const params = {
+ page: {
+ index: 0,
+ size: 25,
+ },
+ searchText: '',
+ typesFilter: [],
+ actionTypesFilter: [],
+ ruleExecutionStatusesFilter: [],
+ ruleStatusesFilter: [],
+ tagsFilter: [],
+ };
+
+ const { result } = renderHook(() =>
+ useLoadRules({
+ ...params,
+ onPage,
+ onError,
+ })
+ );
+
+ await act(async () => {
+ result.current.loadRules();
+ });
+
+ expect(onError).toBeCalled();
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules.ts
new file mode 100644
index 0000000000000..4afdfd4f26a72
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rules.ts
@@ -0,0 +1,185 @@
+/*
+ * 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 { useMemo, useCallback, useReducer } from 'react';
+import { i18n } from '@kbn/i18n';
+import { isEmpty } from 'lodash';
+import { Rule, Pagination } from '../../types';
+import { loadRules, LoadRulesProps } from '../lib/rule_api';
+import { useKibana } from '../../common/lib/kibana';
+
+interface RuleState {
+ isLoading: boolean;
+ data: Rule[];
+ totalItemCount: number;
+}
+
+type UseLoadRulesProps = Omit & {
+ onPage: (pagination: Pagination) => void;
+ onError: (message: string) => void;
+};
+
+interface UseLoadRulesState {
+ rulesState: RuleState;
+ noData: boolean;
+ initialLoad: boolean;
+}
+
+enum ActionTypes {
+ SET_RULE_STATE = 'SET_RULE_STATE',
+ SET_LOADING = 'SET_LOADING',
+ SET_INITIAL_LOAD = 'SET_INITIAL_LOAD',
+ SET_NO_DATA = 'SET_NO_DATA',
+}
+
+interface Action {
+ type: ActionTypes;
+ payload: boolean | RuleState;
+}
+
+const initialState: UseLoadRulesState = {
+ rulesState: {
+ isLoading: false,
+ data: [],
+ totalItemCount: 0,
+ },
+ noData: true,
+ initialLoad: true,
+};
+
+const reducer = (state: UseLoadRulesState, action: Action) => {
+ const { type, payload } = action;
+ switch (type) {
+ case ActionTypes.SET_RULE_STATE:
+ return {
+ ...state,
+ rulesState: payload as RuleState,
+ };
+ case ActionTypes.SET_LOADING:
+ return {
+ ...state,
+ rulesState: {
+ ...state.rulesState,
+ isLoading: payload as boolean,
+ },
+ };
+ case ActionTypes.SET_INITIAL_LOAD:
+ return {
+ ...state,
+ initialLoad: payload as boolean,
+ };
+ case ActionTypes.SET_NO_DATA:
+ return {
+ ...state,
+ noData: payload as boolean,
+ };
+ default:
+ return state;
+ }
+};
+
+export function useLoadRules({
+ page,
+ searchText,
+ typesFilter,
+ actionTypesFilter,
+ ruleExecutionStatusesFilter,
+ ruleStatusesFilter,
+ tagsFilter,
+ sort,
+ onPage,
+ onError,
+}: UseLoadRulesProps) {
+ const { http } = useKibana().services;
+ const [state, dispatch] = useReducer(reducer, initialState);
+
+ const setRulesState = useCallback(
+ (rulesState: RuleState) => {
+ dispatch({
+ type: ActionTypes.SET_RULE_STATE,
+ payload: rulesState,
+ });
+ },
+ [dispatch]
+ );
+
+ const internalLoadRules = useCallback(async () => {
+ dispatch({ type: ActionTypes.SET_LOADING, payload: true });
+
+ try {
+ const rulesResponse = await loadRules({
+ http,
+ page,
+ searchText,
+ typesFilter,
+ actionTypesFilter,
+ ruleExecutionStatusesFilter,
+ ruleStatusesFilter,
+ tagsFilter,
+ sort,
+ });
+
+ dispatch({
+ type: ActionTypes.SET_RULE_STATE,
+ payload: {
+ isLoading: false,
+ data: rulesResponse.data,
+ totalItemCount: rulesResponse.total,
+ },
+ });
+
+ if (!rulesResponse.data?.length && page.index > 0) {
+ onPage({ ...page, index: 0 });
+ }
+
+ const isFilterApplied = !(
+ isEmpty(searchText) &&
+ isEmpty(typesFilter) &&
+ isEmpty(actionTypesFilter) &&
+ isEmpty(ruleExecutionStatusesFilter) &&
+ isEmpty(ruleStatusesFilter) &&
+ isEmpty(tagsFilter)
+ );
+
+ dispatch({
+ type: ActionTypes.SET_NO_DATA,
+ payload: rulesResponse.data.length === 0 && !isFilterApplied,
+ });
+ } catch (e) {
+ onError(
+ i18n.translate('xpack.triggersActionsUI.sections.rulesList.unableToLoadRulesMessage', {
+ defaultMessage: 'Unable to load rules',
+ })
+ );
+ dispatch({ type: ActionTypes.SET_LOADING, payload: false });
+ }
+ dispatch({ type: ActionTypes.SET_INITIAL_LOAD, payload: false });
+ }, [
+ http,
+ page,
+ searchText,
+ typesFilter,
+ actionTypesFilter,
+ ruleExecutionStatusesFilter,
+ ruleStatusesFilter,
+ tagsFilter,
+ sort,
+ dispatch,
+ onPage,
+ onError,
+ ]);
+
+ return useMemo(
+ () => ({
+ rulesState: state.rulesState,
+ noData: state.noData,
+ initialLoad: state.initialLoad,
+ loadRules: internalLoadRules,
+ setRulesState,
+ }),
+ [state, setRulesState, internalLoadRules]
+ );
+}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_tags.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_tags.test.ts
new file mode 100644
index 0000000000000..8973d869e0724
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_tags.test.ts
@@ -0,0 +1,54 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { renderHook, act } from '@testing-library/react-hooks';
+import { useLoadTags } from './use_load_tags';
+
+const MOCK_TAGS = ['a', 'b', 'c'];
+
+jest.mock('../lib/rule_api', () => ({
+ loadRuleTags: jest.fn(),
+}));
+
+const { loadRuleTags } = jest.requireMock('../lib/rule_api');
+
+const onError = jest.fn();
+
+describe('useLoadTags', () => {
+ beforeEach(() => {
+ loadRuleTags.mockResolvedValue({
+ ruleTags: MOCK_TAGS,
+ });
+ jest.clearAllMocks();
+ });
+
+ it('should call loadRuleTags API and handle result', async () => {
+ const { result, waitForNextUpdate } = renderHook(() => useLoadTags({ onError }));
+
+ await act(async () => {
+ result.current.loadTags();
+ await waitForNextUpdate();
+ });
+
+ expect(loadRuleTags).toBeCalled();
+ expect(result.current.tags).toEqual(MOCK_TAGS);
+ });
+
+ it('should call onError if API fails', async () => {
+ loadRuleTags.mockRejectedValue('');
+
+ const { result } = renderHook(() => useLoadTags({ onError }));
+
+ await act(async () => {
+ result.current.loadTags();
+ });
+
+ expect(loadRuleTags).toBeCalled();
+ expect(onError).toBeCalled();
+ expect(result.current.tags).toEqual([]);
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_tags.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_tags.ts
new file mode 100644
index 0000000000000..3357f43a012f1
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_tags.ts
@@ -0,0 +1,45 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { useState, useCallback, useMemo } from 'react';
+import { loadRuleTags } from '../lib/rule_api';
+import { useKibana } from '../../common/lib/kibana';
+
+interface UseLoadTagsProps {
+ onError: (message: string) => void;
+}
+
+export function useLoadTags(props: UseLoadTagsProps) {
+ const { onError } = props;
+ const { http } = useKibana().services;
+ const [tags, setTags] = useState([]);
+
+ const internalLoadTags = useCallback(async () => {
+ try {
+ const ruleTagsAggs = await loadRuleTags({ http });
+ if (ruleTagsAggs?.ruleTags) {
+ setTags(ruleTagsAggs.ruleTags);
+ }
+ } catch (e) {
+ onError(
+ i18n.translate('xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleTags', {
+ defaultMessage: 'Unable to load rule tags',
+ })
+ );
+ }
+ }, [http, setTags, onError]);
+
+ return useMemo(
+ () => ({
+ tags,
+ loadTags: internalLoadTags,
+ setTags,
+ }),
+ [tags, internalLoadTags, setTags]
+ );
+}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_event_log_list_sandbox.tsx b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_event_log_list_sandbox.tsx
index 4af95523dce29..ba45800e49bcb 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_event_log_list_sandbox.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_event_log_list_sandbox.tsx
@@ -5,6 +5,7 @@
* 2.0.
*/
+import React from 'react';
import { getRuleEventLogListLazy } from '../../../common/get_rule_event_log_list';
export const RuleEventLogListSandbox = () => {
@@ -39,5 +40,5 @@ export const RuleEventLogListSandbox = () => {
}),
};
- return getRuleEventLogListLazy(props);
+ return {getRuleEventLogListLazy(props)}
;
};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rules_list_sandbox.tsx b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rules_list_sandbox.tsx
new file mode 100644
index 0000000000000..7702b914cfd36
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rules_list_sandbox.tsx
@@ -0,0 +1,16 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React from 'react';
+import { getRulesListLazy } from '../../../common/get_rules_list';
+
+const style = {
+ flex: 1,
+};
+
+export const RulesListSandbox = () => {
+ return {getRulesListLazy()}
;
+};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx
index af5a05acdf19a..018f0a8794c33 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx
@@ -11,6 +11,7 @@ import { RuleTagFilterSandbox } from './rule_tag_filter_sandbox';
import { RuleStatusFilterSandbox } from './rule_status_filter_sandbox';
import { RuleTagBadgeSandbox } from './rule_tag_badge_sandbox';
import { RuleEventLogListSandbox } from './rule_event_log_list_sandbox';
+import { RulesListSandbox } from './rules_list_sandbox';
export const InternalShareableComponentsSandbox: React.FC<{}> = () => {
return (
@@ -19,6 +20,7 @@ export const InternalShareableComponentsSandbox: React.FC<{}> = () => {
+
>
);
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts
index 1df6177443657..5df7cfc374f89 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts
@@ -44,6 +44,16 @@ export async function loadRuleTags({ http }: { http: HttpSetup }): Promise {
+}: LoadRuleAggregationsProps): Promise {
const filters = mapFiltersToKql({
typesFilter,
actionTypesFilter,
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts
index d0e7728498c5b..64d6b18b7ca5c 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts
@@ -7,6 +7,7 @@
export { alertingFrameworkHealth } from './health';
export { mapFiltersToKql } from './map_filters_to_kql';
+export type { LoadRuleAggregationsProps } from './aggregate';
export { loadRuleAggregations, loadRuleTags } from './aggregate';
export { createRule } from './create';
export { deleteRules } from './delete';
@@ -17,6 +18,7 @@ export { loadRuleSummary } from './rule_summary';
export { muteAlertInstance } from './mute_alert';
export { muteRule, muteRules } from './mute';
export { loadRuleTypes } from './rule_types';
+export type { LoadRulesProps } from './rules';
export { loadRules } from './rules';
export { loadRuleState } from './state';
export type { LoadExecutionLogAggregationsProps } from './load_execution_log_aggregations';
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts
index 6e527989cc91f..3db1cb8b0214d 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts
@@ -11,6 +11,18 @@ import { Rule, Pagination, Sorting, RuleStatus } from '../../../types';
import { mapFiltersToKql } from './map_filters_to_kql';
import { transformRule } from './common_transformations';
+export interface LoadRulesProps {
+ http: HttpSetup;
+ page: Pagination;
+ searchText?: string;
+ typesFilter?: string[];
+ actionTypesFilter?: string[];
+ tagsFilter?: string[];
+ ruleExecutionStatusesFilter?: string[];
+ ruleStatusesFilter?: RuleStatus[];
+ sort?: Sorting;
+}
+
const rewriteResponseRes = (results: Array>): Rule[] => {
return results.map((item) => transformRule(item));
};
@@ -25,17 +37,7 @@ export async function loadRules({
ruleStatusesFilter,
tagsFilter,
sort = { field: 'name', direction: 'asc' },
-}: {
- http: HttpSetup;
- page: Pagination;
- searchText?: string;
- typesFilter?: string[];
- actionTypesFilter?: string[];
- tagsFilter?: string[];
- ruleExecutionStatusesFilter?: string[];
- ruleStatusesFilter?: RuleStatus[];
- sort?: Sorting;
-}): Promise<{
+}: LoadRulesProps): Promise<{
page: number;
perPage: number;
total: number;
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx
index 979630d2a5a99..bd2ef041535f3 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx
@@ -44,3 +44,6 @@ export const RuleTagBadge = suspendedComponentWithProps(
export const RuleEventLogList = suspendedComponentWithProps(
lazy(() => import('./rule_details/components/rule_event_log_list'))
);
+export const RulesList = suspendedComponentWithProps(
+ lazy(() => import('./rules_list/components/rules_list'))
+);
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/action_type_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/action_type_filter.tsx
index a136413d53e42..38d1a62de699a 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/action_type_filter.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/action_type_filter.tsx
@@ -5,9 +5,9 @@
* 2.0.
*/
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useState, useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
-import { EuiFilterGroup, EuiPopover, EuiFilterButton, EuiFilterSelectItem } from '@elastic/eui';
+import { EuiPopover, EuiFilterButton, EuiFilterSelectItem } from '@elastic/eui';
import { ActionType } from '../../../../types';
interface ActionTypeFilterProps {
@@ -29,47 +29,52 @@ export const ActionTypeFilter: React.FunctionComponent =
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedValues]);
+ const onClick = useCallback(
+ (item: ActionType) => {
+ return () => {
+ const isPreviouslyChecked = selectedValues.includes(item.id);
+ if (isPreviouslyChecked) {
+ setSelectedValues(selectedValues.filter((val) => val !== item.id));
+ } else {
+ setSelectedValues(selectedValues.concat(item.id));
+ }
+ };
+ },
+ [selectedValues, setSelectedValues]
+ );
+
return (
-
- setIsPopoverOpen(false)}
- button={
- 0}
- numActiveFilters={selectedValues.length}
- numFilters={selectedValues.length}
- onClick={() => setIsPopoverOpen(!isPopoverOpen)}
- data-test-subj="actionTypeFilterButton"
+ setIsPopoverOpen(false)}
+ button={
+ 0}
+ numActiveFilters={selectedValues.length}
+ numFilters={selectedValues.length}
+ onClick={() => setIsPopoverOpen(!isPopoverOpen)}
+ data-test-subj="actionTypeFilterButton"
+ >
+
+
+ }
+ >
+
+ {actionTypes.map((item) => (
+
-
-
- }
- >
-
- {actionTypes.map((item) => (
- {
- const isPreviouslyChecked = selectedValues.includes(item.id);
- if (isPreviouslyChecked) {
- setSelectedValues(selectedValues.filter((val) => val !== item.id));
- } else {
- setSelectedValues(selectedValues.concat(item.id));
- }
- }}
- checked={selectedValues.includes(item.id) ? 'on' : undefined}
- data-test-subj={`actionType${item.id}FilterOption`}
- >
- {item.name}
-
- ))}
-
-
-
+ {item.name}
+
+ ))}
+
+
);
};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_execution_status_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_execution_status_filter.tsx
index 9acb8489fa09a..e5bb7ffd1b0e4 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_execution_status_filter.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_execution_status_filter.tsx
@@ -5,15 +5,9 @@
* 2.0.
*/
-import React, { useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
-import {
- EuiFilterGroup,
- EuiPopover,
- EuiFilterButton,
- EuiFilterSelectItem,
- EuiHealth,
-} from '@elastic/eui';
+import { EuiPopover, EuiFilterButton, EuiFilterSelectItem, EuiHealth } from '@elastic/eui';
import { RuleExecutionStatuses, RuleExecutionStatusValues } from '@kbn/alerting-plugin/common';
import { rulesStatusesTranslationsMapping } from '../translations';
@@ -22,6 +16,8 @@ interface RuleExecutionStatusFilterProps {
onChange?: (selectedRuleStatusesIds: string[]) => void;
}
+const sortedRuleExecutionStatusValues = [...RuleExecutionStatusValues].sort();
+
export const RuleExecutionStatusFilter: React.FunctionComponent = ({
selectedStatuses,
onChange,
@@ -29,6 +25,14 @@ export const RuleExecutionStatusFilter: React.FunctionComponent(selectedStatuses);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+ const onTogglePopover = useCallback(() => {
+ setIsPopoverOpen((prevIsPopoverOpen) => !prevIsPopoverOpen);
+ }, [setIsPopoverOpen]);
+
+ const onClosePopover = useCallback(() => {
+ setIsPopoverOpen(false);
+ }, [setIsPopoverOpen]);
+
useEffect(() => {
if (onChange) {
onChange(selectedValues);
@@ -41,51 +45,49 @@ export const RuleExecutionStatusFilter: React.FunctionComponent
- setIsPopoverOpen(false)}
- button={
- 0}
- numActiveFilters={selectedValues.length}
- numFilters={selectedValues.length}
- onClick={() => setIsPopoverOpen(!isPopoverOpen)}
- data-test-subj="ruleExecutionStatusFilterButton"
- >
-
-
- }
- >
-
- {[...RuleExecutionStatusValues].sort().map((item: RuleExecutionStatuses) => {
- const healthColor = getHealthColor(item);
- return (
- {
- const isPreviouslyChecked = selectedValues.includes(item);
- if (isPreviouslyChecked) {
- setSelectedValues(selectedValues.filter((val) => val !== item));
- } else {
- setSelectedValues(selectedValues.concat(item));
- }
- }}
- checked={selectedValues.includes(item) ? 'on' : undefined}
- data-test-subj={`ruleExecutionStatus${item}FilterOption`}
- >
- {rulesStatusesTranslationsMapping[item]}
-
- );
- })}
-
-
-
+ 0}
+ numActiveFilters={selectedValues.length}
+ numFilters={selectedValues.length}
+ onClick={onTogglePopover}
+ data-test-subj="ruleExecutionStatusFilterButton"
+ >
+
+
+ }
+ >
+
+ {sortedRuleExecutionStatusValues.map((item: RuleExecutionStatuses) => {
+ const healthColor = getHealthColor(item);
+ return (
+ {
+ const isPreviouslyChecked = selectedValues.includes(item);
+ if (isPreviouslyChecked) {
+ setSelectedValues(selectedValues.filter((val) => val !== item));
+ } else {
+ setSelectedValues(selectedValues.concat(item));
+ }
+ }}
+ checked={selectedValues.includes(item) ? 'on' : undefined}
+ data-test-subj={`ruleExecutionStatus${item}FilterOption`}
+ >
+ {rulesStatusesTranslationsMapping[item]}
+
+ );
+ })}
+
+
);
};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx
index 7c6a71e893f96..194bf86030e56 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx
@@ -33,7 +33,7 @@ import { parseInterval } from '../../../../../common';
import { Rule } from '../../../../types';
-type SnoozeUnit = 'm' | 'h' | 'd' | 'w' | 'M';
+export type SnoozeUnit = 'm' | 'h' | 'd' | 'w' | 'M';
const SNOOZE_END_TIME_FORMAT = 'LL @ LT';
type DropdownRuleRecord = Pick;
@@ -48,6 +48,7 @@ export interface ComponentOpts {
isEditable: boolean;
previousSnoozeInterval?: string | null;
direction?: 'column' | 'row';
+ hideSnoozeOption?: boolean;
}
const COMMON_SNOOZE_TIMES: Array<[number, SnoozeUnit]> = [
@@ -58,9 +59,9 @@ const COMMON_SNOOZE_TIMES: Array<[number, SnoozeUnit]> = [
];
const PREV_SNOOZE_INTERVAL_KEY = 'triggersActionsUi_previousSnoozeInterval';
-const usePreviousSnoozeInterval: (p?: string | null) => [string | null, (n: string) => void] = (
- propsInterval
-) => {
+export const usePreviousSnoozeInterval: (
+ p?: string | null
+) => [string | null, (n: string) => void] = (propsInterval) => {
const intervalFromStorage = localStorage.getItem(PREV_SNOOZE_INTERVAL_KEY);
const usePropsInterval = typeof propsInterval !== 'undefined';
const interval = usePropsInterval ? propsInterval : intervalFromStorage;
@@ -74,7 +75,7 @@ const usePreviousSnoozeInterval: (p?: string | null) => [string | null, (n: stri
return [previousSnoozeInterval, storeAndSetPreviousSnoozeInterval];
};
-const isRuleSnoozed = (rule: { isSnoozedUntil?: Date | null; muteAll: boolean }) =>
+export const isRuleSnoozed = (rule: { isSnoozedUntil?: Date | null; muteAll: boolean }) =>
Boolean(
(rule.isSnoozedUntil && new Date(rule.isSnoozedUntil).getTime() > Date.now()) || rule.muteAll
);
@@ -88,6 +89,7 @@ export const RuleStatusDropdown: React.FunctionComponent = ({
unsnoozeRule,
isEditable,
previousSnoozeInterval: propsPreviousSnoozeInterval,
+ hideSnoozeOption = false,
direction = 'column',
}: ComponentOpts) => {
const [isEnabled, setIsEnabled] = useState(rule.enabled);
@@ -224,6 +226,7 @@ export const RuleStatusDropdown: React.FunctionComponent = ({
isSnoozed={isSnoozed}
snoozeEndTime={rule.isSnoozedUntil}
previousSnoozeInterval={previousSnoozeInterval}
+ hideSnoozeOption={hideSnoozeOption}
/>
) : (
@@ -245,6 +248,7 @@ interface RuleStatusMenuProps {
isSnoozed: boolean;
snoozeEndTime?: Date | null;
previousSnoozeInterval: string | null;
+ hideSnoozeOption?: boolean;
}
const RuleStatusMenu: React.FunctionComponent = ({
@@ -255,6 +259,7 @@ const RuleStatusMenu: React.FunctionComponent = ({
isSnoozed,
snoozeEndTime,
previousSnoozeInterval,
+ hideSnoozeOption = false,
}) => {
const enableRule = useCallback(() => {
if (isSnoozed) {
@@ -290,6 +295,44 @@ const RuleStatusMenu: React.FunctionComponent = ({
);
}
+ const getSnoozeMenuItem = () => {
+ if (!hideSnoozeOption) {
+ return [
+ {
+ name: snoozeButtonTitle,
+ icon: isEnabled && isSnoozed ? 'check' : 'empty',
+ panel: 1,
+ disabled: !isEnabled,
+ 'data-test-subj': 'statusDropdownSnoozeItem',
+ },
+ ];
+ }
+ return [];
+ };
+
+ const getSnoozePanel = () => {
+ if (!hideSnoozeOption) {
+ return [
+ {
+ id: 1,
+ width: 360,
+ title: SNOOZE,
+ content: (
+
+
+
+ ),
+ },
+ ];
+ }
+ return [];
+ };
+
const panels = [
{
id: 0,
@@ -307,28 +350,10 @@ const RuleStatusMenu: React.FunctionComponent = ({
onClick: disableRule,
'data-test-subj': 'statusDropdownDisabledItem',
},
- {
- name: snoozeButtonTitle,
- icon: isEnabled && isSnoozed ? 'check' : 'empty',
- panel: 1,
- disabled: !isEnabled,
- 'data-test-subj': 'statusDropdownSnoozeItem',
- },
+ ...getSnoozeMenuItem(),
],
},
- {
- id: 1,
- width: 360,
- title: SNOOZE,
- content: (
-
- ),
- },
+ ...getSnoozePanel(),
];
return ;
@@ -336,13 +361,15 @@ const RuleStatusMenu: React.FunctionComponent = ({
interface SnoozePanelProps {
interval?: string;
+ isLoading?: boolean;
applySnooze: (value: number | -1, unit?: SnoozeUnit) => void;
showCancel: boolean;
previousSnoozeInterval: string | null;
}
-const SnoozePanel: React.FunctionComponent = ({
+export const SnoozePanel: React.FunctionComponent = ({
interval = '3d',
+ isLoading = false,
applySnooze,
showCancel,
previousSnoozeInterval,
@@ -394,9 +421,9 @@ const SnoozePanel: React.FunctionComponent = ({
>
);
return (
-
+ <>
-
+
= ({
/>
-
+
{i18n.translate('xpack.triggersActionsUI.sections.rulesList.applySnooze', {
defaultMessage: 'Apply',
})}
@@ -471,7 +502,12 @@ const SnoozePanel: React.FunctionComponent = ({
-
+
Cancel snooze
@@ -479,11 +515,11 @@ const SnoozePanel: React.FunctionComponent = ({
>
)}
-
+ >
);
};
-const futureTimeToInterval = (time?: Date | null) => {
+export const futureTimeToInterval = (time?: Date | null) => {
if (!time) return;
const relativeTime = moment(time).locale('en').fromNow(true);
const [valueStr, unitStr] = relativeTime.split(' ');
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.test.tsx
index f1f2957f9cada..a7d3bdfb8e2e0 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.test.tsx
@@ -7,12 +7,12 @@
import React from 'react';
import { mountWithIntl } from '@kbn/test-jest-helpers';
-import { EuiFilterButton, EuiFilterSelectItem } from '@elastic/eui';
+import { EuiFilterButton, EuiSelectableListItem } from '@elastic/eui';
import { RuleStatusFilter } from './rule_status_filter';
const onChangeMock = jest.fn();
-describe('rule_state_filter', () => {
+describe('RuleStatusFilter', () => {
beforeEach(() => {
onChangeMock.mockReset();
});
@@ -22,7 +22,7 @@ describe('rule_state_filter', () => {
);
- expect(wrapper.find(EuiFilterSelectItem).exists()).toBeFalsy();
+ expect(wrapper.find(EuiSelectableListItem).exists()).toBeFalsy();
expect(wrapper.find(EuiFilterButton).exists()).toBeTruthy();
expect(wrapper.find('.euiNotificationBadge').text()).toEqual('0');
@@ -37,7 +37,7 @@ describe('rule_state_filter', () => {
wrapper.find(EuiFilterButton).simulate('click');
- const statusItems = wrapper.find(EuiFilterSelectItem);
+ const statusItems = wrapper.find(EuiSelectableListItem);
expect(statusItems.length).toEqual(3);
});
@@ -48,17 +48,17 @@ describe('rule_state_filter', () => {
wrapper.find(EuiFilterButton).simulate('click');
- wrapper.find(EuiFilterSelectItem).at(0).simulate('click');
+ wrapper.find(EuiSelectableListItem).at(0).simulate('click');
expect(onChangeMock).toHaveBeenCalledWith(['enabled']);
wrapper.setProps({
selectedStatuses: ['enabled'],
});
- wrapper.find(EuiFilterSelectItem).at(0).simulate('click');
+ wrapper.find(EuiSelectableListItem).at(0).simulate('click');
expect(onChangeMock).toHaveBeenCalledWith([]);
- wrapper.find(EuiFilterSelectItem).at(1).simulate('click');
+ wrapper.find(EuiSelectableListItem).at(1).simulate('click');
expect(onChangeMock).toHaveBeenCalledWith(['enabled', 'disabled']);
});
});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.tsx
index 6d286ec6d09d7..f26b3f54c587e 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.tsx
@@ -6,7 +6,13 @@
*/
import React, { useState, useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
-import { EuiFilterButton, EuiPopover, EuiFilterGroup, EuiFilterSelectItem } from '@elastic/eui';
+import {
+ EuiFilterButton,
+ EuiPopover,
+ EuiFilterGroup,
+ EuiSelectableListItem,
+ EuiButtonEmpty,
+} from '@elastic/eui';
import { RuleStatus } from '../../../../types';
const statuses: RuleStatus[] = ['enabled', 'disabled', 'snoozed'];
@@ -53,6 +59,24 @@ export const RuleStatusFilter = (props: RuleStatusFilterProps) => {
setIsPopoverOpen((prevIsOpen) => !prevIsOpen);
}, [setIsPopoverOpen]);
+ const renderClearAll = () => {
+ return (
+
+ onChange([])}
+ >
+ Clear all
+
+
+ );
+ };
+
return (
{
>
}
@@ -77,7 +101,7 @@ export const RuleStatusFilter = (props: RuleStatusFilterProps) => {
{statuses.map((status) => {
return (
- {
checked={selectedStatuses.includes(status) ? 'on' : undefined}
>
{status}
-
+
);
})}
+ {renderClearAll()}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx
index 636bcaf1acb22..47b93ff19c6ea 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx
@@ -9,7 +9,6 @@ import React, { useMemo, useState, useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiSelectable,
- EuiFilterGroup,
EuiFilterButton,
EuiPopover,
EuiSelectableProps,
@@ -103,29 +102,32 @@ export const RuleTagFilter = (props: RuleTagFilterProps) => {
};
return (
-
-
-
- {(list, search) => (
- <>
- {search}
-
- {list}
- >
- )}
-
-
-
+
+
+ {(list, search) => (
+ <>
+ {search}
+
+ {list}
+ >
+ )}
+
+
);
};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx
index 7827033138fbb..893d6cf7bc5ad 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx
@@ -365,7 +365,7 @@ describe('Update Api Key', () => {
fireEvent.click(screen.getByText('Update'));
});
expect(updateAPIKey).toHaveBeenCalledWith(expect.objectContaining({ id: '2' }));
- expect(loadRules).toHaveBeenCalledTimes(2);
+ expect(loadRules).toHaveBeenCalledTimes(3);
expect(screen.queryByText("You can't recover the old API key")).not.toBeInTheDocument();
expect(addSuccess).toHaveBeenCalledWith('API key has been updated');
});
@@ -390,7 +390,7 @@ describe('Update Api Key', () => {
fireEvent.click(screen.getByText('Update'));
});
expect(updateAPIKey).toHaveBeenCalledWith(expect.objectContaining({ id: '2' }));
- expect(loadRules).toHaveBeenCalledTimes(2);
+ expect(loadRules).toHaveBeenCalledTimes(3);
expect(
screen.queryByText('You will not be able to recover the old API key')
).not.toBeInTheDocument();
@@ -514,7 +514,6 @@ describe('rules_list component with items', () => {
// eslint-disable-next-line react-hooks/rules-of-hooks
useKibanaMock().services.actionTypeRegistry = actionTypeRegistry;
wrapper = mountWithIntl();
-
await act(async () => {
await nextTick();
wrapper.update();
@@ -561,7 +560,7 @@ describe('rules_list component with items', () => {
.simulate('mouseOver');
// Run the timers so the EuiTooltip will be visible
- jest.runAllTimers();
+ jest.runOnlyPendingTimers();
wrapper.update();
expect(wrapper.find('.euiToolTipPopover').text()).toBe('Start time of the last run.');
@@ -580,7 +579,7 @@ describe('rules_list component with items', () => {
wrapper.find('[data-test-subj="ruleInterval-config-tooltip-0"]').first().simulate('mouseOver');
// Run the timers so the EuiTooltip will be visible
- jest.runAllTimers();
+ jest.runOnlyPendingTimers();
wrapper.update();
expect(wrapper.find('.euiToolTipPopover').text()).toBe(
@@ -605,7 +604,7 @@ describe('rules_list component with items', () => {
wrapper.find('[data-test-subj="rulesTableCell-durationTooltip"]').first().simulate('mouseOver');
// Run the timers so the EuiTooltip will be visible
- jest.runAllTimers();
+ jest.runOnlyPendingTimers();
wrapper.update();
expect(wrapper.find('.euiToolTipPopover').text()).toBe(
@@ -627,7 +626,7 @@ describe('rules_list component with items', () => {
wrapper.find('EuiButtonEmpty[data-test-subj="ruleStatus-error-license-fix"]').length
).toEqual(1);
- expect(wrapper.find('[data-test-subj="refreshRulesButton"]').exists()).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="rulesListAutoRefresh"]').exists()).toBeTruthy();
expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-error"]').first().text()).toEqual(
'Error'
@@ -724,7 +723,7 @@ describe('rules_list component with items', () => {
.first()
.simulate('click');
- jest.runAllTimers();
+ jest.runOnlyPendingTimers();
wrapper.update();
// Percentile Selection
@@ -740,7 +739,7 @@ describe('rules_list component with items', () => {
// Select P95
percentileOptions.at(1).simulate('click');
- jest.runAllTimers();
+ jest.runOnlyPendingTimers();
wrapper.update();
expect(
@@ -795,18 +794,6 @@ describe('rules_list component with items', () => {
jest.clearAllMocks();
});
- it('loads rules when refresh button is clicked', async () => {
- await setup();
- wrapper.find('[data-test-subj="refreshRulesButton"]').first().simulate('click');
-
- await act(async () => {
- await nextTick();
- wrapper.update();
- });
-
- expect(loadRules).toHaveBeenCalled();
- });
-
it('renders license errors and manage license modal on click', async () => {
global.open = jest.fn();
await setup();
@@ -854,7 +841,7 @@ describe('rules_list component with items', () => {
it('sorts rules when clicking the status control column', async () => {
await setup();
wrapper
- .find('[data-test-subj="tableHeaderCell_enabled_8"] .euiTableHeaderButton')
+ .find('[data-test-subj="tableHeaderCell_enabled_9"] .euiTableHeaderButton')
.first()
.simulate('click');
@@ -923,21 +910,37 @@ describe('rules_list component with items', () => {
loadRules.mockReset();
await setup();
- expect(loadRules.mock.calls[0][0].ruleStatusesFilter).toEqual([]);
+ expect(loadRules).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ ruleStatusesFilter: [],
+ })
+ );
wrapper.find('[data-test-subj="ruleStatusFilterButton"] button').simulate('click');
wrapper.find('[data-test-subj="ruleStatusFilterOption-enabled"]').first().simulate('click');
- expect(loadRules.mock.calls[1][0].ruleStatusesFilter).toEqual(['enabled']);
+ expect(loadRules).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ ruleStatusesFilter: ['enabled'],
+ })
+ );
wrapper.find('[data-test-subj="ruleStatusFilterOption-snoozed"]').first().simulate('click');
- expect(loadRules.mock.calls[2][0].ruleStatusesFilter).toEqual(['enabled', 'snoozed']);
+ expect(loadRules).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ ruleStatusesFilter: ['enabled', 'snoozed'],
+ })
+ );
wrapper.find('[data-test-subj="ruleStatusFilterOption-snoozed"]').first().simulate('click');
- expect(loadRules.mock.calls[3][0].ruleStatusesFilter).toEqual(['enabled']);
+ expect(loadRules).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ ruleStatusesFilter: ['enabled'],
+ })
+ );
});
it('does not render the tag filter is the feature flag is off', async () => {
@@ -956,7 +959,11 @@ describe('rules_list component with items', () => {
loadRules.mockReset();
await setup();
- expect(loadRules.mock.calls[0][0].tagsFilter).toEqual([]);
+ expect(loadRules).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ tagsFilter: [],
+ })
+ );
wrapper.find('[data-test-subj="ruleTagFilterButton"] button').simulate('click');
@@ -967,11 +974,19 @@ describe('rules_list component with items', () => {
tagFilterListItems.at(0).simulate('click');
- expect(loadRules.mock.calls[1][0].tagsFilter).toEqual(['a']);
+ expect(loadRules).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ tagsFilter: ['a'],
+ })
+ );
tagFilterListItems.at(1).simulate('click');
- expect(loadRules.mock.calls[2][0].tagsFilter).toEqual(['a', 'b']);
+ expect(loadRules).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ tagsFilter: ['a', 'b'],
+ })
+ );
});
});
@@ -1255,4 +1270,21 @@ describe('rules_list with disabled items', () => {
wrapper.find('EuiIconTip[data-test-subj="ruleDisabledByLicenseTooltip"]').props().content
).toEqual('This rule type requires a Platinum license.');
});
+
+ it('clicking the notify badge shows the snooze panel', async () => {
+ await setup();
+
+ expect(wrapper.find('[data-test-subj="snoozePanel"]').exists()).toBeFalsy();
+
+ wrapper
+ .find('[data-test-subj="rulesTableCell-rulesListNotify"]')
+ .first()
+ .simulate('mouseenter');
+
+ expect(wrapper.find('[data-test-subj="rulesListNotifyBadge"]').exists()).toBeTruthy();
+
+ wrapper.find('[data-test-subj="rulesListNotifyBadge"]').first().simulate('click');
+
+ expect(wrapper.find('[data-test-subj="snoozePanel"]').exists()).toBeTruthy();
+ });
});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx
index 9c3f1415e6641..b8afb2d3124ef 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx
@@ -8,49 +8,36 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { i18n } from '@kbn/i18n';
-import { capitalize, sortBy } from 'lodash';
import moment from 'moment';
+import { capitalize, sortBy } from 'lodash';
import { FormattedMessage } from '@kbn/i18n-react';
-import React, { useEffect, useState, useMemo, ReactNode, useCallback } from 'react';
+import React, { useEffect, useState, ReactNode, useCallback, useMemo } from 'react';
import {
- EuiBasicTable,
EuiButton,
EuiFieldSearch,
EuiFlexGroup,
EuiFlexItem,
- EuiIconTip,
+ EuiFilterGroup,
EuiSpacer,
EuiLink,
EuiEmptyPrompt,
- EuiButtonEmpty,
EuiHealth,
EuiText,
- EuiToolTip,
EuiTableSortingType,
EuiButtonIcon,
EuiHorizontalRule,
EuiSelectableOption,
EuiIcon,
- EuiScreenReaderOnly,
- RIGHT_ALIGNMENT,
EuiDescriptionList,
- EuiTableFieldDataColumnType,
- EuiTableComputedColumnType,
- EuiTableActionsColumnType,
EuiCallOut,
} from '@elastic/eui';
import { EuiSelectableOptionCheckedType } from '@elastic/eui/src/components/selectable/selectable_option';
import { useHistory } from 'react-router-dom';
-import { isEmpty } from 'lodash';
import {
RuleExecutionStatus,
- RuleExecutionStatusValues,
ALERTS_FEATURE_ID,
RuleExecutionStatusErrorReasons,
- formatDuration,
- parseDuration,
- MONITORING_HISTORY_LIMIT,
} from '@kbn/alerting-plugin/common';
import {
ActionType,
@@ -69,11 +56,8 @@ import { RuleQuickEditButtonsWithApi as RuleQuickEditButtons } from '../../commo
import { CollapsedItemActionsWithApi as CollapsedItemActions } from './collapsed_item_actions';
import { TypeFilter } from './type_filter';
import { ActionTypeFilter } from './action_type_filter';
-import { RuleExecutionStatusFilter, getHealthColor } from './rule_execution_status_filter';
+import { RuleExecutionStatusFilter } from './rule_execution_status_filter';
import {
- loadRules,
- loadRuleAggregations,
- loadRuleTags,
loadRuleTypes,
disableRule,
enableRule,
@@ -87,23 +71,21 @@ import { hasAllPrivilege, hasExecuteActionsCapability } from '../../../lib/capab
import { routeToRuleDetails, DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants';
import { DeleteModalConfirmation } from '../../../components/delete_modal_confirmation';
import { EmptyPrompt } from '../../../components/prompts/empty_prompt';
-import { rulesStatusesTranslationsMapping, ALERT_STATUS_LICENSE_ERROR } from '../translations';
+import { ALERT_STATUS_LICENSE_ERROR } from '../translations';
import { useKibana } from '../../../../common/lib/kibana';
import { DEFAULT_HIDDEN_ACTION_TYPES } from '../../../../common/constants';
import './rules_list.scss';
import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner';
import { ManageLicenseModal } from './manage_license_modal';
-import { checkRuleTypeEnabled } from '../../../lib/check_rule_type_enabled';
-import { RuleStatusDropdown } from './rule_status_dropdown';
-import { RuleTagBadge } from './rule_tag_badge';
-import { PercentileSelectablePopover } from './percentile_selectable_popover';
-import { RuleDurationFormat } from './rule_duration_format';
-import { shouldShowDurationWarning } from '../../../lib/execution_duration_utils';
-import { getFormattedSuccessRatio } from '../../../lib/monitoring_utils';
import { triggersActionsUiConfig } from '../../../../common/lib/config_api';
import { RuleTagFilter } from './rule_tag_filter';
import { RuleStatusFilter } from './rule_status_filter';
import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features';
+import { useLoadRules } from '../../../hooks/use_load_rules';
+import { useLoadTags } from '../../../hooks/use_load_tags';
+import { useLoadRuleAggregations } from '../../../hooks/use_load_rule_aggregations';
+import { RulesListTable, convertRulesToTableItems } from './rules_list_table';
+import { RulesListAutoRefresh } from './rules_list_auto_refresh';
import { UpdateApiKeyModalConfirmation } from '../../../components/update_api_key_modal_confirmation';
const ENTER_KEY = 13;
@@ -113,17 +95,6 @@ interface RuleTypeState {
isInitialized: boolean;
data: RuleTypeIndex;
}
-interface RuleState {
- isLoading: boolean;
- data: Rule[];
- totalItemCount: number;
-}
-
-const percentileOrdinals = {
- [Percentiles.P50]: '50th',
- [Percentiles.P95]: '95th',
- [Percentiles.P99]: '99th',
-};
export const percentileFields = {
[Percentiles.P50]: 'monitoring.execution.calculated_metrics.p50',
@@ -149,8 +120,6 @@ export const RulesList: React.FunctionComponent = () => {
} = useKibana().services;
const canExecuteActions = hasExecuteActionsCapability(capabilities);
- const [initialLoad, setInitialLoad] = useState(true);
- const [noData, setNoData] = useState(true);
const [config, setConfig] = useState({ isUsingSecurity: false });
const [actionTypes, setActionTypes] = useState([]);
const [selectedIds, setSelectedIds] = useState([]);
@@ -162,16 +131,15 @@ export const RulesList: React.FunctionComponent = () => {
const [actionTypesFilter, setActionTypesFilter] = useState([]);
const [ruleExecutionStatusesFilter, setRuleExecutionStatusesFilter] = useState([]);
const [ruleStatusesFilter, setRuleStatusesFilter] = useState([]);
- const [tags, setTags] = useState([]);
const [tagsFilter, setTagsFilter] = useState([]);
const [ruleFlyoutVisible, setRuleFlyoutVisibility] = useState(false);
const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false);
const [currentRuleToEdit, setCurrentRuleToEdit] = useState(null);
- const [tagPopoverOpenIndex, setTagPopoverOpenIndex] = useState(-1);
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>(
{}
);
const [showErrors, setShowErrors] = useState(false);
+ const [lastUpdate, setLastUpdate] = useState('');
const isRuleTagFilterEnabled = getIsExperimentalFeatureEnabled('ruleTagFilter');
const isRuleStatusFilterEnabled = getIsExperimentalFeatureEnabled('ruleStatusFilter');
@@ -185,13 +153,6 @@ export const RulesList: React.FunctionComponent = () => {
const [percentileOptions, setPercentileOptions] =
useState(initialPercentileOptions);
- const selectedPercentile = useMemo(() => {
- const selectedOption = percentileOptions.find((option) => option.checked === 'on');
- if (selectedOption) {
- return Percentiles[selectedOption.key as Percentiles];
- }
- }, [percentileOptions]);
-
const [sort, setSort] = useState['sort']>({
field: 'name',
direction: 'asc',
@@ -200,27 +161,52 @@ export const RulesList: React.FunctionComponent = () => {
licenseType: string;
ruleTypeId: string;
} | null>(null);
- const [rulesStatusesTotal, setRulesStatusesTotal] = useState>(
- RuleExecutionStatusValues.reduce(
- (prev: Record, status: string) =>
- ({
- ...prev,
- [status]: 0,
- } as Record),
- {}
- )
- );
const [ruleTypesState, setRuleTypesState] = useState({
isLoading: false,
isInitialized: false,
data: new Map(),
});
- const [rulesState, setRulesState] = useState({
- isLoading: false,
- data: [],
- totalItemCount: 0,
- });
+
const [rulesToDelete, setRulesToDelete] = useState([]);
+
+ const hasAnyAuthorizedRuleType = useMemo(() => {
+ return ruleTypesState.isInitialized && ruleTypesState.data.size > 0;
+ }, [ruleTypesState]);
+
+ const onError = useCallback(
+ (message: string) => {
+ toasts.addDanger(message);
+ },
+ [toasts]
+ );
+
+ const { rulesState, setRulesState, loadRules, noData, initialLoad } = useLoadRules({
+ page,
+ searchText,
+ typesFilter,
+ actionTypesFilter,
+ ruleExecutionStatusesFilter,
+ ruleStatusesFilter,
+ tagsFilter,
+ sort,
+ onPage: setPage,
+ onError,
+ });
+
+ const { tags, loadTags } = useLoadTags({
+ onError,
+ });
+
+ const { loadRuleAggregations, rulesStatusesTotal } = useLoadRuleAggregations({
+ searchText,
+ typesFilter,
+ actionTypesFilter,
+ ruleExecutionStatusesFilter,
+ ruleStatusesFilter,
+ tagsFilter,
+ onError,
+ });
+
const [rulesToUpdateAPIKey, setRulesToUpdateAPIKey] = useState([]);
const onRuleEdit = (ruleItem: RuleTableItem) => {
setEditFlyoutVisibility(true);
@@ -230,20 +216,30 @@ export const RulesList: React.FunctionComponent = () => {
const isRuleTypeEditableInContext = (ruleTypeId: string) =>
ruleTypeRegistry.has(ruleTypeId) ? !ruleTypeRegistry.get(ruleTypeId).requiresAppContext : false;
- useEffect(() => {
- loadRulesData();
+ const loadData = useCallback(async () => {
+ if (!ruleTypesState || !hasAnyAuthorizedRuleType) {
+ return;
+ }
+ await loadRules();
+ await loadRuleAggregations();
+ if (isRuleStatusFilterEnabled) {
+ await loadTags();
+ }
+ setLastUpdate(moment().format());
}, [
+ loadRules,
+ loadTags,
+ loadRuleAggregations,
+ setLastUpdate,
+ isRuleStatusFilterEnabled,
+ hasAnyAuthorizedRuleType,
ruleTypesState,
- page,
- searchText,
- percentileOptions,
- JSON.stringify(typesFilter),
- JSON.stringify(actionTypesFilter),
- JSON.stringify(ruleExecutionStatusesFilter),
- JSON.stringify(ruleStatusesFilter),
- JSON.stringify(tagsFilter),
]);
+ useEffect(() => {
+ loadData();
+ }, [loadData, percentileOptions]);
+
useEffect(() => {
(async () => {
try {
@@ -289,218 +285,6 @@ export const RulesList: React.FunctionComponent = () => {
})();
}, []);
- async function loadRulesData() {
- const hasAnyAuthorizedRuleType = ruleTypesState.isInitialized && ruleTypesState.data.size > 0;
- if (hasAnyAuthorizedRuleType) {
- setRulesState({ ...rulesState, isLoading: true });
- try {
- const rulesResponse = await loadRules({
- http,
- page,
- searchText,
- typesFilter,
- actionTypesFilter,
- ruleExecutionStatusesFilter,
- ruleStatusesFilter,
- tagsFilter,
- sort,
- });
- await loadRuleTagsAggs();
- await loadRuleAggs();
- setRulesState({
- isLoading: false,
- data: rulesResponse.data,
- totalItemCount: rulesResponse.total,
- });
-
- if (!rulesResponse.data?.length && page.index > 0) {
- setPage({ ...page, index: 0 });
- }
-
- const isFilterApplied = !(
- isEmpty(searchText) &&
- isEmpty(typesFilter) &&
- isEmpty(actionTypesFilter) &&
- isEmpty(ruleExecutionStatusesFilter) &&
- isEmpty(ruleStatusesFilter) &&
- isEmpty(tagsFilter)
- );
-
- setNoData(rulesResponse.data.length === 0 && !isFilterApplied);
- } catch (e) {
- toasts.addDanger({
- title: i18n.translate(
- 'xpack.triggersActionsUI.sections.rulesList.unableToLoadRulesMessage',
- {
- defaultMessage: 'Unable to load rules',
- }
- ),
- });
- setRulesState({ ...rulesState, isLoading: false });
- }
- setInitialLoad(false);
- }
- }
-
- async function loadRuleAggs() {
- try {
- const rulesAggs = await loadRuleAggregations({
- http,
- searchText,
- typesFilter,
- actionTypesFilter,
- ruleExecutionStatusesFilter,
- ruleStatusesFilter,
- tagsFilter,
- });
- if (rulesAggs?.ruleExecutionStatus) {
- setRulesStatusesTotal(rulesAggs.ruleExecutionStatus);
- }
- } catch (e) {
- toasts.addDanger({
- title: i18n.translate(
- 'xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleStatusInfoMessage',
- {
- defaultMessage: 'Unable to load rule status info',
- }
- ),
- });
- }
- }
-
- async function loadRuleTagsAggs() {
- if (!isRuleTagFilterEnabled) {
- return;
- }
- try {
- const ruleTagsAggs = await loadRuleTags({ http });
- if (ruleTagsAggs?.ruleTags) {
- setTags(ruleTagsAggs.ruleTags);
- }
- } catch (e) {
- toasts.addDanger({
- title: i18n.translate('xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleTags', {
- defaultMessage: 'Unable to load rule tags',
- }),
- });
- }
- }
-
- const renderRuleStatusDropdown = (ruleEnabled: boolean | undefined, item: RuleTableItem) => {
- return (
- await disableRule({ http, id: item.id })}
- enableRule={async () => await enableRule({ http, id: item.id })}
- snoozeRule={async (snoozeEndTime: string | -1, interval: string | null) => {
- await snoozeRule({ http, id: item.id, snoozeEndTime });
- }}
- unsnoozeRule={async () => await unsnoozeRule({ http, id: item.id })}
- rule={item}
- onRuleChanged={() => loadRulesData()}
- isEditable={item.isEditable && isRuleTypeEditableInContext(item.ruleTypeId)}
- />
- );
- };
-
- const renderRuleExecutionStatus = (executionStatus: RuleExecutionStatus, item: RuleTableItem) => {
- const healthColor = getHealthColor(executionStatus.status);
- const tooltipMessage =
- executionStatus.status === 'error' ? `Error: ${executionStatus?.error?.message}` : null;
- const isLicenseError =
- executionStatus.error?.reason === RuleExecutionStatusErrorReasons.License;
- const statusMessage = isLicenseError
- ? ALERT_STATUS_LICENSE_ERROR
- : rulesStatusesTranslationsMapping[executionStatus.status];
-
- const health = (
-
- {statusMessage}
-
- );
-
- const healthWithTooltip = tooltipMessage ? (
-
- {health}
-
- ) : (
- health
- );
-
- return (
-
- {healthWithTooltip}
- {isLicenseError && (
-
-
- setManageLicenseModalOpts({
- licenseType: ruleTypesState.data.get(item.ruleTypeId)?.minimumLicenseRequired!,
- ruleTypeId: item.ruleTypeId,
- })
- }
- >
-
-
-
- )}
-
- );
- };
-
- const renderPercentileColumnName = () => {
- return (
-
-
-
- {selectedPercentile}
-
-
-
-
-
- );
- };
-
- const renderPercentileCellValue = (value: number) => {
- return (
-
-
-
- );
- };
-
- const getPercentileColumn = () => {
- return {
- mobileOptions: { header: false },
- field: percentileFields[selectedPercentile!],
- width: '16%',
- name: renderPercentileColumnName(),
- 'data-test-subj': 'rulesTableCell-ruleExecutionPercentile',
- sortable: true,
- truncateText: false,
- render: renderPercentileCellValue,
- };
- };
-
const buildErrorListItems = (_executionStatus: RuleExecutionStatus) => {
const hasErrorMessage = _executionStatus.status === 'error';
const errorMessage = _executionStatus?.error?.message;
@@ -563,383 +347,6 @@ export const RulesList: React.FunctionComponent = () => {
});
}, [showErrors, rulesState]);
- const getRulesTableColumns = (): Array<
- | EuiTableFieldDataColumnType
- | EuiTableComputedColumnType
- | EuiTableActionsColumnType
- > => {
- return [
- {
- field: 'name',
- name: i18n.translate(
- 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.nameTitle',
- { defaultMessage: 'Name' }
- ),
- sortable: true,
- truncateText: true,
- width: '30%',
- 'data-test-subj': 'rulesTableCell-name',
- render: (name: string, rule: RuleTableItem) => {
- const ruleType = ruleTypesState.data.get(rule.ruleTypeId);
- const checkEnabledResult = checkRuleTypeEnabled(ruleType);
- const link = (
- <>
-
-
-
-
- {
- history.push(routeToRuleDetails.replace(`:ruleId`, rule.id));
- }}
- >
- {name}
-
-
-
- {!checkEnabledResult.isEnabled && (
-
- )}
-
-
-
-
-
- {rule.ruleType}
-
-
-
- >
- );
- return <>{link}>;
- },
- },
- {
- field: 'tags',
- name: '',
- sortable: false,
- width: '50px',
- 'data-test-subj': 'rulesTableCell-tagsPopover',
- render: (ruleTags: string[], item: RuleTableItem) => {
- return ruleTags.length > 0 ? (
- setTagPopoverOpenIndex(item.index)}
- onClose={() => setTagPopoverOpenIndex(-1)}
- />
- ) : null;
- },
- },
- {
- field: 'executionStatus.lastExecutionDate',
- name: (
-
-
- Last run{' '}
-
-
-
- ),
- sortable: true,
- width: '15%',
- 'data-test-subj': 'rulesTableCell-lastExecutionDate',
- render: (date: Date) => {
- if (date) {
- return (
- <>
-
-
- {moment(date).format('MMM D, YYYY HH:mm:ssa')}
-
-
-
- {moment(date).fromNow()}
-
-
-
- >
- );
- }
- },
- },
- {
- field: 'schedule.interval',
- width: '6%',
- name: i18n.translate(
- 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.scheduleTitle',
- { defaultMessage: 'Interval' }
- ),
- sortable: false,
- truncateText: false,
- 'data-test-subj': 'rulesTableCell-interval',
- render: (interval: string, item: RuleTableItem) => {
- const durationString = formatDuration(interval);
- return (
- <>
-
- {durationString}
-
- {item.showIntervalWarning && (
-
- {
- if (item.isEditable && isRuleTypeEditableInContext(item.ruleTypeId)) {
- onRuleEdit(item);
- }
- }}
- iconType="flag"
- aria-label={i18n.translate(
- 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.intervalIconAriaLabel',
- { defaultMessage: 'Below configured minimum interval' }
- )}
- />
-
- )}
-
-
- >
- );
- },
- },
- {
- field: 'executionStatus.lastDuration',
- width: '12%',
- name: (
-
-
- Duration{' '}
-
-
-
- ),
- sortable: true,
- truncateText: false,
- 'data-test-subj': 'rulesTableCell-duration',
- render: (value: number, item: RuleTableItem) => {
- const showDurationWarning = shouldShowDurationWarning(
- ruleTypesState.data.get(item.ruleTypeId),
- value
- );
-
- return (
- <>
- {}
- {showDurationWarning && (
-
- )}
- >
- );
- },
- },
- getPercentileColumn(),
- {
- field: 'monitoring.execution.calculated_metrics.success_ratio',
- width: '12%',
- name: (
-
-
- Success ratio{' '}
-
-
-
- ),
- sortable: true,
- truncateText: false,
- 'data-test-subj': 'rulesTableCell-successRatio',
- render: (value: number) => {
- return (
-
- {value !== undefined ? getFormattedSuccessRatio(value) : 'N/A'}
-
- );
- },
- },
- {
- field: 'executionStatus.status',
- name: i18n.translate(
- 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.lastResponseTitle',
- { defaultMessage: 'Last response' }
- ),
- sortable: true,
- truncateText: false,
- width: '120px',
- 'data-test-subj': 'rulesTableCell-lastResponse',
- render: (_executionStatus: RuleExecutionStatus, item: RuleTableItem) => {
- return renderRuleExecutionStatus(item.executionStatus, item);
- },
- },
- {
- field: 'enabled',
- name: i18n.translate(
- 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.stateTitle',
- { defaultMessage: 'State' }
- ),
- sortable: true,
- truncateText: false,
- width: '10%',
- 'data-test-subj': 'rulesTableCell-status',
- render: (_enabled: boolean | undefined, item: RuleTableItem) => {
- return renderRuleStatusDropdown(item.enabled, item);
- },
- },
- {
- name: '',
- width: '90px',
- render(item: RuleTableItem) {
- return (
-
-
-
- {item.isEditable && isRuleTypeEditableInContext(item.ruleTypeId) ? (
-
- onRuleEdit(item)}
- iconType={'pencil'}
- aria-label={i18n.translate(
- 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.editAriaLabel',
- { defaultMessage: 'Edit' }
- )}
- />
-
- ) : null}
- {item.isEditable ? (
-
- setRulesToDelete([item.id])}
- iconType={'trash'}
- aria-label={i18n.translate(
- 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.deleteAriaLabel',
- { defaultMessage: 'Delete' }
- )}
- />
-
- ) : null}
-
-
-
- loadRulesData()}
- setRulesToDelete={setRulesToDelete}
- onEditRule={() => onRuleEdit(item)}
- onUpdateAPIKey={setRulesToUpdateAPIKey}
- />
-
-
- );
- },
- },
- {
- align: RIGHT_ALIGNMENT,
- width: '40px',
- isExpander: true,
- name: (
-
- Expand rows
-
- ),
- render: (item: RuleTableItem) => {
- const _executionStatus = item.executionStatus;
- const hasErrorMessage = _executionStatus.status === 'error';
- const isLicenseError =
- _executionStatus.error?.reason === RuleExecutionStatusErrorReasons.License;
-
- return isLicenseError || hasErrorMessage ? (
- toggleErrorMessage(_executionStatus, item)}
- aria-label={itemIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand'}
- iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'}
- />
- ) : null;
- },
- },
- ];
- };
-
const authorizedRuleTypes = [...ruleTypesState.data.values()];
const authorizedToCreateAnyRules = authorizedRuleTypes.some(
(ruleType) => ruleType.authorizedConsumers[ALERTS_FEATURE_ID]?.all
@@ -979,13 +386,29 @@ export const RulesList: React.FunctionComponent = () => {
return [];
};
- const getRuleStatusFilter = () => {
+ const renderRuleStatusFilter = () => {
if (isRuleStatusFilterEnabled) {
- return [
- ,
- ];
+ return (
+
+ );
}
- return [];
+ return null;
+ };
+
+ const onDisableRule = (rule: RuleTableItem) => {
+ return disableRule({ http, id: rule.id });
+ };
+
+ const onEnableRule = (rule: RuleTableItem) => {
+ return enableRule({ http, id: rule.id });
+ };
+
+ const onSnoozeRule = (rule: RuleTableItem, snoozeEndTime: string | -1) => {
+ return snoozeRule({ http, id: rule.id, snoozeEndTime });
+ };
+
+ const onUnsnoozeRule = (rule: RuleTableItem) => {
+ return unsnoozeRule({ http, id: rule.id });
};
const toolsRight = [
@@ -999,8 +422,6 @@ export const RulesList: React.FunctionComponent = () => {
})
)}
/>,
- ...getRuleTagFilter(),
- ...getRuleStatusFilter(),
{
selectedStatuses={ruleExecutionStatusesFilter}
onChange={(ids: string[]) => setRuleExecutionStatusesFilter(ids)}
/>,
-
-
- ,
+ ...getRuleTagFilter(),
];
const authorizedToModifySelectedRules = selectedIds.length
@@ -1074,7 +484,7 @@ export const RulesList: React.FunctionComponent = () => {
})}
onPerformingAction={() => setIsPerformingAction(true)}
onActionPerformed={() => {
- loadRulesData();
+ loadData();
setIsPerformingAction(false);
}}
setRulesToDelete={setRulesToDelete}
@@ -1119,20 +529,19 @@ export const RulesList: React.FunctionComponent = () => {
)}
/>
+ {renderRuleStatusFilter()}
-
+
{toolsRight.map((tool, index: number) => (
-
- {tool}
-
+ {tool}
))}
-
+
-
+
{
/>
+
{rulesStatusesTotal.error > 0 && (
@@ -1235,64 +645,66 @@ export const RulesList: React.FunctionComponent = () => {
)}
-
- ({
- 'data-test-subj': 'rule-row',
- className: !ruleTypesState.data.get(item.ruleTypeId)?.enabledInLicense
- ? 'actRulesList__tableRowDisabled'
- : '',
- })}
- cellProps={(item: RuleTableItem) => ({
- 'data-test-subj': 'cell',
- className: !ruleTypesState.data.get(item.ruleTypeId)?.enabledInLicense
- ? 'actRulesList__tableCellDisabled'
- : '',
- })}
- data-test-subj="rulesList"
- pagination={{
- pageIndex: page.index,
- pageSize: page.size,
- /* Don't display rule count until we have the rule types initialized */
- totalItemCount: ruleTypesState.isInitialized === false ? 0 : rulesState.totalItemCount,
- }}
- selection={{
- selectable: (rule: RuleTableItem) => rule.isEditable,
- onSelectionChange(updatedSelectedItemsList: RuleTableItem[]) {
- setSelectedIds(updatedSelectedItemsList.map((item) => item.id));
- },
+ loadData()}
+ onRuleClick={(rule) => {
+ history.push(routeToRuleDetails.replace(`:ruleId`, rule.id));
}}
- onChange={({
- page: changedPage,
- sort: changedSort,
- }: {
- page?: Pagination;
- sort?: EuiTableSortingType['sort'];
- }) => {
- if (changedPage) {
- setPage(changedPage);
- }
- if (changedSort) {
- setSort(changedSort);
+ onRuleEditClick={(rule) => {
+ if (rule.isEditable && isRuleTypeEditableInContext(rule.ruleTypeId)) {
+ onRuleEdit(rule);
}
}}
- itemIdToExpandedRowMap={itemIdToExpandedRowMap}
- isExpandable={true}
+ onRuleDeleteClick={(rule) => setRulesToDelete([rule.id])}
+ onManageLicenseClick={(rule) =>
+ setManageLicenseModalOpts({
+ licenseType: ruleTypesState.data.get(rule.ruleTypeId)?.minimumLicenseRequired!,
+ ruleTypeId: rule.ruleTypeId,
+ })
+ }
+ onSelectionChange={(updatedSelectedItemsList) =>
+ setSelectedIds(updatedSelectedItemsList.map((item) => item.id))
+ }
+ onPercentileOptionsChange={setPercentileOptions}
+ onDisableRule={onDisableRule}
+ onEnableRule={onEnableRule}
+ onSnoozeRule={onSnoozeRule}
+ onUnsnoozeRule={onUnsnoozeRule}
+ renderCollapsedItemActions={(rule) => (
+ loadData()}
+ setRulesToDelete={setRulesToDelete}
+ onEditRule={() => onRuleEdit(rule)}
+ onUpdateAPIKey={setRulesToUpdateAPIKey}
+ />
+ )}
+ renderRuleError={(rule) => {
+ const _executionStatus = rule.executionStatus;
+ const hasErrorMessage = _executionStatus.status === 'error';
+ const isLicenseError =
+ _executionStatus.error?.reason === RuleExecutionStatusErrorReasons.License;
+
+ return isLicenseError || hasErrorMessage ? (
+ toggleErrorMessage(_executionStatus, rule)}
+ aria-label={itemIdToExpandedRowMap[rule.id] ? 'Collapse' : 'Expand'}
+ iconType={itemIdToExpandedRowMap[rule.id] ? 'arrowUp' : 'arrowDown'}
+ />
+ ) : null;
+ }}
+ config={config}
/>
{manageLicenseModalOpts && (
{
onDeleted={async () => {
setRulesToDelete([]);
setSelectedIds([]);
- await loadRulesData();
+ await loadData();
}}
onErrors={async () => {
- // Refresh the rules from the server, some rules may have been deleted
- await loadRulesData();
+ // Refresh the rules from the server, some rules may have beend deleted
+ await loadData();
setRulesToDelete([]);
}}
onCancel={() => {
@@ -1364,7 +776,7 @@ export const RulesList: React.FunctionComponent = () => {
}}
onUpdated={async () => {
setRulesToUpdateAPIKey([]);
- await loadRulesData();
+ await loadData();
}}
/>
@@ -1378,7 +790,7 @@ export const RulesList: React.FunctionComponent = () => {
actionTypeRegistry={actionTypeRegistry}
ruleTypeRegistry={ruleTypeRegistry}
ruleTypeIndex={ruleTypesState.data}
- onSave={loadRulesData}
+ onSave={loadData}
/>
)}
{editFlyoutVisible && currentRuleToEdit && (
@@ -1392,7 +804,7 @@ export const RulesList: React.FunctionComponent = () => {
ruleType={
ruleTypesState.data.get(currentRuleToEdit.ruleTypeId) as RuleType
}
- onSave={loadRulesData}
+ onSave={loadData}
/>
)}
@@ -1427,30 +839,3 @@ const noPermissionPrompt = (
function filterRulesById(rules: Rule[], ids: string[]): Rule[] {
return rules.filter((rule) => ids.includes(rule.id));
}
-
-interface ConvertRulesToTableItemsOpts {
- rules: Rule[];
- ruleTypeIndex: RuleTypeIndex;
- canExecuteActions: boolean;
- config: TriggersActionsUiConfig;
-}
-
-function convertRulesToTableItems(opts: ConvertRulesToTableItemsOpts): RuleTableItem[] {
- const { rules, ruleTypeIndex, canExecuteActions, config } = opts;
- const minimumDuration = config.minimumScheduleInterval
- ? parseDuration(config.minimumScheduleInterval.value)
- : 0;
- return rules.map((rule, index: number) => {
- return {
- ...rule,
- index,
- actionsCount: rule.actions.length,
- ruleType: ruleTypeIndex.get(rule.ruleTypeId)?.name ?? rule.ruleTypeId,
- isEditable:
- hasAllPrivilege(rule, ruleTypeIndex.get(rule.ruleTypeId)) &&
- (canExecuteActions || (!canExecuteActions && !rule.actions.length)),
- enabledInLicense: !!ruleTypeIndex.get(rule.ruleTypeId)?.enabledInLicense,
- showIntervalWarning: parseDuration(rule.schedule.interval) < minimumDuration,
- };
- });
-}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_auto_refresh.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_auto_refresh.test.tsx
new file mode 100644
index 0000000000000..9e17561ce652b
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_auto_refresh.test.tsx
@@ -0,0 +1,87 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React from 'react';
+import moment from 'moment';
+import { mountWithIntl } from '@kbn/test-jest-helpers';
+import { act } from 'react-dom/test-utils';
+import { RulesListAutoRefresh } from './rules_list_auto_refresh';
+
+const onRefresh = jest.fn();
+
+describe('RulesListAutoRefresh', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders the update text correctly', async () => {
+ jest.useFakeTimers('modern').setSystemTime(moment('1990-01-01').toDate());
+
+ const wrapper = mountWithIntl(
+
+ );
+
+ expect(
+ wrapper.find('[data-test-subj="rulesListAutoRefresh-lastUpdateText"]').first().text()
+ ).toEqual('Updated a few seconds ago');
+
+ await act(async () => {
+ jest.advanceTimersByTime(1 * 60 * 1000);
+ });
+
+ expect(
+ wrapper.find('[data-test-subj="rulesListAutoRefresh-lastUpdateText"]').first().text()
+ ).toEqual('Updated a minute ago');
+
+ await act(async () => {
+ jest.advanceTimersByTime(1 * 60 * 1000);
+ });
+
+ expect(
+ wrapper.find('[data-test-subj="rulesListAutoRefresh-lastUpdateText"]').first().text()
+ ).toEqual('Updated 2 minutes ago');
+
+ await act(async () => {
+ jest.runOnlyPendingTimers();
+ });
+ });
+
+ it('calls onRefresh when it auto refreshes', async () => {
+ jest.useFakeTimers('modern').setSystemTime(moment('1990-01-01').toDate());
+
+ mountWithIntl(
+
+ );
+
+ expect(onRefresh).toHaveBeenCalledTimes(0);
+
+ await act(async () => {
+ jest.advanceTimersByTime(1000);
+ });
+
+ expect(onRefresh).toHaveBeenCalledTimes(1);
+
+ await act(async () => {
+ jest.advanceTimersByTime(1000);
+ });
+
+ expect(onRefresh).toHaveBeenCalledTimes(2);
+
+ await act(async () => {
+ jest.advanceTimersByTime(10 * 1000);
+ });
+
+ expect(onRefresh).toHaveBeenCalledTimes(12);
+
+ await act(async () => {
+ jest.runOnlyPendingTimers();
+ });
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_auto_refresh.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_auto_refresh.tsx
new file mode 100644
index 0000000000000..eea8d8e5f1bbe
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_auto_refresh.tsx
@@ -0,0 +1,122 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+import React, { useCallback, useEffect, useState, useRef } from 'react';
+import moment from 'moment';
+import { EuiFlexGroup, EuiFlexItem, EuiText, EuiAutoRefreshButton } from '@elastic/eui';
+
+interface RulesListAutoRefreshProps {
+ lastUpdate: string;
+ initialUpdateInterval?: number;
+ onRefresh: () => void;
+}
+
+const flexGroupStyle = {
+ marginLeft: 'auto',
+};
+
+const getLastUpdateText = (lastUpdate: string) => {
+ if (!moment(lastUpdate).isValid()) {
+ return '';
+ }
+ return i18n.translate(
+ 'xpack.triggersActionsUI.sections.rulesList.rulesListAutoRefresh.lastUpdateText',
+ {
+ defaultMessage: 'Updated {lastUpdateText}',
+ values: {
+ lastUpdateText: moment(lastUpdate).fromNow(),
+ },
+ }
+ );
+};
+
+const TEXT_UPDATE_INTERVAL = 60 * 1000;
+const DEFAULT_REFRESH_INTERVAL = 5 * 60 * 1000;
+const MIN_REFRESH_INTERVAL = 1000;
+
+export const RulesListAutoRefresh = (props: RulesListAutoRefreshProps) => {
+ const { lastUpdate, initialUpdateInterval = DEFAULT_REFRESH_INTERVAL, onRefresh } = props;
+
+ const [isPaused, setIsPaused] = useState(false);
+ const [refreshInterval, setRefreshInterval] = useState(
+ Math.max(initialUpdateInterval, MIN_REFRESH_INTERVAL)
+ );
+ const [lastUpdateText, setLastUpdateText] = useState('');
+
+ const cachedOnRefresh = useRef<() => void>(() => {});
+ const textUpdateTimeout = useRef();
+ const refreshTimeout = useRef();
+
+ useEffect(() => {
+ cachedOnRefresh.current = onRefresh;
+ }, [onRefresh]);
+
+ useEffect(() => {
+ setLastUpdateText(getLastUpdateText(lastUpdate));
+
+ const poll = () => {
+ textUpdateTimeout.current = window.setTimeout(() => {
+ setLastUpdateText(getLastUpdateText(lastUpdate));
+ poll();
+ }, TEXT_UPDATE_INTERVAL);
+ };
+ poll();
+
+ return () => {
+ if (textUpdateTimeout.current) {
+ clearTimeout(textUpdateTimeout.current);
+ }
+ };
+ }, [lastUpdate, setLastUpdateText]);
+
+ useEffect(() => {
+ if (isPaused) {
+ return;
+ }
+
+ const poll = () => {
+ refreshTimeout.current = window.setTimeout(() => {
+ cachedOnRefresh.current();
+ poll();
+ }, refreshInterval);
+ };
+ poll();
+
+ return () => {
+ if (refreshTimeout.current) {
+ clearTimeout(refreshTimeout.current);
+ }
+ };
+ }, [isPaused, refreshInterval]);
+
+ const onRefreshChange = useCallback(
+ ({ isPaused: newIsPaused, refreshInterval: newRefreshInterval }) => {
+ setIsPaused(newIsPaused);
+ setRefreshInterval(newRefreshInterval);
+ },
+ [setIsPaused, setRefreshInterval]
+ );
+
+ return (
+
+
+
+ {lastUpdateText}
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_notify_badge.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_notify_badge.tsx
new file mode 100644
index 0000000000000..1f03c76a7de0b
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_notify_badge.tsx
@@ -0,0 +1,224 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useCallback, useMemo, useState } from 'react';
+import moment from 'moment';
+import { EuiButton, EuiButtonIcon, EuiPopover, EuiText, EuiToolTip } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { isRuleSnoozed } from './rule_status_dropdown';
+import { RuleTableItem } from '../../../../types';
+import {
+ SnoozePanel,
+ futureTimeToInterval,
+ usePreviousSnoozeInterval,
+ SnoozeUnit,
+} from './rule_status_dropdown';
+
+export interface RulesListNotifyBadgeProps {
+ rule: RuleTableItem;
+ isOpen: boolean;
+ previousSnoozeInterval?: string | null;
+ onClick: React.MouseEventHandler;
+ onClose: () => void;
+ onRuleChanged: () => void;
+ snoozeRule: (snoozeEndTime: string | -1, interval: string | null) => Promise;
+ unsnoozeRule: () => Promise;
+}
+
+const openSnoozePanelAriaLabel = i18n.translate(
+ 'xpack.triggersActionsUI.sections.rulesList.rulesListNotifyBadge.openSnoozePanel',
+ { defaultMessage: 'Open snooze panel' }
+);
+
+export const RulesListNotifyBadge: React.FunctionComponent = (props) => {
+ const {
+ rule,
+ isOpen,
+ previousSnoozeInterval: propsPreviousSnoozeInterval,
+ onClick,
+ onClose,
+ onRuleChanged,
+ snoozeRule,
+ unsnoozeRule,
+ } = props;
+
+ const { isSnoozedUntil, muteAll } = rule;
+
+ const [previousSnoozeInterval, setPreviousSnoozeInterval] = usePreviousSnoozeInterval(
+ propsPreviousSnoozeInterval
+ );
+
+ const [isLoading, setIsLoading] = useState(false);
+
+ const isSnoozedIndefinitely = muteAll;
+
+ const isSnoozed = useMemo(() => {
+ return isRuleSnoozed(rule);
+ }, [rule]);
+
+ const isScheduled = useMemo(() => {
+ // TODO: Implement scheduled check
+ return false;
+ }, []);
+
+ const formattedSnoozeText = useMemo(() => {
+ if (!isSnoozedUntil) {
+ return '';
+ }
+ return moment(isSnoozedUntil).format('MMM D');
+ }, [isSnoozedUntil]);
+
+ const snoozeTooltipText = useMemo(() => {
+ if (isSnoozedIndefinitely) {
+ return i18n.translate(
+ 'xpack.triggersActionsUI.sections.rulesList.rulesListNotifyBadge.snoozedIndefinitelyTooltip',
+ { defaultMessage: 'Notifications snoozed indefinitely' }
+ );
+ }
+ if (isScheduled) {
+ return '';
+ // TODO: Implement scheduled tooltip
+ }
+ if (isSnoozed) {
+ return i18n.translate(
+ 'xpack.triggersActionsUI.sections.rulesList.rulesListNotifyBadge.snoozedTooltip',
+ {
+ defaultMessage: 'Notifications snoozed for {snoozeTime}',
+ values: {
+ snoozeTime: moment(isSnoozedUntil).fromNow(true),
+ },
+ }
+ );
+ }
+ return '';
+ }, [isSnoozedIndefinitely, isScheduled, isSnoozed, isSnoozedUntil]);
+
+ const snoozedButton = useMemo(() => {
+ return (
+
+ {formattedSnoozeText}
+
+ );
+ }, [formattedSnoozeText, onClick]);
+
+ const scheduledSnoozeButton = useMemo(() => {
+ // TODO: Implement scheduled snooze button
+ return (
+
+ {formattedSnoozeText}
+
+ );
+ }, [formattedSnoozeText, onClick]);
+
+ const unsnoozedButton = useMemo(() => {
+ return (
+
+ );
+ }, [isOpen, onClick]);
+
+ const indefiniteSnoozeButton = useMemo(() => {
+ return (
+
+ );
+ }, [onClick]);
+
+ const button = useMemo(() => {
+ if (isScheduled) {
+ return scheduledSnoozeButton;
+ }
+ if (isSnoozedIndefinitely) {
+ return indefiniteSnoozeButton;
+ }
+ if (isSnoozed) {
+ return snoozedButton;
+ }
+ return unsnoozedButton;
+ }, [
+ isSnoozed,
+ isScheduled,
+ isSnoozedIndefinitely,
+ scheduledSnoozeButton,
+ snoozedButton,
+ indefiniteSnoozeButton,
+ unsnoozedButton,
+ ]);
+
+ const buttonWithToolTip = useMemo(() => {
+ if (isOpen) {
+ return button;
+ }
+ return {button};
+ }, [isOpen, button, snoozeTooltipText]);
+
+ const snoozeRuleAndStoreInterval = useCallback(
+ (newSnoozeEndTime: string | -1, interval: string | null) => {
+ if (interval) {
+ setPreviousSnoozeInterval(interval);
+ }
+ return snoozeRule(newSnoozeEndTime, interval);
+ },
+ [setPreviousSnoozeInterval, snoozeRule]
+ );
+
+ const onChangeSnooze = useCallback(
+ async (value: number, unit?: SnoozeUnit) => {
+ setIsLoading(true);
+ try {
+ if (value === -1) {
+ await snoozeRuleAndStoreInterval(-1, null);
+ } else if (value !== 0) {
+ const newSnoozeEndTime = moment().add(value, unit).toISOString();
+ await snoozeRuleAndStoreInterval(newSnoozeEndTime, `${value}${unit}`);
+ } else await unsnoozeRule();
+ onRuleChanged();
+ } finally {
+ onClose();
+ setIsLoading(false);
+ }
+ },
+ [onRuleChanged, onClose, snoozeRuleAndStoreInterval, unsnoozeRule, setIsLoading]
+ );
+
+ return (
+
+
+
+ );
+};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table.tsx
new file mode 100644
index 0000000000000..53a3b4b69f8c0
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table.tsx
@@ -0,0 +1,724 @@
+/*
+ * 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, { useMemo, useState } from 'react';
+import moment from 'moment';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n-react';
+import {
+ EuiBasicTable,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIconTip,
+ EuiLink,
+ EuiButtonEmpty,
+ EuiHealth,
+ EuiText,
+ EuiToolTip,
+ EuiTableSortingType,
+ EuiButtonIcon,
+ EuiSelectableOption,
+ EuiIcon,
+ EuiScreenReaderOnly,
+ RIGHT_ALIGNMENT,
+ EuiTableFieldDataColumnType,
+ EuiTableComputedColumnType,
+ EuiTableActionsColumnType,
+} from '@elastic/eui';
+import {
+ RuleExecutionStatus,
+ RuleExecutionStatusErrorReasons,
+ formatDuration,
+ parseDuration,
+ MONITORING_HISTORY_LIMIT,
+} from '@kbn/alerting-plugin/common';
+import { rulesStatusesTranslationsMapping, ALERT_STATUS_LICENSE_ERROR } from '../translations';
+import { getHealthColor } from './rule_execution_status_filter';
+import {
+ Rule,
+ RuleTableItem,
+ RuleTypeIndex,
+ Pagination,
+ Percentiles,
+ TriggersActionsUiConfig,
+ RuleTypeRegistryContract,
+} from '../../../../types';
+import { shouldShowDurationWarning } from '../../../lib/execution_duration_utils';
+import { PercentileSelectablePopover } from './percentile_selectable_popover';
+import { RuleDurationFormat } from './rule_duration_format';
+import { checkRuleTypeEnabled } from '../../../lib/check_rule_type_enabled';
+import { getFormattedSuccessRatio } from '../../../lib/monitoring_utils';
+import { hasAllPrivilege } from '../../../lib/capabilities';
+import { RuleTagBadge } from './rule_tag_badge';
+import { RuleStatusDropdown } from './rule_status_dropdown';
+import { RulesListNotifyBadge } from './rules_list_notify_badge';
+
+interface RuleTypeState {
+ isLoading: boolean;
+ isInitialized: boolean;
+ data: RuleTypeIndex;
+}
+
+export interface RuleState {
+ isLoading: boolean;
+ data: Rule[];
+ totalItemCount: number;
+}
+
+const percentileOrdinals = {
+ [Percentiles.P50]: '50th',
+ [Percentiles.P95]: '95th',
+ [Percentiles.P99]: '99th',
+};
+
+export const percentileFields = {
+ [Percentiles.P50]: 'monitoring.execution.calculated_metrics.p50',
+ [Percentiles.P95]: 'monitoring.execution.calculated_metrics.p95',
+ [Percentiles.P99]: 'monitoring.execution.calculated_metrics.p99',
+};
+
+const EMPTY_OBJECT = {};
+const EMPTY_HANDLER = () => {};
+const EMPTY_RENDER = () => null;
+
+interface ConvertRulesToTableItemsOpts {
+ rules: Rule[];
+ ruleTypeIndex: RuleTypeIndex;
+ canExecuteActions: boolean;
+ config: TriggersActionsUiConfig;
+}
+
+export interface RulesListTableProps {
+ rulesState: RuleState;
+ ruleTypesState: RuleTypeState;
+ ruleTypeRegistry: RuleTypeRegistryContract;
+ isLoading?: boolean;
+ sort: EuiTableSortingType['sort'];
+ page: Pagination;
+ percentileOptions: EuiSelectableOption[];
+ canExecuteActions?: boolean;
+ itemIdToExpandedRowMap?: Record;
+ config: TriggersActionsUiConfig;
+ onSort?: (sort: EuiTableSortingType['sort']) => void;
+ onPage?: (page: Pagination) => void;
+ onRuleClick?: (rule: RuleTableItem) => void;
+ onRuleEditClick?: (rule: RuleTableItem) => void;
+ onRuleDeleteClick?: (rule: RuleTableItem) => void;
+ onManageLicenseClick?: (rule: RuleTableItem) => void;
+ onTagClick?: (rule: RuleTableItem) => void;
+ onTagClose?: (rule: RuleTableItem) => void;
+ onSelectionChange?: (updatedSelectedItemsList: RuleTableItem[]) => void;
+ onPercentileOptionsChange?: (options: EuiSelectableOption[]) => void;
+ onRuleChanged: () => void;
+ onEnableRule: (rule: RuleTableItem) => Promise;
+ onDisableRule: (rule: RuleTableItem) => Promise;
+ onSnoozeRule: (rule: RuleTableItem, snoozeEndTime: string | -1) => Promise;
+ onUnsnoozeRule: (rule: RuleTableItem) => Promise;
+ renderCollapsedItemActions?: (rule: RuleTableItem) => React.ReactNode;
+ renderRuleError?: (rule: RuleTableItem) => React.ReactNode;
+}
+
+interface ConvertRulesToTableItemsOpts {
+ rules: Rule[];
+ ruleTypeIndex: RuleTypeIndex;
+ canExecuteActions: boolean;
+ config: TriggersActionsUiConfig;
+}
+
+export function convertRulesToTableItems(opts: ConvertRulesToTableItemsOpts): RuleTableItem[] {
+ const { rules, ruleTypeIndex, canExecuteActions, config } = opts;
+ const minimumDuration = config.minimumScheduleInterval
+ ? parseDuration(config.minimumScheduleInterval.value)
+ : 0;
+ return rules.map((rule, index: number) => {
+ return {
+ ...rule,
+ index,
+ actionsCount: rule.actions.length,
+ ruleType: ruleTypeIndex.get(rule.ruleTypeId)?.name ?? rule.ruleTypeId,
+ isEditable:
+ hasAllPrivilege(rule, ruleTypeIndex.get(rule.ruleTypeId)) &&
+ (canExecuteActions || (!canExecuteActions && !rule.actions.length)),
+ enabledInLicense: !!ruleTypeIndex.get(rule.ruleTypeId)?.enabledInLicense,
+ showIntervalWarning: parseDuration(rule.schedule.interval) < minimumDuration,
+ };
+ });
+}
+
+export const RulesListTable = (props: RulesListTableProps) => {
+ const {
+ rulesState,
+ ruleTypesState,
+ ruleTypeRegistry,
+ isLoading = false,
+ canExecuteActions = false,
+ sort,
+ page,
+ percentileOptions,
+ itemIdToExpandedRowMap = EMPTY_OBJECT,
+ config = EMPTY_OBJECT as TriggersActionsUiConfig,
+ onSort = EMPTY_HANDLER,
+ onPage = EMPTY_HANDLER,
+ onRuleClick = EMPTY_HANDLER,
+ onRuleEditClick = EMPTY_HANDLER,
+ onRuleDeleteClick = EMPTY_HANDLER,
+ onManageLicenseClick = EMPTY_HANDLER,
+ onSelectionChange = EMPTY_HANDLER,
+ onPercentileOptionsChange = EMPTY_HANDLER,
+ onRuleChanged = EMPTY_HANDLER,
+ onEnableRule = EMPTY_HANDLER,
+ onDisableRule = EMPTY_HANDLER,
+ onSnoozeRule = EMPTY_HANDLER,
+ onUnsnoozeRule = EMPTY_HANDLER,
+ renderCollapsedItemActions = EMPTY_RENDER,
+ renderRuleError = EMPTY_RENDER,
+ } = props;
+
+ const [tagPopoverOpenIndex, setTagPopoverOpenIndex] = useState(-1);
+ const [currentlyOpenNotify, setCurrentlyOpenNotify] = useState();
+
+ const selectedPercentile = useMemo(() => {
+ const selectedOption = percentileOptions.find((option) => option.checked === 'on');
+ if (selectedOption) {
+ return Percentiles[selectedOption.key as Percentiles];
+ }
+ }, [percentileOptions]);
+
+ const renderPercentileColumnName = () => {
+ return (
+
+
+
+ {selectedPercentile}
+
+
+
+
+
+ );
+ };
+
+ const renderPercentileCellValue = (value: number) => {
+ return (
+
+
+
+ );
+ };
+
+ const renderRuleStatusDropdown = (ruleEnabled: boolean | undefined, rule: RuleTableItem) => {
+ return (
+ await onDisableRule(rule)}
+ enableRule={async () => await onEnableRule(rule)}
+ snoozeRule={async (snoozeEndTime: string | -1, interval: string | null) => {
+ await onSnoozeRule(rule, snoozeEndTime);
+ }}
+ unsnoozeRule={async () => await onUnsnoozeRule(rule)}
+ rule={rule}
+ onRuleChanged={onRuleChanged}
+ isEditable={rule.isEditable && isRuleTypeEditableInContext(rule.ruleTypeId)}
+ />
+ );
+ };
+
+ const isRuleTypeEditableInContext = (ruleTypeId: string) =>
+ ruleTypeRegistry.has(ruleTypeId) ? !ruleTypeRegistry.get(ruleTypeId).requiresAppContext : false;
+
+ const renderRuleExecutionStatus = (executionStatus: RuleExecutionStatus, rule: RuleTableItem) => {
+ const healthColor = getHealthColor(executionStatus.status);
+ const tooltipMessage =
+ executionStatus.status === 'error' ? `Error: ${executionStatus?.error?.message}` : null;
+ const isLicenseError =
+ executionStatus.error?.reason === RuleExecutionStatusErrorReasons.License;
+ const statusMessage = isLicenseError
+ ? ALERT_STATUS_LICENSE_ERROR
+ : rulesStatusesTranslationsMapping[executionStatus.status];
+
+ const health = (
+
+ {statusMessage}
+
+ );
+
+ const healthWithTooltip = tooltipMessage ? (
+
+ {health}
+
+ ) : (
+ health
+ );
+
+ return (
+
+ {healthWithTooltip}
+ {isLicenseError && (
+
+ onManageLicenseClick(rule)}
+ >
+
+
+
+ )}
+
+ );
+ };
+
+ const getRulesTableColumns = (): Array<
+ | EuiTableFieldDataColumnType
+ | EuiTableComputedColumnType
+ | EuiTableActionsColumnType
+ > => {
+ return [
+ {
+ field: 'name',
+ name: i18n.translate(
+ 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.nameTitle',
+ { defaultMessage: 'Name' }
+ ),
+ sortable: true,
+ truncateText: true,
+ width: '30%',
+ 'data-test-subj': 'rulesTableCell-name',
+ render: (name: string, rule: RuleTableItem) => {
+ const ruleType = ruleTypesState.data.get(rule.ruleTypeId);
+ const checkEnabledResult = checkRuleTypeEnabled(ruleType);
+ const link = (
+ <>
+
+
+
+
+ onRuleClick(rule)}>
+ {name}
+
+
+
+ {!checkEnabledResult.isEnabled && (
+
+ )}
+
+
+
+
+
+ {rule.ruleType}
+
+
+
+ >
+ );
+ return <>{link}>;
+ },
+ },
+ {
+ field: 'tags',
+ name: '',
+ sortable: false,
+ width: '50px',
+ 'data-test-subj': 'rulesTableCell-tagsPopover',
+ render: (ruleTags: string[], rule: RuleTableItem) => {
+ return ruleTags.length > 0 ? (
+ setTagPopoverOpenIndex(rule.index)}
+ onClose={() => setTagPopoverOpenIndex(-1)}
+ />
+ ) : null;
+ },
+ },
+ {
+ field: 'executionStatus.lastExecutionDate',
+ name: (
+
+
+ Last run{' '}
+
+
+
+ ),
+ sortable: true,
+ width: '15%',
+ 'data-test-subj': 'rulesTableCell-lastExecutionDate',
+ render: (date: Date) => {
+ if (date) {
+ return (
+ <>
+
+
+ {moment(date).format('MMM D, YYYY HH:mm:ssa')}
+
+
+
+ {moment(date).fromNow()}
+
+
+
+ >
+ );
+ }
+ },
+ },
+ {
+ name: 'Notify',
+ width: '16%',
+ 'data-test-subj': 'rulesTableCell-rulesListNotify',
+ render: (rule: RuleTableItem) => {
+ return (
+ setCurrentlyOpenNotify(rule.id)}
+ onClose={() => setCurrentlyOpenNotify('')}
+ onRuleChanged={onRuleChanged}
+ snoozeRule={async (snoozeEndTime: string | -1, interval: string | null) => {
+ await onSnoozeRule(rule, snoozeEndTime);
+ }}
+ unsnoozeRule={async () => await onUnsnoozeRule(rule)}
+ />
+ );
+ },
+ },
+ {
+ field: 'schedule.interval',
+ width: '6%',
+ name: i18n.translate(
+ 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.scheduleTitle',
+ { defaultMessage: 'Interval' }
+ ),
+ sortable: false,
+ truncateText: false,
+ 'data-test-subj': 'rulesTableCell-interval',
+ render: (interval: string, rule: RuleTableItem) => {
+ const durationString = formatDuration(interval);
+ return (
+ <>
+
+ {durationString}
+
+ {rule.showIntervalWarning && (
+
+ onRuleEditClick(rule)}
+ iconType="flag"
+ aria-label={i18n.translate(
+ 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.intervalIconAriaLabel',
+ { defaultMessage: 'Below configured minimum interval' }
+ )}
+ />
+
+ )}
+
+
+ >
+ );
+ },
+ },
+ {
+ field: 'executionStatus.lastDuration',
+ width: '12%',
+ name: (
+
+
+ Duration{' '}
+
+
+
+ ),
+ sortable: true,
+ truncateText: false,
+ 'data-test-subj': 'rulesTableCell-duration',
+ render: (value: number, rule: RuleTableItem) => {
+ const showDurationWarning = shouldShowDurationWarning(
+ ruleTypesState.data.get(rule.ruleTypeId),
+ value
+ );
+
+ return (
+ <>
+ {}
+ {showDurationWarning && (
+
+ )}
+ >
+ );
+ },
+ },
+ {
+ mobileOptions: { header: false },
+ field: percentileFields[selectedPercentile!],
+ width: '16%',
+ name: renderPercentileColumnName(),
+ 'data-test-subj': 'rulesTableCell-ruleExecutionPercentile',
+ sortable: true,
+ truncateText: false,
+ render: renderPercentileCellValue,
+ },
+ {
+ field: 'monitoring.execution.calculated_metrics.success_ratio',
+ width: '12%',
+ name: (
+
+
+ Success ratio{' '}
+
+
+
+ ),
+ sortable: true,
+ truncateText: false,
+ 'data-test-subj': 'rulesTableCell-successRatio',
+ render: (value: number) => {
+ return (
+
+ {value !== undefined ? getFormattedSuccessRatio(value) : 'N/A'}
+
+ );
+ },
+ },
+ {
+ field: 'executionStatus.status',
+ name: i18n.translate(
+ 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.lastResponseTitle',
+ { defaultMessage: 'Last response' }
+ ),
+ sortable: true,
+ truncateText: false,
+ width: '120px',
+ 'data-test-subj': 'rulesTableCell-lastResponse',
+ render: (_executionStatus: RuleExecutionStatus, rule: RuleTableItem) => {
+ return renderRuleExecutionStatus(rule.executionStatus, rule);
+ },
+ },
+ {
+ field: 'enabled',
+ name: i18n.translate(
+ 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.stateTitle',
+ { defaultMessage: 'State' }
+ ),
+ sortable: true,
+ truncateText: false,
+ width: '10%',
+ 'data-test-subj': 'rulesTableCell-status',
+ render: (_enabled: boolean | undefined, rule: RuleTableItem) => {
+ return renderRuleStatusDropdown(rule.enabled, rule);
+ },
+ },
+ {
+ name: '',
+ width: '90px',
+ render(rule: RuleTableItem) {
+ return (
+
+
+
+ {rule.isEditable && isRuleTypeEditableInContext(rule.ruleTypeId) ? (
+
+ onRuleEditClick(rule)}
+ iconType={'pencil'}
+ aria-label={i18n.translate(
+ 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.editAriaLabel',
+ { defaultMessage: 'Edit' }
+ )}
+ />
+
+ ) : null}
+ {rule.isEditable ? (
+
+ onRuleDeleteClick(rule)}
+ iconType={'trash'}
+ aria-label={i18n.translate(
+ 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.deleteAriaLabel',
+ { defaultMessage: 'Delete' }
+ )}
+ />
+
+ ) : null}
+
+
+ {renderCollapsedItemActions(rule)}
+
+ );
+ },
+ },
+ {
+ align: RIGHT_ALIGNMENT,
+ width: '40px',
+ isExpander: true,
+ name: (
+
+ Expand rows
+
+ ),
+ render: renderRuleError,
+ },
+ ];
+ };
+
+ return (
+ ({
+ 'data-test-subj': 'rule-row',
+ className: !ruleTypesState.data.get(rule.ruleTypeId)?.enabledInLicense
+ ? 'actRulesList__tableRowDisabled'
+ : '',
+ })}
+ cellProps={(rule: RuleTableItem) => ({
+ 'data-test-subj': 'cell',
+ className: !ruleTypesState.data.get(rule.ruleTypeId)?.enabledInLicense
+ ? 'actRulesList__tableCellDisabled'
+ : '',
+ })}
+ data-test-subj="rulesList"
+ pagination={{
+ pageIndex: page.index,
+ pageSize: page.size,
+ /* Don't display rule count until we have the rule types initialized */
+ totalItemCount: ruleTypesState.isInitialized === false ? 0 : rulesState.totalItemCount,
+ }}
+ selection={{
+ selectable: (rule: RuleTableItem) => rule.isEditable,
+ onSelectionChange,
+ }}
+ onChange={({
+ page: changedPage,
+ sort: changedSort,
+ }: {
+ page?: Pagination;
+ sort?: EuiTableSortingType['sort'];
+ }) => {
+ if (changedPage) {
+ onPage(changedPage);
+ }
+ if (changedSort) {
+ onSort(changedSort);
+ }
+ }}
+ itemIdToExpandedRowMap={itemIdToExpandedRowMap}
+ isExpandable={true}
+ />
+ );
+};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/type_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/type_filter.tsx
index 6ce697f65f898..f8cb70745911c 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/type_filter.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/type_filter.tsx
@@ -7,13 +7,7 @@
import React, { Fragment, useEffect, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
-import {
- EuiFilterGroup,
- EuiPopover,
- EuiFilterButton,
- EuiFilterSelectItem,
- EuiTitle,
-} from '@elastic/eui';
+import { EuiPopover, EuiFilterButton, EuiFilterSelectItem, EuiTitle } from '@elastic/eui';
interface TypeFilterProps {
options: Array<{
@@ -41,53 +35,51 @@ export const TypeFilter: React.FunctionComponent = ({
}, [selectedValues]);
return (
-
- setIsPopoverOpen(false)}
- button={
- 0}
- numActiveFilters={selectedValues.length}
- numFilters={selectedValues.length}
- onClick={() => setIsPopoverOpen(!isPopoverOpen)}
- data-test-subj="ruleTypeFilterButton"
- >
-
-
- }
- >
-
- {options.map((groupItem, groupIndex) => (
-
-
- {groupItem.groupName}
-
- {groupItem.subOptions.map((item, index) => (
- {
- const isPreviouslyChecked = selectedValues.includes(item.value);
- if (isPreviouslyChecked) {
- setSelectedValues(selectedValues.filter((val) => val !== item.value));
- } else {
- setSelectedValues(selectedValues.concat(item.value));
- }
- }}
- checked={selectedValues.includes(item.value) ? 'on' : undefined}
- data-test-subj={`ruleType${item.value}FilterOption`}
- >
- {item.name}
-
- ))}
-
- ))}
-
-
-
+ setIsPopoverOpen(false)}
+ button={
+ 0}
+ numActiveFilters={selectedValues.length}
+ numFilters={selectedValues.length}
+ onClick={() => setIsPopoverOpen(!isPopoverOpen)}
+ data-test-subj="ruleTypeFilterButton"
+ >
+
+
+ }
+ >
+
+ {options.map((groupItem, groupIndex) => (
+
+
+ {groupItem.groupName}
+
+ {groupItem.subOptions.map((item, index) => (
+ {
+ const isPreviouslyChecked = selectedValues.includes(item.value);
+ if (isPreviouslyChecked) {
+ setSelectedValues(selectedValues.filter((val) => val !== item.value));
+ } else {
+ setSelectedValues(selectedValues.concat(item.value));
+ }
+ }}
+ checked={selectedValues.includes(item.value) ? 'on' : undefined}
+ data-test-subj={`ruleType${item.value}FilterOption`}
+ >
+ {item.name}
+
+ ))}
+
+ ))}
+
+
);
};
diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_rules_list.tsx
new file mode 100644
index 0000000000000..b315668c4fab9
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/common/get_rules_list.tsx
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { RulesList } from '../application/sections';
+
+export const getRulesListLazy = () => {
+ return ;
+};
diff --git a/x-pack/plugins/triggers_actions_ui/public/mocks.ts b/x-pack/plugins/triggers_actions_ui/public/mocks.ts
index 75ca6d8fd2987..605d83a8eb32e 100644
--- a/x-pack/plugins/triggers_actions_ui/public/mocks.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/mocks.ts
@@ -30,6 +30,7 @@ import { getRuleTagFilterLazy } from './common/get_rule_tag_filter';
import { getRuleStatusFilterLazy } from './common/get_rule_status_filter';
import { getRuleTagBadgeLazy } from './common/get_rule_tag_badge';
import { getRuleEventLogListLazy } from './common/get_rule_event_log_list';
+import { getRulesListLazy } from './common/get_rules_list';
import { getAlertsTableStateLazy } from './common/get_alerts_table_state';
import { AlertsTableStateProps } from './application/sections/alerts_table/alerts_table_state';
@@ -85,6 +86,9 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart {
getRuleEventLogList: (props) => {
return getRuleEventLogListLazy(props);
},
+ getRulesList: () => {
+ return getRulesListLazy();
+ },
};
}
diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts
index f9df34a5e4abb..f2237ff22f4ae 100644
--- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts
@@ -35,6 +35,7 @@ import { getRuleTagFilterLazy } from './common/get_rule_tag_filter';
import { getRuleStatusFilterLazy } from './common/get_rule_status_filter';
import { getRuleTagBadgeLazy } from './common/get_rule_tag_badge';
import { getRuleEventLogListLazy } from './common/get_rule_event_log_list';
+import { getRulesListLazy } from './common/get_rules_list';
import { ExperimentalFeaturesService } from './common/experimental_features_service';
import {
ExperimentalFeatures,
@@ -91,6 +92,7 @@ export interface TriggersAndActionsUIPublicPluginStart {
getRuleStatusFilter: (props: RuleStatusFilterProps) => ReactElement;
getRuleTagBadge: (props: RuleTagBadgeProps) => ReactElement;
getRuleEventLogList: (props: RuleEventLogListProps) => ReactElement;
+ getRulesList: () => ReactElement;
}
interface PluginsSetup {
@@ -279,6 +281,9 @@ export class Plugin
getRuleEventLogList: (props: RuleEventLogListProps) => {
return getRuleEventLogListLazy(props);
},
+ getRulesList: () => {
+ return getRulesListLazy();
+ },
};
}
diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts
index c5ed118c105bb..832cf6c7a9078 100644
--- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts
@@ -20,5 +20,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => {
loadTestFile(require.resolve('./rule_status_filter'));
loadTestFile(require.resolve('./rule_tag_badge'));
loadTestFile(require.resolve('./rule_event_log_list'));
+ loadTestFile(require.resolve('./rules_list'));
});
};
diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_tag_filter.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_tag_filter.ts
index 77d57e2819db5..15ea8fc302622 100644
--- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_tag_filter.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_tag_filter.ts
@@ -10,7 +10,6 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default ({ getPageObjects, getService }: FtrProviderContext) => {
const testSubjects = getService('testSubjects');
- const find = getService('find');
const PageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']);
const esArchiver = getService('esArchiver');
@@ -31,24 +30,5 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const exists = await testSubjects.exists('ruleTagFilter');
expect(exists).to.be(true);
});
-
- it('should allow tag filters to be selected', async () => {
- let badge = await find.byCssSelector('.euiFilterButton__notification');
- expect(await badge.getVisibleText()).to.be('0');
-
- await testSubjects.click('ruleTagFilter');
- await testSubjects.click('ruleTagFilterOption-tag1');
-
- badge = await find.byCssSelector('.euiFilterButton__notification');
- expect(await badge.getVisibleText()).to.be('1');
-
- await testSubjects.click('ruleTagFilterOption-tag2');
-
- badge = await find.byCssSelector('.euiFilterButton__notification');
- expect(await badge.getVisibleText()).to.be('2');
-
- await testSubjects.click('ruleTagFilterOption-tag1');
- expect(await badge.getVisibleText()).to.be('1');
- });
});
};
diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_list.ts
new file mode 100644
index 0000000000000..30baba0caaa08
--- /dev/null
+++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_list.ts
@@ -0,0 +1,34 @@
+/*
+ * 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 ({ getPageObjects, getService }: FtrProviderContext) => {
+ const testSubjects = getService('testSubjects');
+ const PageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']);
+ const esArchiver = getService('esArchiver');
+
+ describe('Rules list', () => {
+ before(async () => {
+ await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts');
+ await PageObjects.common.navigateToUrlWithBrowserHistory(
+ 'triggersActions',
+ '/__components_sandbox'
+ );
+ });
+ after(async () => {
+ await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts');
+ });
+
+ it('shoud load from shareable lazy loader', async () => {
+ await testSubjects.find('rulesList');
+ const exists = await testSubjects.exists('rulesList');
+ expect(exists).to.be(true);
+ });
+ });
+};