[Synthetics] Configure rules auto for status/tls (#161578)

## Summary

Fixes https://github.com/elastic/kibana/issues/161197

<img width="1906" alt="image"
src="37942f97-c44e-473a-9753-e6dcd4694d3c">
This commit is contained in:
Shahzad 2023-07-11 22:47:43 +02:00 committed by GitHub
parent 7035adb4cc
commit 7f8310982d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 277 additions and 87 deletions

View file

@ -22,11 +22,21 @@ journey('OverviewScrolling', async ({ page, params }) => {
const retry: RetryService = params.getService('retry');
const listOfRequests: string[] = [];
const expected = [
'http://localhost:5620/internal/synthetics/service/enablement',
'http://localhost:5620/internal/uptime/dynamic_settings',
'http://localhost:5620/internal/synthetics/monitor/filters',
'http://localhost:5620/internal/uptime/service/locations',
'http://localhost:5620/internal/synthetics/overview?sortField=status&sortOrder=asc&',
'http://localhost:5620/internal/synthetics/overview_status?&scopeStatusByLocation=true',
'http://localhost:5620/internal/synthetics/service/monitors?perPage=10&page=1&sortOrder=asc&sortField=name.keyword&',
'http://localhost:5620/internal/synthetics/enable_default_alerting',
];
before(async () => {
page.on('request', (request) => {
const url = request.url();
if (url.includes('/synthetics/') || url.includes('/uptime/')) {
if (url.includes('/internal/synthetics/') || url.includes('/internal/uptime/')) {
listOfRequests.push(request.url());
}
});
@ -67,7 +77,7 @@ journey('OverviewScrolling', async ({ page, params }) => {
assertUnique('/service/monitors');
assertUnique('/monitor/filters');
expect(listOfRequests.length).toBe(16);
expect(listOfRequests).toEqual(expected);
});
step('scroll until you see showing all monitors', async () => {

View file

@ -5,33 +5,51 @@
* 2.0.
*/
import { useFetcher } from '@kbn/observability-shared-plugin/public';
import { useDispatch, useSelector } from 'react-redux';
import { useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { Rule } from '@kbn/triggers-actions-ui-plugin/public';
import { useSyntheticsSettingsContext } from '../../../contexts';
import {
selectSyntheticsAlerts,
selectSyntheticsAlertsLoading,
} from '../../../state/alert_rules/selectors';
import {
enableDefaultAlertingSilentlyAction,
getDefaultAlertingAction,
} from '../../../state/alert_rules';
import { SYNTHETICS_TLS_RULE } from '../../../../../../common/constants/synthetics_alerts';
import { selectAlertFlyoutVisibility, setAlertFlyoutVisible } from '../../../state';
import { enableDefaultAlertingAPI } from '../../../state/alert_rules/api';
import { ClientPluginsStart } from '../../../../../plugin';
export const useSyntheticsAlert = (isOpen: boolean) => {
const dispatch = useDispatch();
const [defaultRules, setAlert] = useState<{ statusRule: Rule; tlsRule: Rule } | null>(null);
const defaultRules = useSelector(selectSyntheticsAlerts);
const loading = useSelector(selectSyntheticsAlertsLoading);
const alertFlyoutVisible = useSelector(selectAlertFlyoutVisibility);
const { data, loading } = useFetcher(() => {
if (isOpen) {
return enableDefaultAlertingAPI();
const { canSave } = useSyntheticsSettingsContext();
const getOrCreateAlerts = useCallback(() => {
if (canSave) {
dispatch(enableDefaultAlertingSilentlyAction.get());
} else {
dispatch(getDefaultAlertingAction.get());
}
}, [isOpen]);
}, [canSave, dispatch]);
useEffect(() => {
if (data) {
setAlert(data);
if (!defaultRules) {
// on initial load we prioritize loading the app
setTimeout(() => {
getOrCreateAlerts();
}, 1000);
} else {
getOrCreateAlerts();
}
}, [data]);
// we don't want to run this on defaultRules change
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch, isOpen]);
const { triggersActionsUi } = useKibana<ClientPluginsStart>().services;

View file

@ -8,8 +8,7 @@
import React from 'react';
import { waitFor } from '@testing-library/react';
import { render } from '../../../utils/testing/rtl_helpers';
import { AlertingCallout } from './alerting_callout';
import * as alertingHooks from '../../settings/alerting_defaults/hooks/use_alerting_defaults';
import { AlertingCallout, MISSING_RULES_PRIVILEGES_LABEL } from './alerting_callout';
jest.mock('../../../contexts', () => ({
...jest.requireActual('../../../contexts'),
@ -33,23 +32,19 @@ describe('AlertingCallout', () => {
[true, false, false],
[true, true, false],
])('renders correctly', async (hasConnectors, statusAlertEnabled, shouldShowCallout) => {
jest.spyOn(alertingHooks, 'useAlertingDefaults').mockReturnValue({
settingsLoading: false,
connectorsLoading: false,
connectors: [],
defaultConnectors: hasConnectors ? ['default-connector'] : [],
actionTypes: [],
options: [
{
value: 'test',
label: 'test',
'data-test-subj': 'test',
},
],
});
const { getByText, queryByText } = render(<AlertingCallout />, {
state: {
dynamicSettings: {
...(shouldShowCallout ? { settings: {} } : {}),
},
defaultAlerting: {
data: {
statusRule: {},
tlsRule: {},
},
loading: false,
success: true,
},
monitorList: {
loaded: true,
data: {
@ -85,23 +80,23 @@ describe('AlertingCallout', () => {
])(
'overwrites rendering with isAlertingEnabled prop',
async (hasConnectors, statusAlertEnabled, shouldShowCallout) => {
jest.spyOn(alertingHooks, 'useAlertingDefaults').mockReturnValue({
settingsLoading: false,
connectorsLoading: false,
defaultConnectors: hasConnectors ? ['default-connector'] : [],
connectors: [],
actionTypes: [],
options: [
{
value: 'test',
label: 'test',
'data-test-subj': 'test',
},
],
});
const { getByText, queryByText } = render(
<AlertingCallout isAlertingEnabled={statusAlertEnabled} />
<AlertingCallout isAlertingEnabled={statusAlertEnabled} />,
{
state: {
dynamicSettings: {
...(shouldShowCallout ? { settings: {} } : {}),
},
defaultAlerting: {
data: {
statusRule: {},
tlsRule: {},
},
loading: false,
success: true,
},
},
}
);
await waitFor(() => {
@ -113,4 +108,21 @@ describe('AlertingCallout', () => {
});
}
);
it('show call out for missing privileges rules', async () => {
const { getByText } = render(<AlertingCallout />, {
state: {
defaultAlerting: {
data: {},
loading: false,
success: true,
},
},
});
await waitFor(() => {
expect(getByText(/Alerts are not being sent/)).toBeInTheDocument();
expect(getByText(MISSING_RULES_PRIVILEGES_LABEL)).toBeInTheDocument();
});
});
});

View file

@ -5,22 +5,35 @@
* 2.0.
*/
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { useDispatch, useSelector } from 'react-redux';
import { EuiButton, EuiCallOut, EuiSpacer } from '@elastic/eui';
import { EuiButton, EuiButtonEmpty, EuiCallOut, EuiMarkdownFormat, EuiSpacer } from '@elastic/eui';
import { syntheticsSettingsLocatorID } from '@kbn/observability-plugin/common';
import { useFetcher } from '@kbn/observability-shared-plugin/public';
import { useSessionStorage } from 'react-use';
import { i18n } from '@kbn/i18n';
import { isEmpty } from 'lodash';
import { selectDynamicSettings } from '../../../state/settings';
import {
selectSyntheticsAlerts,
selectSyntheticsAlertsLoaded,
} from '../../../state/alert_rules/selectors';
import { selectMonitorListState } from '../../../state';
import { getDynamicSettingsAction } from '../../../state/settings/actions';
import { useAlertingDefaults } from '../../settings/alerting_defaults/hooks/use_alerting_defaults';
import { useSyntheticsStartPlugins } from '../../../contexts';
import { useSyntheticsSettingsContext, useSyntheticsStartPlugins } from '../../../contexts';
import { ConfigKey } from '../../../../../../common/runtime_types';
export const AlertingCallout = ({ isAlertingEnabled }: { isAlertingEnabled?: boolean }) => {
const dispatch = useDispatch();
const [url, setUrl] = useState<string | undefined>('');
const { defaultConnectors, settingsLoading } = useAlertingDefaults();
const defaultRules = useSelector(selectSyntheticsAlerts);
const rulesLoaded = useSelector(selectSyntheticsAlertsLoaded);
const { settings } = useSelector(selectDynamicSettings);
const hasDefaultConnector = !settings || !isEmpty(settings?.defaultConnectors);
const { canSave } = useSyntheticsSettingsContext();
const {
data: { monitors },
@ -30,45 +43,76 @@ export const AlertingCallout = ({ isAlertingEnabled }: { isAlertingEnabled?: boo
const syntheticsLocators = useSyntheticsStartPlugins()?.share?.url.locators;
const locator = syntheticsLocators?.get(syntheticsSettingsLocatorID);
useEffect(() => {
async function generateUrl() {
const settingsUrl = await locator?.getUrl({});
setUrl(settingsUrl);
}
generateUrl();
const { data: url } = useFetcher(() => {
return locator?.getUrl({});
}, [locator]);
const hasDefaultConnector =
Boolean(defaultConnectors?.length) || (!settingsLoading && Boolean(defaultConnectors?.length));
const hasAlertingConfigured =
isAlertingEnabled ??
(monitorsLoaded &&
monitors.some((monitor) => monitor[ConfigKey.ALERT_CONFIG]?.status?.enabled));
const showCallout = url && !hasDefaultConnector && hasAlertingConfigured;
const showCallout = !hasDefaultConnector && hasAlertingConfigured;
const hasDefaultRules =
!rulesLoaded || Boolean(defaultRules?.statusRule && defaultRules?.tlsRule);
const missingRules = !hasDefaultRules && !canSave;
useEffect(() => {
dispatch(getDynamicSettingsAction.get());
}, [dispatch]);
return showCallout ? (
return (
<>
<EuiCallOut
title={
<FormattedMessage
id="xpack.synthetics.alerting.noConnectorsCallout.header"
defaultMessage="Alerts are not being sent"
/>
}
color="warning"
iconType="warning"
>
<p>
<FormattedMessage
id="xpack.synthetics.alerting.noConnectorsCallout.content"
defaultMessage="You have monitors with alerting enabled, but there is no default connector configured to send those alerts."
/>
</p>
<MissingRulesCallout
url={url}
missingConfig={Boolean(showCallout)}
missingRules={missingRules}
/>
<EuiSpacer size="m" />
</>
);
};
const MissingRulesCallout = ({
url,
missingRules,
missingConfig,
}: {
url?: string;
missingConfig?: boolean;
missingRules?: boolean;
}) => {
const [isHidden, setIsHidden] = useSessionStorage('MissingRulesCalloutHidden', false);
if ((!missingConfig && !missingRules) || isHidden) {
return null;
}
const point = missingRules === missingConfig ? '* ' : '';
const configCallout = missingConfig ? (
<EuiMarkdownFormat>{`${point}${MISSING_CONFIG_LABEL}`}</EuiMarkdownFormat>
) : null;
const rulesCallout = missingRules ? (
<EuiMarkdownFormat>{`${point}${MISSING_RULES_PRIVILEGES_LABEL}`}</EuiMarkdownFormat>
) : null;
return (
<EuiCallOut
title={
<FormattedMessage
id="xpack.synthetics.alerting.noConnectorsCallout.header"
defaultMessage="Alerts are not being sent"
/>
}
color="warning"
iconType="warning"
>
<p>
{configCallout}
{rulesCallout}
</p>
{missingConfig && (
<EuiButton
data-test-subj="syntheticsAlertingCalloutLinkButtonButton"
href={url}
@ -79,8 +123,34 @@ export const AlertingCallout = ({ isAlertingEnabled }: { isAlertingEnabled?: boo
defaultMessage="Configure now"
/>
</EuiButton>
</EuiCallOut>
<EuiSpacer size="m" />
</>
) : null;
)}
<EuiButtonEmpty
data-test-subj="syntheticsMissingRulesCalloutRemindMeLaterButton"
onClick={() => {
setIsHidden(true);
}}
>
<FormattedMessage
id="xpack.synthetics.alerting.remindMeLater.button"
defaultMessage="Remind me later"
/>
</EuiButtonEmpty>
</EuiCallOut>
);
};
const MISSING_CONFIG_LABEL = i18n.translate(
'xpack.synthetics.alerting.noConnectorsCallout.content',
{
defaultMessage:
'You have monitors with alerting enabled, but there is no default connector configured to send those alerts.',
}
);
export const MISSING_RULES_PRIVILEGES_LABEL = i18n.translate(
'xpack.synthetics.alerting.missingRules.content',
{
defaultMessage:
'You have monitors with alerting enabled, but there is no rules configured to send those alerts. Default rules are automatically created when user with write privileges opens Synthetics app.',
}
);

View file

@ -8,11 +8,21 @@
import { Rule } from '@kbn/triggers-actions-ui-plugin/public';
import { createAsyncAction } from '../utils/actions';
export const getDefaultAlertingAction = createAsyncAction<
void,
{ statusRule: Rule; tlsRule: Rule }
>('getDefaultAlertingAction');
export const enableDefaultAlertingAction = createAsyncAction<
void,
{ statusRule: Rule; tlsRule: Rule }
>('enableDefaultAlertingAction');
export const enableDefaultAlertingSilentlyAction = createAsyncAction<
void,
{ statusRule: Rule; tlsRule: Rule }
>('enableDefaultAlertingSilentlyAction');
export const updateDefaultAlertingAction = createAsyncAction<void, Rule>(
'updateDefaultAlertingAction'
);

View file

@ -9,6 +9,10 @@ import { Rule } from '@kbn/triggers-actions-ui-plugin/public';
import { SYNTHETICS_API_URLS } from '../../../../../common/constants';
import { apiService } from '../../../../utils/api_service';
export async function getDefaultAlertingAPI(): Promise<{ statusRule: Rule; tlsRule: Rule }> {
return apiService.get(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING);
}
export async function enableDefaultAlertingAPI(): Promise<{ statusRule: Rule; tlsRule: Rule }> {
return apiService.post(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING);
}

View file

@ -7,9 +7,27 @@
import { takeLeading } from 'redux-saga/effects';
import { i18n } from '@kbn/i18n';
import { enableDefaultAlertingAction, updateDefaultAlertingAction } from './actions';
import {
enableDefaultAlertingAction,
enableDefaultAlertingSilentlyAction,
getDefaultAlertingAction,
updateDefaultAlertingAction,
} from './actions';
import { fetchEffectFactory } from '../utils/fetch_effect';
import { enableDefaultAlertingAPI, updateDefaultAlertingAPI } from './api';
import { enableDefaultAlertingAPI, getDefaultAlertingAPI, updateDefaultAlertingAPI } from './api';
export function* getDefaultAlertingEffect() {
yield takeLeading(
getDefaultAlertingAction.get,
fetchEffectFactory(
getDefaultAlertingAPI,
enableDefaultAlertingAction.success,
enableDefaultAlertingAction.fail,
undefined,
failureMessage
)
);
}
export function* enableDefaultAlertingEffect() {
yield takeLeading(
@ -24,6 +42,19 @@ export function* enableDefaultAlertingEffect() {
);
}
export function* enableDefaultAlertingSilentlyEffect() {
yield takeLeading(
enableDefaultAlertingSilentlyAction.get,
fetchEffectFactory(
enableDefaultAlertingAPI,
enableDefaultAlertingAction.success,
enableDefaultAlertingAction.fail,
undefined,
failureMessage
)
);
}
export function* updateDefaultAlertingEffect() {
yield takeLeading(
updateDefaultAlertingAction.get,

View file

@ -6,10 +6,17 @@
*/
import { createReducer } from '@reduxjs/toolkit';
import { Rule } from '@kbn/triggers-actions-ui-plugin/public';
import { IHttpSerializedFetchError } from '..';
import { enableDefaultAlertingAction, updateDefaultAlertingAction } from './actions';
import {
enableDefaultAlertingAction,
enableDefaultAlertingSilentlyAction,
getDefaultAlertingAction,
updateDefaultAlertingAction,
} from './actions';
export interface DefaultAlertingState {
data?: { statusRule: Rule; tlsRule: Rule };
success: boolean | null;
loading: boolean;
error: IHttpSerializedFetchError | null;
@ -23,10 +30,17 @@ const initialSettingState: DefaultAlertingState = {
export const defaultAlertingReducer = createReducer(initialSettingState, (builder) => {
builder
.addCase(getDefaultAlertingAction.get, (state) => {
state.loading = true;
})
.addCase(enableDefaultAlertingSilentlyAction.get, (state) => {
state.loading = true;
})
.addCase(enableDefaultAlertingAction.get, (state) => {
state.loading = true;
})
.addCase(enableDefaultAlertingAction.success, (state, action) => {
state.data = action.payload;
state.success = Boolean(action.payload);
state.loading = false;
})

View file

@ -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 { createSelector } from 'reselect';
import type { SyntheticsAppState } from '../root_reducer';
const getState = (appState: SyntheticsAppState) => appState.defaultAlerting;
export const selectSyntheticsAlerts = createSelector(getState, (state) => state.data);
export const selectSyntheticsAlertsLoading = createSelector(getState, (state) => state.loading);
export const selectSyntheticsAlertsLoaded = createSelector(getState, (state) => state.success);

View file

@ -9,7 +9,12 @@ import { all, fork } from 'redux-saga/effects';
import { getCertsListEffect } from './certs';
import { addGlobalParamEffect, editGlobalParamEffect, getGlobalParamEffect } from './global_params';
import { fetchManualTestRunsEffect } from './manual_test_runs/effects';
import { enableDefaultAlertingEffect, updateDefaultAlertingEffect } from './alert_rules/effects';
import {
enableDefaultAlertingEffect,
enableDefaultAlertingSilentlyEffect,
getDefaultAlertingEffect,
updateDefaultAlertingEffect,
} from './alert_rules/effects';
import { executeEsQueryEffect } from './elasticsearch';
import {
fetchAlertConnectorsEffect,
@ -63,5 +68,7 @@ export const rootEffect = function* root(): Generator {
fork(editGlobalParamEffect),
fork(getGlobalParamEffect),
fork(getCertsListEffect),
fork(getDefaultAlertingEffect),
fork(enableDefaultAlertingSilentlyEffect),
]);
};