diff --git a/cypress/integration/cluster_metrics_monitor_spec.js b/cypress/integration/cluster_metrics_monitor_spec.js
index 897766dfa..e66145e7c 100644
--- a/cypress/integration/cluster_metrics_monitor_spec.js
+++ b/cypress/integration/cluster_metrics_monitor_spec.js
@@ -15,7 +15,7 @@ const SAMPLE_DESTINATION = 'sample_destination';
const addClusterMetricsTrigger = (triggerName, triggerIndex, actionName, isEdit, source) => {
// Click 'Add trigger' button
- cy.contains('Add trigger', { timeout: 20000 }).click({ force: true });
+ cy.contains('Add trigger', { timeout: 30000 }).click({ force: true });
if (isEdit === true) {
// TODO: Passing button props in EUI accordion was added in newer versions (31.7.0+).
@@ -37,7 +37,7 @@ const addClusterMetricsTrigger = (triggerName, triggerIndex, actionName, isEdit,
force: true,
parseSpecialCharSequences: false,
delay: 5,
- timeout: 20000,
+ timeout: 30000,
})
.trigger('blur', { force: true });
});
@@ -76,7 +76,7 @@ describe('ClusterMetricsMonitor', () => {
cy.visit(`${Cypress.env('opensearch_dashboards')}/app/${PLUGIN_NAME}#/monitors`);
// Common text to wait for to confirm page loaded, give up to 20 seconds for initial load
- cy.contains('Create monitor', { timeout: 20000 });
+ cy.contains('Create monitor', { timeout: 30000 });
});
describe('can be created', () => {
@@ -87,7 +87,7 @@ describe('ClusterMetricsMonitor', () => {
it('for the Cluster Health API', () => {
// Go to create monitor page
- cy.contains('Create monitor', { timeout: 20000 }).click({ force: true });
+ cy.contains('Create monitor', { timeout: 30000 }).click({ force: true });
// Select ClusterMetrics radio card
cy.get('[data-test-subj="clusterMetricsMonitorRadioCard"]').click({ force: true });
@@ -141,7 +141,7 @@ describe('ClusterMetricsMonitor', () => {
it('for the Nodes Stats API', () => {
// Go to create monitor page
- cy.contains('Create monitor', { timeout: 20000 }).click({ force: true });
+ cy.contains('Create monitor', { timeout: 30000 }).click({ force: true });
// Select ClusterMetrics radio card
cy.get('[data-test-subj="clusterMetricsMonitorRadioCard"]').click({ force: true });
@@ -202,7 +202,7 @@ describe('ClusterMetricsMonitor', () => {
it('for the CAT Snapshots API', () => {
// Go to create monitor page
- cy.contains('Create monitor', { timeout: 20000 }).click({ force: true });
+ cy.contains('Create monitor', { timeout: 30000 }).click({ force: true });
// Select ClusterMetrics radio card
cy.get('[data-test-subj="clusterMetricsMonitorRadioCard"]').click({ force: true });
@@ -228,7 +228,7 @@ describe('ClusterMetricsMonitor', () => {
// Begin monitor creation
// Go to create monitor page
- cy.contains('Create monitor', { timeout: 20000 }).click({ force: true });
+ cy.contains('Create monitor', { timeout: 30000 }).click({ force: true });
// Select ClusterMetrics radio card
cy.get('[data-test-subj="clusterMetricsMonitorRadioCard"]').click({ force: true });
@@ -335,7 +335,7 @@ describe('ClusterMetricsMonitor', () => {
cy.get('[data-test-subj="clusterMetricsApiTypeComboBox"]').contains('Cluster settings');
// Confirm there are 0 triggers defined
- cy.contains('Triggers (0)', { timeout: 20000 });
+ cy.contains('Triggers (0)', { timeout: 30000 });
});
});
});
diff --git a/public/components/FeatureAnywhereContextMenu/AddAlertingMonitor/__snapshots__/AddAlertingMonitor.test.js.snap b/public/components/FeatureAnywhereContextMenu/AddAlertingMonitor/__snapshots__/AddAlertingMonitor.test.js.snap
index 4d1dfa1b6..3c2c05e33 100644
--- a/public/components/FeatureAnywhereContextMenu/AddAlertingMonitor/__snapshots__/AddAlertingMonitor.test.js.snap
+++ b/public/components/FeatureAnywhereContextMenu/AddAlertingMonitor/__snapshots__/AddAlertingMonitor.test.js.snap
@@ -19,6 +19,7 @@ exports[`AddAlertingMonitor renders 1`] = `
"associatedMonitorsList": Array [],
"bucketUnitOfTime": "h",
"bucketValue": 1,
+ "clusterNames": Array [],
"cronExpression": "0 */1 * * *",
"daily": 0,
"description": "",
@@ -60,6 +61,7 @@ exports[`AddAlertingMonitor renders 1`] = `
"timezone": Array [],
"uri": Object {
"api_type": "",
+ "clusters": Array [],
"path": "",
"path_params": "",
"url": "",
diff --git a/public/components/Flyout/flyouts/components/AlertsDashboardFlyoutComponent.js b/public/components/Flyout/flyouts/components/AlertsDashboardFlyoutComponent.js
index e95a89d9f..a6af885c6 100644
--- a/public/components/Flyout/flyouts/components/AlertsDashboardFlyoutComponent.js
+++ b/public/components/Flyout/flyouts/components/AlertsDashboardFlyoutComponent.js
@@ -24,6 +24,7 @@ import { getTime } from '../../../../pages/MonitorDetails/components/MonitorOver
import { PLUGIN_NAME } from '../../../../../utils/constants';
import {
ALERT_STATE,
+ DEFAULT_EMPTY_DATA,
MONITOR_GROUP_BY,
MONITOR_INPUT_DETECTOR_ID,
MONITOR_TYPE,
@@ -35,8 +36,6 @@ import { UNITS_OF_TIME } from '../../../../pages/CreateMonitor/components/Monito
import { DEFAULT_WHERE_EXPRESSION_TEXT } from '../../../../pages/CreateMonitor/components/MonitorExpressions/expressions/utils/whereHelpers';
import { acknowledgeAlerts, backendErrorNotification } from '../../../../utils/helpers';
import {
- displayAcknowledgedAlertsToast,
- filterActiveAlerts,
getQueryObjectFromState,
getURLQueryParams,
insertGroupByColumn,
@@ -54,6 +53,11 @@ import {
TABLE_TAB_IDS,
} from '../../../../pages/Dashboard/components/FindingsDashboard/findingsUtils';
import FindingsDashboard from '../../../../pages/Dashboard/containers/FindingsDashboard';
+import { CLUSTER_METRICS_CROSS_CLUSTER_ALERT_TABLE_COLUMN } from '../../../../pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorConstants';
+import {
+ getDataSources,
+ getLocalClusterName,
+} from '../../../../pages/CreateMonitor/components/CrossClusterConfigurations/utils/helpers';
export const DEFAULT_NUM_FLYOUT_ROWS = 10;
@@ -73,6 +77,7 @@ export default class AlertsDashboardFlyoutComponent extends Component {
alerts: [],
alertState: alertState,
loading: true,
+ localClusterName: undefined,
monitor: monitor,
monitorIds: [monitor_id],
monitorType: monitorType,
@@ -103,6 +108,7 @@ export default class AlertsDashboardFlyoutComponent extends Component {
alertState,
monitorIds
);
+ this.getLocalClusterName();
}
componentDidUpdate(prevProps, prevState) {
@@ -141,7 +147,7 @@ export default class AlertsDashboardFlyoutComponent extends Component {
getMultipleGraphConditions = (trigger) => {
let conditions = _.get(trigger, 'condition.script.source');
if (_.isEmpty(conditions)) {
- return '-';
+ return DEFAULT_EMPTY_DATA;
} else {
conditions = conditions.replaceAll(' && ', '&AND&');
conditions = conditions.replaceAll(' || ', '&OR&');
@@ -150,6 +156,12 @@ export default class AlertsDashboardFlyoutComponent extends Component {
}
};
+ getLocalClusterName = async () => {
+ this.setState({
+ localClusterName: await getLocalClusterName(this.props.httpClient),
+ });
+ };
+
getSeverityText = (severity) => {
return _.get(_.find(SEVERITY_OPTIONS, { value: severity }), 'text');
};
@@ -319,6 +331,10 @@ export default class AlertsDashboardFlyoutComponent extends Component {
case MONITOR_TYPE.BUCKET_LEVEL:
columns = insertGroupByColumn(groupBy);
break;
+ case MONITOR_TYPE.CLUSTER_METRICS:
+ columns = _.cloneDeep(queryColumns);
+ columns.push(CLUSTER_METRICS_CROSS_CLUSTER_ALERT_TABLE_COLUMN);
+ break;
case MONITOR_TYPE.DOC_LEVEL:
columns = _.cloneDeep(queryColumns);
columns.splice(
@@ -495,7 +511,7 @@ export default class AlertsDashboardFlyoutComponent extends Component {
triggerID,
trigger_name,
} = this.props;
- const { loading, monitor, monitorType, tabContent } = this.state;
+ const { loading, localClusterName, monitor, monitorType, tabContent } = this.state;
const searchType = _.get(monitor, 'ui_metadata.search.searchType', SEARCH_TYPE.GRAPH);
const triggerType = this.getTriggerType(monitorType);
@@ -511,7 +527,7 @@ export default class AlertsDashboardFlyoutComponent extends Component {
searchType === SEARCH_TYPE.GRAPH &&
(monitorType === MONITOR_TYPE.BUCKET_LEVEL || monitorType === MONITOR_TYPE.DOC_LEVEL)
? this.getMultipleGraphConditions(trigger)
- : _.get(trigger, 'condition.script.source', '-');
+ : _.get(trigger, 'condition.script.source', DEFAULT_EMPTY_DATA);
let displayMultipleConditions;
switch (monitorType) {
@@ -526,7 +542,7 @@ export default class AlertsDashboardFlyoutComponent extends Component {
const filters =
monitorType === MONITOR_TYPE.BUCKET_LEVEL && searchType === SEARCH_TYPE.GRAPH
? this.getBucketLevelGraphFilter(trigger)
- : '-';
+ : DEFAULT_EMPTY_DATA;
const bucketValue = _.get(monitor, 'ui_metadata.search.bucketValue');
let bucketUnitOfTime = _.get(monitor, 'ui_metadata.search.bucketUnitOfTime');
@@ -536,7 +552,7 @@ export default class AlertsDashboardFlyoutComponent extends Component {
const timeRangeForLast =
bucketValue !== undefined && !_.isEmpty(bucketUnitOfTime)
? `${bucketValue} ${bucketUnitOfTime}`
- : '-';
+ : DEFAULT_EMPTY_DATA;
let displayTableTabs;
switch (monitorType) {
@@ -551,6 +567,7 @@ export default class AlertsDashboardFlyoutComponent extends Component {
monitorType === MONITOR_TYPE.COMPOSITE_LEVEL ? '?type=workflow' : ''
}`;
+ const dataSources = getDataSources(monitor, localClusterName).join('\n');
return (
@@ -566,7 +583,7 @@ export default class AlertsDashboardFlyoutComponent extends Component {
Severity
- {this.getSeverityText(severity) || severity || '-'}
+ {this.getSeverityText(severity) || severity || DEFAULT_EMPTY_DATA}
@@ -599,6 +616,12 @@ export default class AlertsDashboardFlyoutComponent extends Component {
+
+
+ Monitor data sources
+ {dataSources}
+
+
@@ -651,7 +674,7 @@ export default class AlertsDashboardFlyoutComponent extends Component {
? 'Loading groups...'
: !_.isEmpty(groupBy)
? _.join(_.orderBy(groupBy), ', ')
- : '-'}
+ : DEFAULT_EMPTY_DATA}
diff --git a/public/components/Flyout/flyouts/components/__snapshots__/AlertsDashboardFlyoutComponent.test.js.snap b/public/components/Flyout/flyouts/components/__snapshots__/AlertsDashboardFlyoutComponent.test.js.snap
index a0549545e..0b87d9708 100644
--- a/public/components/Flyout/flyouts/components/__snapshots__/AlertsDashboardFlyoutComponent.test.js.snap
+++ b/public/components/Flyout/flyouts/components/__snapshots__/AlertsDashboardFlyoutComponent.test.js.snap
@@ -76,6 +76,24 @@ exports[`AlertsDashboardFlyoutComponent renders 1`] = `
+
+
+
+ Monitor data sources
+
+
+ -
+
+
+
{},
+ dataSources = [],
+ localClusterName = '',
+ monitorType = MONITOR_TYPE.QUERY_LEVEL,
+}) => {
+ const columns = [
+ {
+ field: 'cluster',
+ name: 'Data connection',
+ sortable: true,
+ truncateText: true,
+ },
+ ];
+ switch (monitorType) {
+ case MONITOR_TYPE.CLUSTER_METRICS:
+ // Cluster metrics monitors do not use indexes as data sources; excluding that column.
+ break;
+ default:
+ columns.push({
+ field: 'index',
+ name: 'Index',
+ sortable: true,
+ truncateText: true,
+ });
+ }
+
+ const indexItems = dataSources.map((dataSource = '', int) => {
+ const item = { id: int };
+ switch (monitorType) {
+ case MONITOR_TYPE.CLUSTER_METRICS:
+ item.cluster =
+ dataSource === localClusterName ? `${dataSource} (Local)` : `${dataSource} (Remote)`;
+ break;
+ default:
+ const shouldSplit = dataSource.includes(':');
+ const splitIndex = dataSource.split(':');
+ let clusterName = shouldSplit ? splitIndex[0] : localClusterName;
+ clusterName =
+ clusterName === localClusterName ? `${clusterName} (Local)` : `${clusterName} (Remote)`;
+ const indexName = shouldSplit ? splitIndex[1] : dataSource;
+ item.cluster = clusterName;
+ item.index = indexName;
+ }
+ return item;
+ });
+
+ return {
+ flyoutProps: {
+ 'aria-labelledby': 'dataSourcesFlyout',
+ size: 'm',
+ hideCloseButton: true,
+ 'data-test-subj': `dataSourcesFlyout`,
+ },
+ headerProps: { hasBorder: true },
+ header: (
+
+
+
+ {`Data sources`}
+
+
+
+
+
+
+ ),
+ footerProps: { style: { backgroundColor: '#F5F7FA' } },
+ body: (
+ item.id}
+ columns={columns}
+ pagination={true}
+ isSelectable={false}
+ hasActions={false}
+ noItemsMessage={'No data sources configured for this monitor.'}
+ data-test-subj={'dataSourcesFlyout_table'}
+ />
+ ),
+ };
+};
+
+export default dataSources;
diff --git a/public/components/Flyout/flyouts/index.js b/public/components/Flyout/flyouts/index.js
index 90b7aff9f..8ab6c60c2 100644
--- a/public/components/Flyout/flyouts/index.js
+++ b/public/components/Flyout/flyouts/index.js
@@ -7,12 +7,14 @@ import message from './message';
import messageFrequency from './messageFrequency';
import triggerCondition from './triggerCondition';
import alertsDashboard from './alertsDashboard';
+import dataSources from './dataSources';
const Flyouts = {
messageFrequency,
message,
triggerCondition,
alertsDashboard,
+ dataSources,
};
export default Flyouts;
diff --git a/public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorConstants.js b/public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorConstants.js
index fa66e696e..81c976c54 100644
--- a/public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorConstants.js
+++ b/public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorConstants.js
@@ -3,6 +3,9 @@
* SPDX-License-Identifier: Apache-2.0
*/
+import _ from 'lodash';
+import { DEFAULT_EMPTY_DATA } from '../../../../../utils/constants';
+
export const URL_DEFAULT_PREFIX = 'http://localhost:9200';
export const API_PATH_REQUIRED_PLACEHOLDER_TEXT = 'Select an API.';
export const EMPTY_PATH_PARAMS_TEXT = 'Enter remaining path components and path parameters';
@@ -31,6 +34,14 @@ export const DEFAULT_CLUSTER_METRICS_SCRIPT = {
source: 'ctx.results[0] != null',
};
+export const CLUSTER_METRICS_CROSS_CLUSTER_ALERT_TABLE_COLUMN = {
+ field: 'clusters',
+ name: 'Triggered clusters',
+ sortable: true,
+ truncateText: true,
+ render: (clusters = [DEFAULT_EMPTY_DATA]) => _.sortBy(clusters).join(', '),
+};
+
export const API_TYPES = {
CLUSTER_HEALTH: {
type: 'CLUSTER_HEALTH',
diff --git a/public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers.test.js b/public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers.test.js
index 018614b7f..69ae2ab9b 100644
--- a/public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers.test.js
+++ b/public/pages/CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers.test.js
@@ -68,6 +68,7 @@ describe('clusterMetricsMonitorHelpers', () => {
path: path,
path_params: '',
url: `http://localhost:9200/${path}`,
+ clusters: [],
},
};
expect(buildClusterMetricsRequest(values)).toEqual(expectedResult.uri);
@@ -90,6 +91,7 @@ describe('clusterMetricsMonitorHelpers', () => {
path: path,
path_params: '/' + pathParams,
url: `http://localhost:9200/${path}/${pathParams}`,
+ clusters: [],
},
};
expect(buildClusterMetricsRequest(values)).toEqual(expectedResult.uri);
diff --git a/public/pages/CreateMonitor/components/CrossClusterConfigurations/components/ExperimentalBanner.js b/public/pages/CreateMonitor/components/CrossClusterConfigurations/components/ExperimentalBanner.js
new file mode 100644
index 000000000..e5f58fb90
--- /dev/null
+++ b/public/pages/CreateMonitor/components/CrossClusterConfigurations/components/ExperimentalBanner.js
@@ -0,0 +1,34 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui';
+
+export const REMOTE_MONITORING_ENABLED_SETTING_PATH = 'plugins.alerting.remote_monitoring_enabled';
+
+export const ExperimentalBanner = () => {
+ return (
+ <>
+
+
+ The feature is experimental and should not be used in a production environment. Any index
+ patterns, visualization, and observability panels will be impacted if the feature is
+ deactivated. For more information see
+
+ Alerting documentation
+
+ . To leave feedback, visit
+
+ forum.opensearch.org
+
+
+
+
+ >
+ );
+};
diff --git a/public/pages/CreateMonitor/components/CrossClusterConfigurations/containers/CrossClusterConfiguration.js b/public/pages/CreateMonitor/components/CrossClusterConfigurations/containers/CrossClusterConfiguration.js
new file mode 100644
index 000000000..14a2067cd
--- /dev/null
+++ b/public/pages/CreateMonitor/components/CrossClusterConfigurations/containers/CrossClusterConfiguration.js
@@ -0,0 +1,397 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { Component } from 'react';
+import _ from 'lodash';
+import { EuiHealth, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
+import { FormikComboBox } from '../../../../../components/FormControls';
+import { MONITOR_TYPE } from '../../../../../utils/constants';
+import { connect } from 'formik';
+import { validateIndex } from '../../../../../utils/validate';
+import { ExperimentalBanner } from '../components/ExperimentalBanner';
+
+export const CROSS_CLUSTER_SETUP_LINK =
+ 'https://opensearch.org/docs/latest/security/access-control/cross-cluster-search/';
+
+export const HEALTH_TO_COLOR = {
+ green: 'success',
+ yellow: 'warning',
+ red: 'danger',
+ undefined: 'subdued',
+};
+
+export const GENERIC_LOCAL_CLUSTER_KEY = '_localCluster';
+export class CrossClusterConfiguration extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ loadedInitialValues: false,
+ loading: true,
+ localClusterName: '',
+ clusterCount: 0,
+ clusterOptions: [],
+ selectedClusters: [],
+ indexOptions: [],
+ selectedIndexes: [],
+ };
+ }
+
+ componentDidMount() {
+ this.getIndexes();
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ const { loadedInitialValues, selectedClusters } = this.state;
+ if (prevState.selectedClusters !== selectedClusters && loadedInitialValues) this.getIndexes();
+ }
+
+ async getIndexes() {
+ const { httpClient } = this.props;
+ const { loadedInitialValues, selectedClusters } = this.state;
+ this.setState({ loading: true });
+ try {
+ const indexes = selectedClusters.map((cluster) =>
+ cluster.hub_cluster ? '*' : `${cluster.cluster}:*`
+ );
+ const query = {
+ indexes: indexes.length === 0 ? '*,*:*' : indexes.join(','),
+ include_mappings: !loadedInitialValues,
+ };
+ const response = await httpClient.get(`../api/alerting/remote/indexes`, { query: query });
+ if (response.ok) {
+ this.parseOptions(response.resp);
+ } else {
+ console.log('Error getting clusters:', response);
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ this.setState({ loading: false });
+ }
+
+ parseOptions = (clusterInfos = {}) => {
+ const {
+ formik: { values },
+ } = this.props;
+ const { loadedInitialValues } = this.state;
+ const clusterOptions = [];
+ const categorizedClusterOptions = {
+ Local: [],
+ Remote: [],
+ };
+ const categorizedIndexOptions = {};
+ const selectedClusters = this.state.selectedClusters;
+ const selectedIndexes = this.state.selectedIndexes;
+ let localClusterName = '';
+
+ // Parse the selected clusters and indexes when editing a monitor.
+ const indexes = {};
+ if (!loadedInitialValues && !selectedIndexes.length && (values.index || []).length) {
+ // In 'values', 'index' consists of an array of '{ label: index-name }' objects.
+ values?.index.forEach(({ label }) => {
+ if (label.includes(':')) {
+ // Splits the index from 'cluster-name:index-name' format to an array with the cluster
+ // name as entry 0, and index name as entry 1.
+ const clusterName = label.split(':')[0];
+ const indexName = label.split(':')[1];
+ if (!indexes[clusterName]) indexes[clusterName] = [];
+ indexes[clusterName].push(indexName);
+ } else {
+ // Indexes in `index-name` format indicate the local cluster
+ if (!indexes[GENERIC_LOCAL_CLUSTER_KEY]) indexes[GENERIC_LOCAL_CLUSTER_KEY] = [];
+ indexes[GENERIC_LOCAL_CLUSTER_KEY].push(label);
+ }
+ });
+ }
+
+ const getClusterOptionLabel = (clusterInfo) =>
+ `${clusterInfo.cluster} ${clusterInfo.hub_cluster ? '(Local)' : '(Remote)'}`;
+
+ Object.entries(clusterInfos).forEach(([clusterName, clusterInfo]) => {
+ const clusterLabel = getClusterOptionLabel(clusterInfo);
+ const clusterOption = {
+ label: clusterLabel,
+ cluster: clusterInfo.cluster,
+ health: clusterInfo.health,
+ hub_cluster: clusterInfo.hub_cluster,
+ latency: clusterInfo.latency,
+ };
+ clusterOptions.push(clusterOption);
+ if (clusterOption.hub_cluster) {
+ localClusterName = clusterOption.cluster;
+ categorizedClusterOptions.Local.push(clusterOption);
+
+ // To simplify iterations, consolidate any indexes listed under GENERIC_LOCAL_CLUSTER_KEY
+ // with the local cluster, then delete the GENERIC_LOCAL_CLUSTER_KEY entry.
+ indexes[localClusterName] = (indexes[localClusterName] || []).concat(
+ indexes[GENERIC_LOCAL_CLUSTER_KEY] || []
+ );
+ delete indexes[GENERIC_LOCAL_CLUSTER_KEY];
+ } else {
+ categorizedClusterOptions.Remote.push(clusterOption);
+ }
+
+ if (!loadedInitialValues) {
+ // Parse the selected clusters when editing a monitor.
+ switch (values.monitor_type) {
+ case MONITOR_TYPE.CLUSTER_METRICS:
+ if ((values.clusterNames || []).includes(clusterName))
+ selectedClusters.push(clusterOption);
+ break;
+ default:
+ if (Object.keys(indexes).includes(clusterName)) selectedClusters.push(clusterOption);
+ }
+
+ // Select the local cluster by default if there are no other selected clusters.
+ if (!selectedClusters.length && clusterInfo.hub_cluster) {
+ selectedClusters.push(clusterOption);
+ this.setState({ selectedClusters: selectedClusters });
+ }
+ }
+
+ // Only display indexes for the selected clusters
+ if (selectedClusters.some((option) => option.cluster === clusterName)) {
+ const clusterIndexOptions =
+ clusterInfo.indexes === undefined
+ ? []
+ : Object.entries(clusterInfo.indexes).map(([_, indexInfo]) => {
+ const indexOption = {
+ label: indexInfo.name,
+ health: indexInfo.health,
+ index: indexInfo.name,
+ cluster: clusterInfo.cluster,
+ value:
+ clusterInfo.cluster === undefined || clusterInfo.hub_cluster
+ ? indexInfo.name
+ : `${clusterInfo.cluster}:${indexInfo.name}`,
+ };
+
+ // Parse the selected indexes when editing a monitor.
+ if (
+ !loadedInitialValues &&
+ (indexes[clusterName] || []).includes(indexOption.index)
+ )
+ selectedIndexes.push(indexOption);
+ return indexOption;
+ });
+
+ if (!categorizedIndexOptions[clusterInfo.cluster])
+ categorizedIndexOptions[clusterInfo.cluster] = { label: clusterLabel, options: [] };
+
+ categorizedIndexOptions[clusterInfo.cluster].options = _.orderBy(
+ clusterIndexOptions,
+ ['index'],
+ ['asc']
+ );
+ }
+ });
+
+ categorizedClusterOptions.Remote = _.orderBy(
+ categorizedClusterOptions.Remote,
+ ['hub_cluster', 'cluster'],
+ [`desc`, 'asc']
+ );
+
+ let outputState = {};
+ if (!loadedInitialValues) {
+ // Create generic indexOptions for any pre-selected indexes that have not yet been added to selectedIndexes.
+ Object.entries(indexes).forEach(([clusterName, indexList = []]) => {
+ indexList.forEach((index) => {
+ const includesIndex = (categorizedIndexOptions[clusterName].options || []).some(
+ (option) => option.index === index
+ );
+ if (!includesIndex) {
+ const isLocalCluster = clusterName === localClusterName;
+ const newOption = {
+ label: index,
+ health: undefined,
+ index: index,
+ cluster: clusterName,
+ value: isLocalCluster ? index : `${clusterName}:${index}`,
+ };
+ selectedIndexes.push(newOption);
+ }
+ });
+ });
+
+ outputState = {
+ clusterCount: clusterOptions.length,
+ clusterOptions: Object.entries(categorizedClusterOptions).map(([category, clusters]) => ({
+ label: category,
+ options: clusters,
+ })),
+ loadedInitialValues: true,
+ localClusterName: localClusterName,
+ selectedClusters: selectedClusters,
+ selectedIndexes: this.renderSelectedClusterIndexesOptions(selectedIndexes),
+ };
+ }
+
+ let indexOptions = Object.entries(categorizedIndexOptions).map(
+ ([_, clusterIndexOptions]) => clusterIndexOptions
+ );
+ indexOptions = _.orderBy(indexOptions, ['label'], ['asc']);
+ this.setState({
+ ...outputState,
+ indexOptions: indexOptions,
+ });
+ };
+
+ renderClusterOption = (option) => {
+ const { label, health } = option;
+ return {label};
+ };
+
+ onClustersChange = (options = [], field, form) => {
+ const { clusterOptions, selectedClusters, selectedIndexes } = this.state;
+ // If no clusters are selected, select the local cluster.
+ if (!options.length) {
+ const localClusterOption = clusterOptions.find((category) => category.label === 'Local')
+ ?.options[0];
+ options.push(localClusterOption);
+ }
+
+ // Remove index selections for cluster that are no longer selected.
+ if (options.length && options.some((option) => !selectedClusters.includes(option))) {
+ const clusterNames = options.map((clusterOption) => clusterOption.cluster);
+ const matchingIndexes = selectedIndexes.filter((indexOption) =>
+ clusterNames.includes(indexOption.cluster)
+ );
+ this.onIndexesChange(matchingIndexes, { name: 'index' }, form);
+ }
+
+ form.setFieldValue(
+ field.name,
+ options.map((option) => option.cluster)
+ );
+ this.setState({ selectedClusters: options });
+ };
+
+ renderClusterIndexesOption = (option) => {
+ const { label, health } = option;
+ return {label};
+ };
+
+ renderSelectedClusterIndexesOptions = (options = []) => {
+ // If the cluster name in the option is undefined, it indicates the index is on the local cluster.
+ const getLabel = ({ cluster, index }) =>
+ `${index} (${cluster ? cluster : this.state.localClusterName})`;
+ return options.map((option) => ({
+ ...option,
+ label: getLabel(option),
+ }));
+ };
+
+ onIndexesChange = (options, field, form) => {
+ const selectedIndexes = this.renderSelectedClusterIndexesOptions(options);
+ form.setFieldValue(field.name, selectedIndexes);
+ this.setState({ selectedIndexes: selectedIndexes });
+ };
+
+ onCreateOption = (value, field, form) => {
+ const { localClusterName, selectedIndexes } = this.state;
+ let clusterName = localClusterName;
+ let indexName = value;
+ if (value.includes(':')) {
+ const splitValue = value.split(':');
+ clusterName = splitValue[0];
+ indexName = splitValue[1];
+ }
+ selectedIndexes.push({
+ label: indexName,
+ health: undefined,
+ index: indexName,
+ cluster: clusterName,
+ value: clusterName === localClusterName ? indexName : `${clusterName}:${indexName}`,
+ });
+ form.setFieldValue(field.name, selectedIndexes);
+ this.setState({ selectedIndexes: selectedIndexes });
+ };
+
+ render() {
+ const { monitorType } = this.props;
+ const {
+ loading,
+ clusterCount,
+ clusterOptions,
+ selectedClusters,
+ indexOptions,
+ selectedIndexes,
+ } = this.state;
+
+ return (
+ <>
+
+
+
+ Select clusters
+
+
+ Select a local cluster or remote clusters from cross-cluster connections.{' '}
+
+ Learn more
+
+
+
+ ),
+ style: { paddingLeft: '10px' },
+ }}
+ inputProps={{
+ isLoading: loading,
+ // Disable cluster selection field when loading, or when there is only 1 cluster.
+ isDisabled: loading || clusterCount <= 1,
+ options: clusterOptions,
+ renderOption: this.renderClusterOption,
+ onChange: this.onClustersChange,
+ selectedOptions: selectedClusters,
+ 'data-test-subj': 'clustersComboBox',
+ }}
+ />
+
+
+
+ {monitorType !== MONITOR_TYPE.CLUSTER_METRICS && (
+
+
+ Indexes
+
+
+ Select one or more indexes or wildcard patterns
+
+
+ ),
+ helpText:
+ 'You can use * as a wildcard or date math index resolution in your index pattern.',
+ style: { paddingLeft: '10px' },
+ }}
+ inputProps={{
+ isLoading: loading,
+ isDisabled: loading || selectedClusters.length < 1,
+ options: indexOptions,
+ renderOption: this.renderClusterIndexesOption,
+ onChange: this.onIndexesChange,
+ onCreateOption: this.onCreateOption,
+ selectedOptions: selectedIndexes,
+ 'data-test-subj': 'indicesComboBox',
+ }}
+ />
+ )}
+ >
+ );
+ }
+}
+
+export default connect(CrossClusterConfiguration);
diff --git a/public/pages/CreateMonitor/components/CrossClusterConfigurations/containers/index.js b/public/pages/CreateMonitor/components/CrossClusterConfigurations/containers/index.js
new file mode 100644
index 000000000..afd93d0da
--- /dev/null
+++ b/public/pages/CreateMonitor/components/CrossClusterConfigurations/containers/index.js
@@ -0,0 +1,8 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import CrossClusterConfiguration from './CrossClusterConfiguration';
+
+export default CrossClusterConfiguration;
diff --git a/public/pages/CreateMonitor/components/CrossClusterConfigurations/utils/helpers.js b/public/pages/CreateMonitor/components/CrossClusterConfigurations/utils/helpers.js
new file mode 100644
index 000000000..408a0e924
--- /dev/null
+++ b/public/pages/CreateMonitor/components/CrossClusterConfigurations/utils/helpers.js
@@ -0,0 +1,46 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import _ from 'lodash';
+import { DEFAULT_EMPTY_DATA, MONITOR_TYPE } from '../../../../../utils/constants';
+
+export const getLocalClusterName = async (httpClient) => {
+ let localClusterName = DEFAULT_EMPTY_DATA;
+ try {
+ const response = await httpClient.get('../api/alerting/_health');
+ if (response.ok) {
+ localClusterName = response.resp[0]?.cluster;
+ } else {
+ console.log('Error getting local cluster name:', response);
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ return localClusterName;
+};
+
+export const getDataSources = (monitor, localClusterName) => {
+ const monitorType = _.get(
+ monitor,
+ 'monitor_type',
+ _.get(monitor, 'ui_metadata.monitor_type', MONITOR_TYPE.QUERY_LEVEL)
+ );
+ let dataSources;
+ switch (monitorType) {
+ case MONITOR_TYPE.CLUSTER_METRICS:
+ dataSources = _.get(monitor, 'inputs.0.uri.clusters');
+ // To preserve functionality of legacy monitors, cluster metrics monitors run on the
+ // local cluster by default if no clusters are specified in the monitor configuration.
+ if (_.isEmpty(dataSources)) dataSources = [localClusterName || DEFAULT_EMPTY_DATA];
+ break;
+ case MONITOR_TYPE.DOC_LEVEL:
+ dataSources = _.get(monitor, 'inputs.0.doc_level_input.indices', [DEFAULT_EMPTY_DATA]);
+ break;
+ default:
+ dataSources = _.get(monitor, 'inputs.0.search.indices', [DEFAULT_EMPTY_DATA]);
+ }
+ dataSources = _.sortBy(dataSources);
+ return dataSources;
+};
diff --git a/public/pages/CreateMonitor/components/QueryPerformance/QueryPerformance.js b/public/pages/CreateMonitor/components/QueryPerformance/QueryPerformance.js
index 278ad9b31..73a788fa0 100644
--- a/public/pages/CreateMonitor/components/QueryPerformance/QueryPerformance.js
+++ b/public/pages/CreateMonitor/components/QueryPerformance/QueryPerformance.js
@@ -5,57 +5,154 @@
import React, { Fragment } from 'react';
import _ from 'lodash';
-import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
-
-import { DEFAULT_EMPTY_DATA } from '../../../../utils/constants';
-import { URL } from '../../../../../utils/constants';
+import {
+ EuiButton,
+ EuiCallOut,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIcon,
+ EuiLink,
+ EuiModal,
+ EuiModalBody,
+ EuiModalFooter,
+ EuiModalHeader,
+ EuiModalHeaderTitle,
+ EuiSpacer,
+ EuiText,
+} from '@elastic/eui';
+import { DEFAULT_EMPTY_DATA, MONITOR_TYPE } from '../../../../utils/constants';
import ContentPanel from '../../../../components/ContentPanel';
-const QueryPerformance = ({ response, actions }) => (
-
-
- Check the performance of your query and make sure to follow best practices.{' '}
-
- Learn more
-
-
- }
- actions={actions}
- >
-
-
-
-
- Monitor duration
-
- {`${_.get(response, 'took', DEFAULT_EMPTY_DATA)} ms`}
-
-
-
-
-
- Request duration
-
- {_.get(response, 'invalid.path', DEFAULT_EMPTY_DATA)}
-
-
-
-
-
- Hits
-
- {_.get(response, 'hits.total.value', DEFAULT_EMPTY_DATA)}
-
-
-
-
-
-
+export const RECOMMENDED_DURATION = 100;
+export const SEARCH_DOCUMENTATION = 'https://opensearch.org/docs/latest/search-plugins/';
+
+const getPerformanceCallOut = () => (
+
+ We recommend reducing your query size and time range or changing data sources to optimize for
+ monitor performance.{' '}
+
+ Learn more
+
+
);
+export const getPerformanceModal = ({ edit, onClose, onSubmit, values }) => {
+ const monitorType = _.get(
+ values,
+ 'monitor_type',
+ _.get(values, 'workflow_type', MONITOR_TYPE.QUERY_LEVEL)
+ );
+
+ let hasRemoteClusters;
+ switch (monitorType) {
+ case MONITOR_TYPE.CLUSTER_METRICS:
+ hasRemoteClusters = !_.isEmpty(_.get(values, 'uri.clusters', []));
+ break;
+ default:
+ // Indexes for remote clusters will store the index name in
+ // the 'value' attribute of the object, not the 'label' attribute.
+ hasRemoteClusters = _.get(values, 'index', [])
+ .map(({ label, value }) => value || label)
+ .some((indexName) => indexName.includes(':'));
+ }
+
+ return (
+
+
+
+ Monitor is not optimized
+
+
+
+
+
+ The following use cases may impact this monitor's performance.
+
+ {hasRemoteClusters && - One or more remote indexes may affect monitor accuracy
}
+ - Large queries may impact monitor and remote cluster performance
+
+
+
+
+
+
+ {edit ? 'Update' : 'Create'} anyway
+
+
+
+ Reconfigure
+
+
+
+ );
+};
+
+const QueryPerformance = ({ response, actions }) => {
+ const monitorDuration = _.get(response, 'took', DEFAULT_EMPTY_DATA);
+ const monitorDurationCallout = monitorDuration >= RECOMMENDED_DURATION;
+
+ // TODO: Need to confirm the purpose of requestDuration.
+ // There's no explanation for it in the frontend code even back to opendistro implementation.
+ const requestDuration = _.get(response, 'invalid.path', DEFAULT_EMPTY_DATA);
+ const requestDurationCallout = requestDuration >= RECOMMENDED_DURATION;
+ const displayPerfCallOut = monitorDurationCallout || requestDurationCallout;
+ const alertIcon = (
+ <>
+
+
+ >
+ );
+ return (
+
+ {displayPerfCallOut && (
+ <>
+ {getPerformanceCallOut()}
+
+ >
+ )}
+
+
+
+
+
+
+ Monitor duration
+
+ {`${monitorDuration} ms`}
+ {monitorDurationCallout ? alertIcon : undefined}
+
+
+
+
+
+ Request duration
+
+ {requestDuration}
+ {requestDurationCallout ? alertIcon : undefined}
+
+
+
+
+
+ Hits
+
+ {_.get(response, 'hits.total.value', DEFAULT_EMPTY_DATA)}
+
+
+
+
+
+
+ );
+};
+
export default QueryPerformance;
diff --git a/public/pages/CreateMonitor/components/QueryPerformance/__snapshots__/QueryPerformance.test.js.snap b/public/pages/CreateMonitor/components/QueryPerformance/__snapshots__/QueryPerformance.test.js.snap
index fa380a6b7..68b77018f 100644
--- a/public/pages/CreateMonitor/components/QueryPerformance/__snapshots__/QueryPerformance.test.js.snap
+++ b/public/pages/CreateMonitor/components/QueryPerformance/__snapshots__/QueryPerformance.test.js.snap
@@ -36,27 +36,7 @@ exports[`QueryPerformance renders 1`] = `
>
-
- Check the performance of your query and make sure to follow best practices.
-
- Learn more
-
- EuiIconMock
-
-
- (opens in a new tab or window)
-
-
-
-
+ />
= RECOMMENDED_DURATION;
+
+ // TODO: Need to confirm the purpose of requestDuration.
+ // There's no explanation for it in the frontend code even back to opendistro implementation.
+ const requestDurationCallout =
+ _.get(performanceResponse, 'invalid.path') >= RECOMMENDED_DURATION;
+ const displayPerfCallOut = monitorDurationCallout || requestDurationCallout;
+
+ if (!createModalOpen && displayPerfCallOut) {
+ this.setState({
+ createModalOpen: true,
+ formikBag: formikBag,
+ });
+ } else {
+ this.onSubmit(values, formikBag);
+ }
+ }
+
onSubmit(values, formikBag) {
const { edit, history, updateMonitor, notifications, httpClient } = this.props;
const { triggerToEdit } = this.state;
@@ -134,11 +162,15 @@ export default class CreateMonitor extends Component {
isDarkMode,
notificationService,
} = this.props;
- const { initialValues, plugins } = this.state;
+ const { createModalOpen, initialValues, plugins } = this.state;
return (
-
+
{({ values, errors, handleSubmit, isSubmitting, isValid, touched }) => {
const isComposite = values.monitor_type === MONITOR_TYPE.COMPOSITE_LEVEL;
@@ -239,6 +271,23 @@ export default class CreateMonitor extends Component {
})
}
/>
+
+ {createModalOpen &&
+ getPerformanceModal({
+ edit: edit,
+ onClose: () => {
+ this.state.formikBag.setSubmitting(false);
+ this.setState({
+ createModalOpen: false,
+ formikBag: undefined,
+ });
+ },
+ onSubmit: () => {
+ this.onSubmit(values, this.state.formikBag);
+ this.setState({ createModalOpen: false });
+ },
+ values: values,
+ })}
);
}}
diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap b/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap
index 6400e4709..598937daf 100644
--- a/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap
+++ b/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap
@@ -23,6 +23,7 @@ exports[`CreateMonitor renders 1`] = `
"associatedMonitorsList": Array [],
"bucketUnitOfTime": "h",
"bucketValue": 1,
+ "clusterNames": Array [],
"cronExpression": "0 */1 * * *",
"daily": 0,
"description": "",
@@ -64,6 +65,7 @@ exports[`CreateMonitor renders 1`] = `
"timezone": Array [],
"uri": Object {
"api_type": "",
+ "clusters": Array [],
"path": "",
"path_params": "",
"url": "",
diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/__snapshots__/formikToMonitor.test.js.snap b/public/pages/CreateMonitor/containers/CreateMonitor/utils/__snapshots__/formikToMonitor.test.js.snap
index 4a180d30c..1456896ef 100644
--- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/__snapshots__/formikToMonitor.test.js.snap
+++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/__snapshots__/formikToMonitor.test.js.snap
@@ -49,6 +49,7 @@ exports[`formikToClusterMetricsUri can build a ClusterMetricsMonitor request wit
Object {
"uri": Object {
"api_type": "",
+ "clusters": Array [],
"path": "",
"path_params": "",
"url": "",
@@ -60,6 +61,7 @@ exports[`formikToClusterMetricsUri can build a ClusterMetricsMonitor request wit
Object {
"uri": Object {
"api_type": "CLUSTER_HEALTH",
+ "clusters": Array [],
"path": "_cluster/health",
"path_params": "",
"url": "http://localhost:9200/_cluster/health",
diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js
index 9e35830be..39c2de490 100644
--- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js
+++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js
@@ -49,8 +49,10 @@ export const FORMIK_INITIAL_VALUES = {
/* DEFINE MONITOR */
monitor_type: MONITOR_TYPE.QUERY_LEVEL,
searchType: 'graph',
+ clusterNames: [],
uri: {
api_type: '',
+ clusters: [],
path: '',
path_params: '',
url: '',
diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js
index db7c6aedd..4619c08b2 100644
--- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js
+++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js
@@ -170,12 +170,15 @@ export function formikToClusterMetricsInput(values) {
url = url + pathParams + _.get(API_TYPES, `${apiType}.appendText`, '');
}
}
+ const clusterNames = _.get(values, 'clusterNames', []);
+
return {
uri: {
api_type: apiType,
path: path,
path_params: pathParams,
url: url,
+ clusters: clusterNames,
},
};
}
@@ -204,7 +207,10 @@ export function formikToUiSearch(values) {
}
export function formikToIndices(values) {
- return values.index.map(({ label }) => label);
+ const hasRemoteClusters = values.index.some(
+ ({ cluster, value }) => !_.isEmpty(cluster) && !_.isEmpty(value)
+ );
+ return values.index.map(({ label, value }) => (hasRemoteClusters ? value : label));
}
export function formikToQuery(values) {
diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js
index 999197c78..1375cbebe 100644
--- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js
+++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js
@@ -37,6 +37,8 @@ export default function monitorToFormik(monitor) {
return {
index: FORMIK_INITIAL_VALUES.index,
uri: inputs[0].uri,
+ clusterNames: inputs[0].uri.clusters || [],
+ searchType: SEARCH_TYPE.CLUSTER_METRICS,
};
case MONITOR_TYPE.DOC_LEVEL:
return docLevelInputToFormik(monitor);
diff --git a/public/pages/CreateMonitor/containers/DataSource/DataSource.js b/public/pages/CreateMonitor/containers/DataSource/DataSource.js
index 2ca2b61c5..fa9029863 100644
--- a/public/pages/CreateMonitor/containers/DataSource/DataSource.js
+++ b/public/pages/CreateMonitor/containers/DataSource/DataSource.js
@@ -33,15 +33,26 @@ class DataSource extends Component {
}
render() {
- const { isMinimal } = this.props;
+ const { isMinimal, remoteMonitoringEnabled } = this.props;
const { monitor_type, searchType } = this.props.values;
const displayTimeField =
- searchType === SEARCH_TYPE.GRAPH && monitor_type !== MONITOR_TYPE.DOC_LEVEL;
+ searchType === SEARCH_TYPE.GRAPH &&
+ monitor_type !== MONITOR_TYPE.DOC_LEVEL &&
+ monitor_type !== MONITOR_TYPE.CLUSTER_METRICS;
const monitorIndexDisplay = (
<>
-
-
- {displayTimeField && }
+
+
+ {displayTimeField && (
+ <>
+
+
+ >
+ )}
>
);
diff --git a/public/pages/CreateMonitor/containers/DataSource/__snapshots__/DataSource.test.js.snap b/public/pages/CreateMonitor/containers/DataSource/__snapshots__/DataSource.test.js.snap
index 3182e6526..16b82d6b0 100644
--- a/public/pages/CreateMonitor/containers/DataSource/__snapshots__/DataSource.test.js.snap
+++ b/public/pages/CreateMonitor/containers/DataSource/__snapshots__/DataSource.test.js.snap
@@ -20,9 +20,7 @@ exports[`DataSource renders 1`] = `
httpClient={[MockFunction]}
monitorType="query_level_monitor"
/>
-
+
`;
diff --git a/public/pages/CreateMonitor/containers/DefineMonitor/DefineMonitor.js b/public/pages/CreateMonitor/containers/DefineMonitor/DefineMonitor.js
index 1e59a00bb..64fdc91fb 100644
--- a/public/pages/CreateMonitor/containers/DefineMonitor/DefineMonitor.js
+++ b/public/pages/CreateMonitor/containers/DefineMonitor/DefineMonitor.js
@@ -39,6 +39,7 @@ import ConfigureDocumentLevelQueries from '../../components/DocumentLevelMonitor
import FindingsDashboard from '../../../Dashboard/containers/FindingsDashboard';
import { validDocLevelGraphQueries } from '../../components/DocumentLevelMonitorQueries/utils/helpers';
import { validateWhereFilters } from '../../components/MonitorExpressions/expressions/utils/whereHelpers';
+import { REMOTE_MONITORING_ENABLED_SETTING_PATH } from '../../components/CrossClusterConfigurations/components/ExperimentalBanner';
function renderEmptyMessage(message) {
return (
@@ -74,6 +75,7 @@ class DefineMonitor extends Component {
plugins: [],
loadingResponse: false,
PanelComponent: props.flyoutMode ? ({ children }) => <>{children}> : ContentPanel,
+ remoteMonitoringEnabled: false,
};
this.renderGraph = this.renderGraph.bind(this);
@@ -88,6 +90,7 @@ class DefineMonitor extends Component {
this.getPlugins = this.getPlugins.bind(this);
this.getSupportedApiList = this.getSupportedApiList.bind(this);
this.showPluginWarning = this.showPluginWarning.bind(this);
+ this.getSettings = this.getSettings.bind(this);
}
componentDidMount() {
@@ -96,6 +99,7 @@ class DefineMonitor extends Component {
const isGraph = searchType === SEARCH_TYPE.GRAPH;
const hasIndices = !!index.length;
const hasTimeField = !!timeField;
+ this.getSettings();
if (isGraph && hasIndices) {
this.onQueryMappings();
if (hasTimeField || !this.requiresTimeField()) this.onRunQuery();
@@ -175,6 +179,34 @@ class DefineMonitor extends Component {
}
}
+ async getSettings() {
+ try {
+ const { httpClient } = this.props;
+ const response = await httpClient.get('../api/alerting/_settings');
+ if (response.ok) {
+ const { defaults, transient, persistent } = response.resp;
+ let remoteMonitoringEnabled = _.get(
+ // If present, take the 'transient' setting.
+ transient,
+ REMOTE_MONITORING_ENABLED_SETTING_PATH,
+ // Else take the 'persistent' setting.
+ _.get(
+ persistent,
+ REMOTE_MONITORING_ENABLED_SETTING_PATH,
+ // Else take the 'default' setting.
+ _.get(defaults, REMOTE_MONITORING_ENABLED_SETTING_PATH, false)
+ )
+ );
+ // Boolean settings are returned as strings (e.g., `"true"`, and `"false"`). Constructing boolean value from the string.
+ if (typeof remoteMonitoringEnabled === 'string')
+ remoteMonitoringEnabled = JSON.parse(remoteMonitoringEnabled);
+ this.setState({ remoteMonitoringEnabled: remoteMonitoringEnabled });
+ }
+ } catch (e) {
+ console.log('Error while retrieving settings', e);
+ }
+ }
+
requiresTimeField() {
const {
values: { monitor_type, searchType },
@@ -406,7 +438,8 @@ class DefineMonitor extends Component {
}
async onQueryMappings() {
- const index = this.props.values.index.map(({ label }) => label);
+ // Indexes for remote clusters will store the index name in the 'value' attribute of the object, not the 'label' attribute.
+ const index = this.props.values.index.map(({ label, value }) => value || label);
try {
const mappings = await this.queryMappings(index);
const dataTypes = getPathsPerDataType(mappings);
@@ -417,16 +450,34 @@ class DefineMonitor extends Component {
}
async queryMappings(index) {
- if (!index.length) {
- return {};
- }
+ if (!index.length) return {};
try {
- const response = await this.props.httpClient.post('../api/alerting/_mappings', {
- body: JSON.stringify({ index }),
- });
+ // If any index contain ":", it indicates at least 1 remote index is configured.
+ const hasRemoteClusters = index.some((indexName) => indexName.includes(':'));
+ const response = hasRemoteClusters
+ ? await this.props.httpClient.get('../api/alerting/remote/indexes', {
+ query: {
+ indexes: index.join(','),
+ include_mappings: true,
+ },
+ })
+ : // Otherwise, all configured indexes are on the local cluster.
+ await this.props.httpClient.post('../api/alerting/_mappings', {
+ body: JSON.stringify({ index }),
+ });
if (response.ok) {
- return response.resp;
+ if (hasRemoteClusters) {
+ const mappings = {};
+ Object.entries(response.resp).forEach(([_, clusterInfo]) => {
+ Object.entries(clusterInfo.indexes).forEach(([indexName, indexInfo]) => {
+ mappings[indexName] = { mappings: indexInfo.mappings };
+ });
+ });
+ return mappings;
+ } else {
+ return response.resp;
+ }
}
return {};
} catch (err) {
@@ -528,7 +579,9 @@ class DefineMonitor extends Component {
requiresPathParams = _.isEmpty(requiresPathParams);
if (!requiresPathParams) {
const path = _.get(API_TYPES, `${apiKey}.paths.withoutPathParams`);
- const values = { uri: { ...FORMIK_INITIAL_VALUES.uri, path } };
+ const values = {
+ uri: { ...FORMIK_INITIAL_VALUES.uri, path, clusterNames: [] },
+ };
requests.push(buildClusterMetricsRequest(values));
}
});
@@ -593,16 +646,27 @@ class DefineMonitor extends Component {
}
render() {
- const { values, errors, httpClient, detectorId, notifications, isDarkMode, flyoutMode } =
- this.props;
- const { dataTypes, PanelComponent } = this.state;
+ const {
+ values,
+ values: { monitor_type },
+ errors,
+ httpClient,
+ detectorId,
+ notifications,
+ isDarkMode,
+ flyoutMode,
+ } = this.props;
+ const { dataTypes, PanelComponent, remoteMonitoringEnabled } = this.state;
const monitorContent = this.getMonitorContent();
const { searchType } = this.props.values;
- const isGraphOrQuery = searchType === SEARCH_TYPE.GRAPH || searchType === SEARCH_TYPE.QUERY;
+ const displayDataSourcePanel =
+ searchType === SEARCH_TYPE.GRAPH ||
+ searchType === SEARCH_TYPE.QUERY ||
+ (remoteMonitoringEnabled && monitor_type === MONITOR_TYPE.CLUSTER_METRICS);
return (
- {!flyoutMode && isGraphOrQuery && (
+ {!flyoutMode && displayDataSourcePanel && (
diff --git a/public/pages/CreateMonitor/containers/DefineMonitor/__snapshots__/DefineMonitor.test.js.snap b/public/pages/CreateMonitor/containers/DefineMonitor/__snapshots__/DefineMonitor.test.js.snap
index d887a6adb..a0c25c035 100644
--- a/public/pages/CreateMonitor/containers/DefineMonitor/__snapshots__/DefineMonitor.test.js.snap
+++ b/public/pages/CreateMonitor/containers/DefineMonitor/__snapshots__/DefineMonitor.test.js.snap
@@ -8,6 +8,7 @@ exports[`DefineMonitor renders 1`] = `
errors={Object {}}
httpClient={[MockFunction]}
isMinimal={false}
+ remoteMonitoringEnabled={false}
values={
Object {
"aggregationType": "count",
@@ -21,6 +22,7 @@ exports[`DefineMonitor renders 1`] = `
"associatedMonitorsList": Array [],
"bucketUnitOfTime": "h",
"bucketValue": 1,
+ "clusterNames": Array [],
"cronExpression": "0 */1 * * *",
"daily": 0,
"description": "",
@@ -62,6 +64,7 @@ exports[`DefineMonitor renders 1`] = `
"timezone": Array [],
"uri": Object {
"api_type": "",
+ "clusters": Array [],
"path": "",
"path_params": "",
"url": "",
diff --git a/public/pages/CreateMonitor/containers/MonitorIndex/MonitorIndex.js b/public/pages/CreateMonitor/containers/MonitorIndex/MonitorIndex.js
index dacd8f209..0809cfbda 100644
--- a/public/pages/CreateMonitor/containers/MonitorIndex/MonitorIndex.js
+++ b/public/pages/CreateMonitor/containers/MonitorIndex/MonitorIndex.js
@@ -12,6 +12,7 @@ import { FormikComboBox } from '../../../../components/FormControls';
import { validateIndex, hasError, isInvalid } from '../../../../utils/validate';
import { canAppendWildcard, createReasonableWait, getMatchedOptions } from './utils/helpers';
import { MONITOR_TYPE } from '../../../../utils/constants';
+import CrossClusterConfiguration from '../../components/CrossClusterConfigurations/containers';
const CustomOption = ({ option, searchValue, contentClassName }) => {
const { health, label, index } = option;
@@ -197,6 +198,7 @@ class MonitorIndex extends React.Component {
}
render() {
+ const { httpClient, remoteMonitoringEnabled } = this.props;
const {
isLoading,
allIndices,
@@ -217,42 +219,62 @@ class MonitorIndex extends React.Component {
false //isIncludingSystemIndices
);
- const supportMultipleIndices = this.props.monitorType !== MONITOR_TYPE.DOC_LEVEL;
+ let supportMultipleIndices = true;
+ let supportsCrossClusterMonitoring = false;
+ switch (this.props.monitorType) {
+ case MONITOR_TYPE.DOC_LEVEL:
+ supportMultipleIndices = false;
+ supportsCrossClusterMonitoring = false;
+ break;
+ case MONITOR_TYPE.BUCKET_LEVEL:
+ case MONITOR_TYPE.CLUSTER_METRICS:
+ case MONITOR_TYPE.QUERY_LEVEL:
+ supportsCrossClusterMonitoring = true;
+ break;
+ default:
+ }
return (
-
{
- form.setFieldTouched('index', true);
- },
- onChange: (options, field, form) => {
- form.setFieldValue('index', options);
- },
- onCreateOption: (value, field, form) => {
- this.onCreateOption(value, field.value, form.setFieldValue, supportMultipleIndices);
- },
- onSearchChange: this.onSearchChange,
- renderOption: this.renderOption,
- isClearable: true,
- singleSelection: supportMultipleIndices ? false : { asPlainText: true },
- 'data-test-subj': 'indicesComboBox',
- }}
- />
+ <>
+ {remoteMonitoringEnabled && supportsCrossClusterMonitoring ? (
+
+ ) : (
+ {
+ form.setFieldTouched('index', true);
+ },
+ onChange: (options, field, form) => {
+ form.setFieldValue('index', options);
+ },
+ onCreateOption: (value, field, form) => {
+ this.onCreateOption(value, field.value, form.setFieldValue, supportMultipleIndices);
+ },
+ onSearchChange: this.onSearchChange,
+ renderOption: this.renderOption,
+ delimiter: ',',
+ isClearable: true,
+ singleSelection: supportMultipleIndices ? false : { asPlainText: true },
+ 'data-test-subj': 'indicesComboBox',
+ }}
+ />
+ )}
+ >
);
}
}
diff --git a/public/pages/CreateMonitor/containers/MonitorIndex/__snapshots__/MonitorIndex.test.js.snap b/public/pages/CreateMonitor/containers/MonitorIndex/__snapshots__/MonitorIndex.test.js.snap
index 2c0d6ed4f..5c818aa44 100644
--- a/public/pages/CreateMonitor/containers/MonitorIndex/__snapshots__/MonitorIndex.test.js.snap
+++ b/public/pages/CreateMonitor/containers/MonitorIndex/__snapshots__/MonitorIndex.test.js.snap
@@ -15,6 +15,7 @@ exports[`MonitorIndex renders 1`] = `
"associatedMonitorsList": Array [],
"bucketUnitOfTime": "h",
"bucketValue": 1,
+ "clusterNames": Array [],
"cronExpression": "0 */1 * * *",
"daily": 0,
"description": "",
@@ -56,6 +57,7 @@ exports[`MonitorIndex renders 1`] = `
"timezone": Array [],
"uri": Object {
"api_type": "",
+ "clusters": Array [],
"path": "",
"path_params": "",
"url": "",
@@ -86,6 +88,7 @@ exports[`MonitorIndex renders 1`] = `
Object {
"async": true,
"data-test-subj": "indicesComboBox",
+ "delimiter": ",",
"isClearable": true,
"isLoading": true,
"onBlur": [Function],
@@ -160,6 +163,7 @@ exports[`MonitorIndex renders 1`] = `
"associatedMonitorsList": Array [],
"bucketUnitOfTime": "h",
"bucketValue": 1,
+ "clusterNames": Array [],
"cronExpression": "0 */1 * * *",
"daily": 0,
"description": "",
@@ -201,6 +205,7 @@ exports[`MonitorIndex renders 1`] = `
"timezone": Array [],
"uri": Object {
"api_type": "",
+ "clusters": Array [],
"path": "",
"path_params": "",
"url": "",
@@ -251,6 +256,7 @@ exports[`MonitorIndex renders 1`] = `
"associatedMonitorsList": Array [],
"bucketUnitOfTime": "h",
"bucketValue": 1,
+ "clusterNames": Array [],
"cronExpression": "0 */1 * * *",
"daily": 0,
"description": "",
@@ -292,6 +298,7 @@ exports[`MonitorIndex renders 1`] = `
"timezone": Array [],
"uri": Object {
"api_type": "",
+ "clusters": Array [],
"path": "",
"path_params": "",
"url": "",
@@ -406,6 +413,7 @@ exports[`MonitorIndex renders 1`] = `
"associatedMonitorsList": Array [],
"bucketUnitOfTime": "h",
"bucketValue": 1,
+ "clusterNames": Array [],
"cronExpression": "0 */1 * * *",
"daily": 0,
"description": "",
@@ -447,6 +455,7 @@ exports[`MonitorIndex renders 1`] = `
"timezone": Array [],
"uri": Object {
"api_type": "",
+ "clusters": Array [],
"path": "",
"path_params": "",
"url": "",
@@ -497,6 +506,7 @@ exports[`MonitorIndex renders 1`] = `
"associatedMonitorsList": Array [],
"bucketUnitOfTime": "h",
"bucketValue": 1,
+ "clusterNames": Array [],
"cronExpression": "0 */1 * * *",
"daily": 0,
"description": "",
@@ -538,6 +548,7 @@ exports[`MonitorIndex renders 1`] = `
"timezone": Array [],
"uri": Object {
"api_type": "",
+ "clusters": Array [],
"path": "",
"path_params": "",
"url": "",
@@ -559,6 +570,7 @@ exports[`MonitorIndex renders 1`] = `
Object {
"async": true,
"data-test-subj": "indicesComboBox",
+ "delimiter": ",",
"isClearable": true,
"isLoading": true,
"onBlur": [Function],
@@ -588,6 +600,7 @@ exports[`MonitorIndex renders 1`] = `
async={true}
compressed={false}
data-test-subj="indicesComboBox"
+ delimiter=","
fullWidth={false}
id="index"
isClearable={true}
diff --git a/public/pages/Dashboard/components/AcknowledgeAlertsModal/__snapshots__/AcknowledgeAlertsModal.test.js.snap b/public/pages/Dashboard/components/AcknowledgeAlertsModal/__snapshots__/AcknowledgeAlertsModal.test.js.snap
index 0b146e70c..040b38d27 100644
--- a/public/pages/Dashboard/components/AcknowledgeAlertsModal/__snapshots__/AcknowledgeAlertsModal.test.js.snap
+++ b/public/pages/Dashboard/components/AcknowledgeAlertsModal/__snapshots__/AcknowledgeAlertsModal.test.js.snap
@@ -44,6 +44,7 @@ exports[`AcknowledgeAlertsModal renders 1`] = `
"associatedMonitorsList": Array [],
"bucketUnitOfTime": "h",
"bucketValue": 1,
+ "clusterNames": Array [],
"cronExpression": "0 */1 * * *",
"daily": 0,
"description": "",
@@ -85,6 +86,7 @@ exports[`AcknowledgeAlertsModal renders 1`] = `
"timezone": Array [],
"uri": Object {
"api_type": "",
+ "clusters": Array [],
"path": "",
"path_params": "",
"url": "",
diff --git a/public/pages/Dashboard/containers/Dashboard.js b/public/pages/Dashboard/containers/Dashboard.js
index cea536244..f55d344f5 100644
--- a/public/pages/Dashboard/containers/Dashboard.js
+++ b/public/pages/Dashboard/containers/Dashboard.js
@@ -40,6 +40,7 @@ import { MAX_ALERT_COUNT } from '../utils/constants';
import AcknowledgeAlertsModal from '../components/AcknowledgeAlertsModal';
import { getAlertsFindingColumn } from '../components/FindingsDashboard/findingsUtils';
import { ChainedAlertDetailsFlyout } from '../components/ChainedAlertDetailsFlyout/ChainedAlertDetailsFlyout';
+import { CLUSTER_METRICS_CROSS_CLUSTER_ALERT_TABLE_COLUMN } from '../../CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorConstants';
export default class Dashboard extends Component {
constructor(props) {
@@ -364,6 +365,10 @@ export default class Dashboard extends Component {
case MONITOR_TYPE.BUCKET_LEVEL:
columnType = insertGroupByColumn(groupBy);
break;
+ case MONITOR_TYPE.CLUSTER_METRICS:
+ columnType = _.cloneDeep(queryColumns);
+ columnType.push(CLUSTER_METRICS_CROSS_CLUSTER_ALERT_TABLE_COLUMN);
+ break;
case MONITOR_TYPE.DOC_LEVEL:
columnType = _.cloneDeep(queryColumns);
columnType.splice(
diff --git a/public/pages/MonitorDetails/components/MonitorOverview/MonitorOverview.js b/public/pages/MonitorDetails/components/MonitorOverview/MonitorOverview.js
index 038073d9f..feec3ccd3 100644
--- a/public/pages/MonitorDetails/components/MonitorOverview/MonitorOverview.js
+++ b/public/pages/MonitorDetails/components/MonitorOverview/MonitorOverview.js
@@ -20,16 +20,10 @@ const MonitorOverview = ({
detector,
detectorId,
delegateMonitors,
+ localClusterName,
+ setFlyout,
}) => {
const [flyoutData, setFlyoutData] = useState(undefined);
- const items = getOverviewStats(
- monitor,
- monitorId,
- monitorVersion,
- activeCount,
- detector,
- detectorId
- );
let relatedMonitorsStat = null;
let relatedMonitorsData = null;
@@ -76,6 +70,16 @@ const MonitorOverview = ({
const onFlyoutClose = () => setFlyoutData(undefined);
+ const items = getOverviewStats(
+ monitor,
+ monitorId,
+ monitorVersion,
+ activeCount,
+ detector,
+ detectorId,
+ localClusterName,
+ setFlyout
+ );
return (
<>
{flyoutData && (
diff --git a/public/pages/MonitorDetails/components/MonitorOverview/__snapshots__/MonitorOverview.test.js.snap b/public/pages/MonitorDetails/components/MonitorOverview/__snapshots__/MonitorOverview.test.js.snap
index 69cc25cf8..c68b27eb4 100644
--- a/public/pages/MonitorDetails/components/MonitorOverview/__snapshots__/MonitorOverview.test.js.snap
+++ b/public/pages/MonitorDetails/components/MonitorOverview/__snapshots__/MonitorOverview.test.js.snap
@@ -75,6 +75,20 @@ exports[`MonitorOverview renders 1`] = `
+
+
+
diff --git a/public/pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats.js b/public/pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats.js
index 30fa20829..d2c134aa9 100644
--- a/public/pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats.js
+++ b/public/pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats.js
@@ -5,7 +5,7 @@
import React from 'react';
import _ from 'lodash';
-import { EuiIcon, EuiLink } from '@elastic/eui';
+import { EuiBadge, EuiLink } from '@elastic/eui';
import moment from 'moment-timezone';
import getScheduleFromMonitor from './getScheduleFromMonitor';
import {
@@ -16,6 +16,8 @@ import {
} from '../../../../../utils/constants';
import { API_TYPES } from '../../../../CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorConstants';
import { getApiType } from '../../../../CreateMonitor/components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers';
+import { DATA_SOURCES_FLYOUT_TYPE } from '../../../../../components/Flyout/flyouts/dataSources';
+import { getDataSources } from '../../../../CreateMonitor/components/CrossClusterConfigurations/utils/helpers';
// TODO: used in multiple places, move into helper
export function getTime(time) {
@@ -59,13 +61,56 @@ function getMonitorLevelType(monitorType) {
}
}
+const getDataSourcesDisplay = (
+ dataSources = [],
+ localClusterName = DEFAULT_EMPTY_DATA,
+ monitorType,
+ setFlyout
+) => {
+ const closeFlyout = () => {
+ if (typeof setFlyout === 'function') setFlyout(null);
+ };
+
+ const openFlyout = () => {
+ if (typeof setFlyout === 'function') {
+ setFlyout({
+ type: DATA_SOURCES_FLYOUT_TYPE,
+ payload: {
+ closeFlyout: closeFlyout,
+ dataSources: dataSources,
+ localClusterName: localClusterName,
+ monitorType: monitorType,
+ },
+ });
+ }
+ };
+
+ return dataSources.length <= 1 ? (
+ dataSources[0] || localClusterName
+ ) : (
+ <>
+ {dataSources[0]}
+
+ View all {dataSources.length}
+
+ >
+ );
+};
+
export default function getOverviewStats(
monitor,
monitorId,
monitorVersion,
activeCount,
detector,
- detectorId
+ detectorId,
+ localClusterName,
+ setFlyout
) {
const searchType = _.has(monitor, 'inputs[0].uri')
? SEARCH_TYPE.CLUSTER_METRICS
@@ -90,6 +135,9 @@ export default function getOverviewStats(
if (!monitorLevelType) {
monitorLevelType = _.get(monitor, 'ui_metadata.monitor_type', 'query_level_monitor');
}
+
+ const dataSources = getDataSources(monitor, localClusterName);
+
const overviewStats = [
{
header: 'Monitor type',
@@ -100,6 +148,10 @@ export default function getOverviewStats(
value: getMonitorType(searchType, monitor),
},
...detectorOverview,
+ {
+ header: 'Data sources',
+ value: getDataSourcesDisplay(dataSources, localClusterName, monitorLevelType, setFlyout),
+ },
{
header: 'Total active alerts',
value: activeCount,
diff --git a/public/pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats.test.js b/public/pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats.test.js
index d9c567dfe..3f385e1fc 100644
--- a/public/pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats.test.js
+++ b/public/pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats.test.js
@@ -26,6 +26,10 @@ describe('getOverviewStats', () => {
header: 'Monitor definition type',
value: 'Extraction Query',
},
+ {
+ header: 'Data sources',
+ value: DEFAULT_EMPTY_DATA,
+ },
{
header: 'Total active alerts',
value: activeCount,
diff --git a/public/pages/MonitorDetails/containers/MonitorDetails.js b/public/pages/MonitorDetails/containers/MonitorDetails.js
index d33f153ef..d3a673dc3 100644
--- a/public/pages/MonitorDetails/containers/MonitorDetails.js
+++ b/public/pages/MonitorDetails/containers/MonitorDetails.js
@@ -47,6 +47,7 @@ import monitorToFormik from '../../CreateMonitor/containers/CreateMonitor/utils/
import FindingsDashboard from '../../Dashboard/containers/FindingsDashboard';
import { TABLE_TAB_IDS } from '../../Dashboard/components/FindingsDashboard/findingsUtils';
import { DeleteMonitorModal } from '../../../components/DeleteModal/DeleteMonitorModal';
+import { getLocalClusterName } from '../../CreateMonitor/components/CrossClusterConfigurations/utils/helpers';
export default class MonitorDetails extends Component {
constructor(props) {
@@ -72,6 +73,7 @@ export default class MonitorDetails extends Component {
isJsonModalOpen: false,
tabId: TABLE_TAB_IDS.ALERTS.id,
showDeleteModal: false,
+ localClusterName: undefined,
};
}
@@ -89,6 +91,7 @@ export default class MonitorDetails extends Component {
componentDidMount() {
this.getMonitor(this.props.match.params.monitorId);
+ this.getLocalClusterName();
}
componentDidUpdate(prevProps, prevState) {
@@ -104,6 +107,12 @@ export default class MonitorDetails extends Component {
this.props.setFlyout(null);
}
+ getLocalClusterName = async () => {
+ this.setState({
+ localClusterName: await getLocalClusterName(this.props.httpClient),
+ });
+ };
+
getDetector = (id) => {
const { httpClient, notifications } = this.props;
httpClient
@@ -407,6 +416,7 @@ export default class MonitorDetails extends Component {
isJsonModalOpen,
showDeleteModal,
delegateMonitors,
+ localClusterName,
} = this.state;
const {
location,
@@ -501,6 +511,8 @@ export default class MonitorDetails extends Component {
detector={detector}
detectorId={detectorId}
delegateMonitors={delegateMonitors}
+ localClusterName={localClusterName}
+ setFlyout={setFlyout}
/>
{
- switch (monitor.monitor_type) {
- case MONITOR_TYPE.BUCKET_LEVEL:
- return trigger[TRIGGER_TYPE.BUCKET_LEVEL];
- case MONITOR_TYPE.DOC_LEVEL:
- return trigger[TRIGGER_TYPE.DOC_LEVEL];
- case MONITOR_TYPE.COMPOSITE_LEVEL:
- return trigger[TRIGGER_TYPE.COMPOSITE_LEVEL];
- default:
- return trigger[TRIGGER_TYPE.QUERY_LEVEL];
+ let unwrappedTrigger = trigger;
+ if (Object.keys(trigger).length === 1) {
+ switch (monitor.monitor_type) {
+ case MONITOR_TYPE.BUCKET_LEVEL:
+ unwrappedTrigger = trigger[TRIGGER_TYPE.BUCKET_LEVEL];
+ break;
+ case MONITOR_TYPE.DOC_LEVEL:
+ unwrappedTrigger = trigger[TRIGGER_TYPE.DOC_LEVEL];
+ break;
+ case MONITOR_TYPE.COMPOSITE_LEVEL:
+ unwrappedTrigger = trigger[TRIGGER_TYPE.COMPOSITE_LEVEL];
+ break;
+ case MONITOR_TYPE.CLUSTER_METRICS:
+ case MONITOR_TYPE.QUERY_LEVEL:
+ default:
+ unwrappedTrigger = trigger[TRIGGER_TYPE.QUERY_LEVEL];
+ break;
+ }
}
+ return unwrappedTrigger;
});
}
diff --git a/public/pages/MonitorDetails/containers/Triggers/__snapshots__/Triggers.test.js.snap b/public/pages/MonitorDetails/containers/Triggers/__snapshots__/Triggers.test.js.snap
index cb7cc4c55..a73f0d641 100644
--- a/public/pages/MonitorDetails/containers/Triggers/__snapshots__/Triggers.test.js.snap
+++ b/public/pages/MonitorDetails/containers/Triggers/__snapshots__/Triggers.test.js.snap
@@ -40,7 +40,15 @@ exports[`Triggers renders 1`] = `
itemId="id"
items={
Array [
- undefined,
+ Object {
+ "actions": Array [
+ Object {
+ "name": "Random Action",
+ },
+ ],
+ "name": "Random Trigger",
+ "severity": 1,
+ },
]
}
key="0"
diff --git a/server/clusters/alerting/alertingPlugin.js b/server/clusters/alerting/alertingPlugin.js
index 65d26315f..a8c7f026e 100644
--- a/server/clusters/alerting/alertingPlugin.js
+++ b/server/clusters/alerting/alertingPlugin.js
@@ -10,6 +10,7 @@ import {
EMAIL_ACCOUNT_BASE_API,
EMAIL_GROUP_BASE_API,
WORKFLOW_BASE_API,
+ CROSS_CLUSTER_BASE_API,
} from '../../services/utils/constants';
export default function alertingPlugin(Client, config, components) {
@@ -420,4 +421,22 @@ export default function alertingPlugin(Client, config, components) {
},
method: 'GET',
});
+
+ alerting.getRemoteIndexes = ca({
+ url: {
+ fmt: `${CROSS_CLUSTER_BASE_API}/indexes?indexes=<%=indexes%>&include_mappings=<%=include_mappings%>`,
+ req: {
+ indexes: {
+ type: 'string',
+ required: true,
+ },
+ include_mappings: {
+ type: 'boolean',
+ required: false,
+ },
+ },
+ },
+ needBody: true,
+ method: 'GET',
+ });
}
diff --git a/server/plugin.js b/server/plugin.js
index b21b66ac5..901c6acd8 100644
--- a/server/plugin.js
+++ b/server/plugin.js
@@ -12,8 +12,17 @@ import {
MonitorService,
AnomalyDetectorService,
FindingService,
+ CrossClusterService,
} from './services';
-import { alerts, destinations, opensearch, monitors, detectors, findings } from '../server/routes';
+import {
+ alerts,
+ destinations,
+ opensearch,
+ monitors,
+ detectors,
+ findings,
+ crossCluster,
+} from '../server/routes';
export class AlertingPlugin {
constructor(initializerContext) {
@@ -36,6 +45,7 @@ export class AlertingPlugin {
const destinationsService = new DestinationsService(alertingESClient);
const anomalyDetectorService = new AnomalyDetectorService(adESClient);
const findingService = new FindingService(alertingESClient);
+ const crossClusterService = new CrossClusterService(alertingESClient);
const services = {
alertService,
destinationsService,
@@ -43,6 +53,7 @@ export class AlertingPlugin {
monitorService,
anomalyDetectorService,
findingService,
+ crossClusterService,
};
// Create router
@@ -54,6 +65,7 @@ export class AlertingPlugin {
monitors(services, router);
detectors(services, router);
findings(services, router);
+ crossCluster(services, router);
return {};
}
diff --git a/server/routes/crossCluster.js b/server/routes/crossCluster.js
new file mode 100644
index 000000000..883a1a723
--- /dev/null
+++ b/server/routes/crossCluster.js
@@ -0,0 +1,23 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { schema } from '@osd/config-schema';
+
+export default function (services, router) {
+ const { crossClusterService } = services;
+
+ router.get(
+ {
+ path: '/api/alerting/remote/indexes',
+ validate: {
+ query: schema.object({
+ indexes: schema.string(),
+ include_mappings: schema.maybe(schema.boolean()),
+ }),
+ },
+ },
+ crossClusterService.getRemoteIndexes
+ );
+}
diff --git a/server/routes/index.js b/server/routes/index.js
index 029623900..095c03fc7 100644
--- a/server/routes/index.js
+++ b/server/routes/index.js
@@ -9,5 +9,6 @@ import opensearch from './opensearch';
import monitors from './monitors';
import detectors from './anomalyDetector';
import findings from './findings';
+import crossCluster from './crossCluster';
-export { alerts, destinations, opensearch, monitors, detectors, findings };
+export { alerts, destinations, opensearch, monitors, detectors, findings, crossCluster };
diff --git a/server/routes/opensearch.js b/server/routes/opensearch.js
index 1bb0ab70d..28ade243b 100644
--- a/server/routes/opensearch.js
+++ b/server/routes/opensearch.js
@@ -69,4 +69,12 @@ export default function (services, router) {
},
opensearchService.getSettings
);
+
+ router.get(
+ {
+ path: '/api/alerting/_health',
+ validate: false,
+ },
+ opensearchService.getClusterHealth
+ );
}
diff --git a/server/services/CrossClusterService.js b/server/services/CrossClusterService.js
new file mode 100644
index 000000000..78c284bff
--- /dev/null
+++ b/server/services/CrossClusterService.js
@@ -0,0 +1,32 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export default class CrossClusterService {
+ constructor(esDriver) {
+ this.esDriver = esDriver;
+ }
+
+ getRemoteIndexes = async (context, req, res) => {
+ try {
+ const { callAsCurrentUser } = await this.esDriver.asScoped(req);
+ const response = await callAsCurrentUser('alerting.getRemoteIndexes', req.query);
+
+ return res.ok({
+ body: {
+ ok: true,
+ resp: response,
+ },
+ });
+ } catch (err) {
+ console.error('Alerting - CrossClusterService - getRemoteIndexes:', err);
+ return res.ok({
+ body: {
+ ok: false,
+ resp: err.message,
+ },
+ });
+ }
+ };
+}
diff --git a/server/services/OpensearchService.js b/server/services/OpensearchService.js
index 30b28c499..feafba9cf 100644
--- a/server/services/OpensearchService.js
+++ b/server/services/OpensearchService.js
@@ -95,6 +95,30 @@ export default class OpensearchService {
}
};
+ getClusterHealth = async (context, req, res) => {
+ try {
+ const { callAsCurrentUser } = this.esDriver.asScoped(req);
+ const health = await callAsCurrentUser('cat.health', {
+ format: 'json',
+ h: 'cluster,status',
+ });
+ return res.ok({
+ body: {
+ ok: true,
+ resp: health,
+ },
+ });
+ } catch (err) {
+ console.error('Alerting - OpensearchService - getClusterHealth:', err);
+ return res.ok({
+ body: {
+ ok: false,
+ resp: err.message,
+ },
+ });
+ }
+ };
+
getMappings = async (context, req, res) => {
try {
const { index } = req.body;
diff --git a/server/services/index.js b/server/services/index.js
index 6e3da1d6e..277b9c678 100644
--- a/server/services/index.js
+++ b/server/services/index.js
@@ -9,6 +9,7 @@ import OpensearchService from './OpensearchService';
import MonitorService from './MonitorService';
import AnomalyDetectorService from './AnomalyDetectorService';
import FindingService from './FindingService';
+import CrossClusterService from './CrossClusterService';
export {
AlertService,
@@ -17,4 +18,5 @@ export {
MonitorService,
AnomalyDetectorService,
FindingService,
+ CrossClusterService,
};
diff --git a/server/services/utils/constants.js b/server/services/utils/constants.js
index c8ba9897a..12c5a45e4 100644
--- a/server/services/utils/constants.js
+++ b/server/services/utils/constants.js
@@ -6,6 +6,7 @@
export const API_ROUTE_PREFIX = '/_plugins/_alerting';
export const MONITOR_BASE_API = `${API_ROUTE_PREFIX}/monitors`;
export const WORKFLOW_BASE_API = `${API_ROUTE_PREFIX}/workflows`;
+export const CROSS_CLUSTER_BASE_API = `${API_ROUTE_PREFIX}/remote`;
export const AD_BASE_API = `/_plugins/_anomaly_detection/detectors`;
export const DESTINATION_BASE_API = `${API_ROUTE_PREFIX}/destinations`;
export const EMAIL_ACCOUNT_BASE_API = `${DESTINATION_BASE_API}/email_accounts`;