mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Synthetics] Initial monitor status alert (#147672)
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Fixes https://github.com/elastic/kibana/issues/145980
This commit is contained in:
parent
7875f0c348
commit
86527753a2
157 changed files with 4196 additions and 898 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -112,3 +112,4 @@ elastic-agent.yml
|
|||
fleet-server.yml
|
||||
/packages/kbn-package-map/package-map.json
|
||||
/packages/kbn-synthetic-package-map/
|
||||
**/.synthetics/
|
||||
|
|
|
@ -725,7 +725,7 @@
|
|||
"@cypress/webpack-preprocessor": "^5.12.2",
|
||||
"@elastic/eslint-plugin-eui": "0.0.2",
|
||||
"@elastic/makelogs": "^6.1.1",
|
||||
"@elastic/synthetics": "^1.0.0-beta.23",
|
||||
"@elastic/synthetics": "^1.0.0-beta.39",
|
||||
"@emotion/babel-preset-css-prop": "^11.10.0",
|
||||
"@emotion/jest": "^11.10.0",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||
|
|
|
@ -18,6 +18,11 @@ const { argv } = yargs(process.argv.slice(2))
|
|||
type: 'boolean',
|
||||
description: 'Pause on error',
|
||||
})
|
||||
.option('watch', {
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
description: 'Runs the server in watch mode, restarting on changes',
|
||||
})
|
||||
.option('grep', {
|
||||
default: undefined,
|
||||
type: 'string',
|
||||
|
|
|
@ -62,10 +62,7 @@ export class SyntheticsRunner {
|
|||
|
||||
const promises = dataArchives.map((archive) => esArchiver.loadIfNeeded(e2eDir + archive));
|
||||
|
||||
await Promise.all([
|
||||
...promises,
|
||||
esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'),
|
||||
]);
|
||||
await Promise.all([...promises]);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
@ -98,10 +95,18 @@ export class SyntheticsRunner {
|
|||
const { headless, match, pauseOnError } = this.params;
|
||||
const results = await syntheticsRun({
|
||||
params: { kibanaUrl: this.kibanaUrl, getService: this.getService },
|
||||
playwrightOptions: { headless, chromiumSandbox: false, timeout: 60 * 1000 },
|
||||
playwrightOptions: {
|
||||
headless,
|
||||
chromiumSandbox: false,
|
||||
timeout: 60 * 1000,
|
||||
viewport: {
|
||||
height: 900,
|
||||
width: 1600,
|
||||
},
|
||||
},
|
||||
match: match === 'undefined' ? '' : match,
|
||||
pauseOnError,
|
||||
screenshots: 'off',
|
||||
screenshots: 'only-on-failure',
|
||||
});
|
||||
|
||||
await this.assertResults(results);
|
||||
|
|
|
@ -344,6 +344,7 @@ export class Plugin
|
|||
});
|
||||
|
||||
return {
|
||||
observabilityRuleTypeRegistry: this.observabilityRuleTypeRegistry,
|
||||
navigation: {
|
||||
PageTemplate,
|
||||
},
|
||||
|
|
|
@ -34,6 +34,7 @@ export const DEFAULT_COMMON_FIELDS: CommonFields = {
|
|||
[ConfigKey.MONITOR_TYPE]: DataStream.HTTP,
|
||||
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.MULTISTEP,
|
||||
[ConfigKey.ENABLED]: true,
|
||||
[ConfigKey.ALERT_CONFIG]: { status: { enabled: true } },
|
||||
[ConfigKey.SCHEDULE]: {
|
||||
number: '3',
|
||||
unit: ScheduleUnit.MINUTES,
|
||||
|
|
|
@ -5,8 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export enum AlertConfigKey {
|
||||
STATUS_ENABLED = 'alert.status.enabled',
|
||||
TLS_ENABLED = 'alert.tls.enabled',
|
||||
}
|
||||
|
||||
// values must match keys in the integration package
|
||||
export enum ConfigKey {
|
||||
ALERT_CONFIG = 'alert',
|
||||
APM_SERVICE_NAME = 'service.name',
|
||||
CUSTOM_HEARTBEAT_ID = 'custom_heartbeat_id',
|
||||
CONFIG_ID = 'config_id',
|
||||
|
|
|
@ -13,4 +13,5 @@ export enum SYNTHETICS_API_URLS {
|
|||
INDEX_SIZE = `/internal/synthetics/index_size`,
|
||||
PARAMS = `/synthetics/params`,
|
||||
SYNC_GLOBAL_PARAMS = `/synthetics/sync_global_params`,
|
||||
ENABLE_DEFAULT_ALERTING = `/synthetics/enable_default_alerting`,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { ActionGroup } from '@kbn/alerting-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export type MonitorStatusActionGroup =
|
||||
ActionGroup<'xpack.synthetics.alerts.actionGroups.monitorStatus'>;
|
||||
|
||||
export const MONITOR_STATUS: MonitorStatusActionGroup = {
|
||||
id: 'xpack.synthetics.alerts.actionGroups.monitorStatus',
|
||||
name: i18n.translate('xpack.synthetics.alertRules.actionGroups.monitorStatus', {
|
||||
defaultMessage: 'Synthetics monitor status',
|
||||
}),
|
||||
};
|
||||
|
||||
export const ACTION_GROUP_DEFINITIONS: {
|
||||
MONITOR_STATUS: MonitorStatusActionGroup;
|
||||
} = {
|
||||
MONITOR_STATUS,
|
||||
};
|
||||
|
||||
export const SYNTHETICS_STATUS_RULE = 'xpack.synthetics.alerts.monitorStatus';
|
||||
|
||||
export const SYNTHETICS_ALERT_RULE_TYPES = {
|
||||
MONITOR_STATUS: SYNTHETICS_STATUS_RULE,
|
||||
};
|
||||
|
||||
export const SYNTHETICS_RULE_TYPES = [SYNTHETICS_STATUS_RULE];
|
19
x-pack/plugins/synthetics/common/field_names.ts
Normal file
19
x-pack/plugins/synthetics/common/field_names.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const AGENT_NAME = 'agent.name';
|
||||
|
||||
export const MONITOR_ID = 'monitor.id';
|
||||
export const MONITOR_NAME = 'monitor.name';
|
||||
export const MONITOR_TYPE = 'monitor.type';
|
||||
|
||||
export const URL_FULL = 'url.full';
|
||||
export const URL_PORT = 'url.port';
|
||||
|
||||
export const OBSERVER_GEO_NAME = 'observer.geo.name';
|
||||
|
||||
export const ERROR_MESSAGE = 'error.message';
|
|
@ -16,6 +16,7 @@ export const commonFormatters: CommonFormatMap = {
|
|||
[ConfigKey.LOCATIONS]: null,
|
||||
[ConfigKey.MONITOR_TYPE]: null,
|
||||
[ConfigKey.ENABLED]: null,
|
||||
[ConfigKey.ALERT_CONFIG]: null,
|
||||
[ConfigKey.CONFIG_ID]: null,
|
||||
[ConfigKey.SCHEDULE]: (fields) =>
|
||||
JSON.stringify(
|
||||
|
|
|
@ -6,41 +6,14 @@
|
|||
*/
|
||||
|
||||
import { populateAlertActions } from './alert_actions';
|
||||
import { ActionConnector } from '../alerts/alerts';
|
||||
|
||||
const selectedMonitor = {
|
||||
docId: 'X5dkPncBy0xTcvZ347hy',
|
||||
timestamp: '2021-01-26T11:12:14.519Z',
|
||||
'@timestamp': '2021-01-26T11:12:14.519Z',
|
||||
url: { scheme: 'tcp', domain: 'localhost', port: 18278, full: 'tcp://localhost:18278' },
|
||||
error: { type: 'io', message: 'dial tcp 127.0.0.1:18278: connect: connection refused' },
|
||||
ecs: { version: '1.7.0' },
|
||||
resolve: { ip: '127.0.0.1', rtt: { us: 410 } },
|
||||
summary: { down: 1, up: 0 },
|
||||
monitor: {
|
||||
ip: '127.0.0.1',
|
||||
name: 'Always Down Local Port',
|
||||
type: 'tcp',
|
||||
timespan: { gte: '2021-01-26T11:12:14.519Z', lt: '2021-01-26T11:17:14.519Z' },
|
||||
id: 'always-down',
|
||||
status: 'down',
|
||||
duration: { us: 695 },
|
||||
check_group: 'a53b0003-5fc6-11eb-9241-42010a84000f',
|
||||
},
|
||||
event: { dataset: 'uptime' },
|
||||
agent: {
|
||||
ephemeral_id: '7d86e765-9f29-46e6-b1ec-047b09b4074e',
|
||||
id: '7c9d2825-614f-4906-a13e-c9db1c6e5585',
|
||||
name: 'gke-edge-oblt-edge-oblt-pool-c9faf257-m1ci',
|
||||
type: 'heartbeat',
|
||||
version: '8.0.0',
|
||||
},
|
||||
};
|
||||
import { ActionConnector } from './types';
|
||||
import { MONITOR_STATUS } from '../constants/uptime_alerts';
|
||||
import { MonitorStatusTranslations } from '../translations';
|
||||
|
||||
describe('Alert Actions factory', () => {
|
||||
it('generate expected action for pager duty', async () => {
|
||||
const resp = populateAlertActions({
|
||||
selectedMonitor,
|
||||
groupId: MONITOR_STATUS.id,
|
||||
defaultActions: [
|
||||
{
|
||||
actionTypeId: '.pagerduty',
|
||||
|
@ -49,33 +22,36 @@ describe('Alert Actions factory', () => {
|
|||
dedupKey: 'always-downxpack.uptime.alerts.actionGroups.monitorStatus',
|
||||
eventAction: 'trigger',
|
||||
severity: 'error',
|
||||
summary:
|
||||
'Monitor {{context.monitorName}} with url {{{context.monitorUrl}}} from {{context.observerLocation}} {{{context.statusMessage}}} The latest error message is {{{context.latestErrorMessage}}}',
|
||||
summary: MonitorStatusTranslations.defaultActionMessage,
|
||||
},
|
||||
id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9',
|
||||
},
|
||||
] as unknown as ActionConnector[],
|
||||
translations: {
|
||||
defaultActionMessage: MonitorStatusTranslations.defaultActionMessage,
|
||||
defaultRecoveryMessage: MonitorStatusTranslations.defaultRecoveryMessage,
|
||||
defaultSubjectMessage: MonitorStatusTranslations.defaultSubjectMessage,
|
||||
},
|
||||
});
|
||||
expect(resp).toEqual([
|
||||
{
|
||||
group: 'recovered',
|
||||
id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9',
|
||||
params: {
|
||||
dedupKey: 'always-downxpack.uptime.alerts.actionGroups.monitorStatus',
|
||||
dedupKey: expect.any(String),
|
||||
eventAction: 'resolve',
|
||||
summary:
|
||||
'Monitor Always Down Local Port with url tcp://localhost:18278 has recovered with status Up',
|
||||
'Alert for monitor {{context.monitorName}} with url {{{context.monitorUrl}}} from {{context.observerLocation}} has recovered',
|
||||
},
|
||||
},
|
||||
{
|
||||
group: 'xpack.uptime.alerts.actionGroups.monitorStatus',
|
||||
id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9',
|
||||
params: {
|
||||
dedupKey: 'always-downxpack.uptime.alerts.actionGroups.monitorStatus',
|
||||
dedupKey: expect.any(String),
|
||||
eventAction: 'trigger',
|
||||
severity: 'error',
|
||||
summary:
|
||||
'Monitor {{context.monitorName}} with url {{{context.monitorUrl}}} from {{context.observerLocation}} {{{context.statusMessage}}} The latest error message is {{{context.latestErrorMessage}}}',
|
||||
summary: MonitorStatusTranslations.defaultActionMessage,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
@ -83,7 +59,7 @@ describe('Alert Actions factory', () => {
|
|||
|
||||
it('generate expected action for slack action connector', async () => {
|
||||
const resp = populateAlertActions({
|
||||
selectedMonitor,
|
||||
groupId: MONITOR_STATUS.id,
|
||||
defaultActions: [
|
||||
{
|
||||
actionTypeId: '.pagerduty',
|
||||
|
@ -98,27 +74,31 @@ describe('Alert Actions factory', () => {
|
|||
id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9',
|
||||
},
|
||||
] as unknown as ActionConnector[],
|
||||
translations: {
|
||||
defaultActionMessage: MonitorStatusTranslations.defaultActionMessage,
|
||||
defaultRecoveryMessage: MonitorStatusTranslations.defaultRecoveryMessage,
|
||||
defaultSubjectMessage: MonitorStatusTranslations.defaultSubjectMessage,
|
||||
},
|
||||
});
|
||||
expect(resp).toEqual([
|
||||
{
|
||||
group: 'recovered',
|
||||
id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9',
|
||||
params: {
|
||||
dedupKey: 'always-downxpack.uptime.alerts.actionGroups.monitorStatus',
|
||||
dedupKey: expect.any(String),
|
||||
eventAction: 'resolve',
|
||||
summary:
|
||||
'Monitor Always Down Local Port with url tcp://localhost:18278 has recovered with status Up',
|
||||
'Alert for monitor {{context.monitorName}} with url {{{context.monitorUrl}}} from {{context.observerLocation}} has recovered',
|
||||
},
|
||||
},
|
||||
{
|
||||
group: 'xpack.uptime.alerts.actionGroups.monitorStatus',
|
||||
id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9',
|
||||
params: {
|
||||
dedupKey: 'always-downxpack.uptime.alerts.actionGroups.monitorStatus',
|
||||
dedupKey: expect.any(String),
|
||||
eventAction: 'trigger',
|
||||
severity: 'error',
|
||||
summary:
|
||||
'Monitor {{context.monitorName}} with url {{{context.monitorUrl}}} from {{context.observerLocation}} {{{context.statusMessage}}} The latest error message is {{{context.latestErrorMessage}}}',
|
||||
summary: MonitorStatusTranslations.defaultActionMessage,
|
||||
},
|
||||
},
|
||||
]);
|
|
@ -5,8 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { RuleAction as RuleActionOrig } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import type {
|
||||
IndexActionParams,
|
||||
PagerDutyActionParams,
|
||||
|
@ -16,12 +14,11 @@ import type {
|
|||
WebhookActionParams,
|
||||
EmailActionParams,
|
||||
} from '@kbn/stack-connectors-plugin/server/connector_types';
|
||||
import { NewAlertParams } from './alerts';
|
||||
import { ACTION_GROUP_DEFINITIONS } from '../../../../common/constants/alerts';
|
||||
import { MonitorStatusTranslations } from '../../../../common/translations';
|
||||
import { ActionTypeId } from '../../components/settings/types';
|
||||
import { Ping } from '../../../../common/runtime_types/ping';
|
||||
import { DefaultEmail } from '../../../../common/runtime_types';
|
||||
import { RuleAction as RuleActionOrig } from '@kbn/alerting-plugin/common';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import { ActionConnector, ActionTypeId } from './types';
|
||||
import { DefaultEmail } from '../runtime_types';
|
||||
|
||||
export const SLACK_ACTION_ID: ActionTypeId = '.slack';
|
||||
export const PAGER_DUTY_ACTION_ID: ActionTypeId = '.pagerduty';
|
||||
|
@ -33,30 +30,30 @@ export const JIRA_ACTION_ID: ActionTypeId = '.jira';
|
|||
export const WEBHOOK_ACTION_ID: ActionTypeId = '.webhook';
|
||||
export const EMAIL_ACTION_ID: ActionTypeId = '.email';
|
||||
|
||||
const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS;
|
||||
|
||||
export type RuleAction = Omit<RuleActionOrig, 'actionTypeId'>;
|
||||
|
||||
const getRecoveryMessage = (selectedMonitor: Ping) => {
|
||||
return i18n.translate('xpack.synthetics.alerts.monitorStatus.recoveryMessage', {
|
||||
defaultMessage: 'Monitor {monitor} with url {url} has recovered with status Up',
|
||||
values: {
|
||||
monitor: selectedMonitor?.monitor?.name || selectedMonitor?.monitor?.id,
|
||||
url: selectedMonitor?.url?.full,
|
||||
},
|
||||
});
|
||||
};
|
||||
interface Translations {
|
||||
defaultActionMessage: string;
|
||||
defaultRecoveryMessage: string;
|
||||
defaultSubjectMessage: string;
|
||||
}
|
||||
|
||||
export function populateAlertActions({
|
||||
defaultActions,
|
||||
selectedMonitor,
|
||||
defaultEmail,
|
||||
}: NewAlertParams) {
|
||||
groupId,
|
||||
translations,
|
||||
}: {
|
||||
groupId: string;
|
||||
defaultActions: ActionConnector[];
|
||||
defaultEmail?: DefaultEmail;
|
||||
translations: Translations;
|
||||
}) {
|
||||
const actions: RuleAction[] = [];
|
||||
defaultActions.forEach((aId) => {
|
||||
const action: RuleAction = {
|
||||
id: aId.id,
|
||||
group: MONITOR_STATUS.id,
|
||||
group: groupId,
|
||||
params: {},
|
||||
};
|
||||
|
||||
|
@ -64,54 +61,55 @@ export function populateAlertActions({
|
|||
id: aId.id,
|
||||
group: 'recovered',
|
||||
params: {
|
||||
message: getRecoveryMessage(selectedMonitor),
|
||||
message: translations.defaultRecoveryMessage,
|
||||
},
|
||||
};
|
||||
|
||||
switch (aId.actionTypeId) {
|
||||
case PAGER_DUTY_ACTION_ID:
|
||||
action.params = getPagerDutyActionParams(selectedMonitor);
|
||||
recoveredAction.params = getPagerDutyActionParams(selectedMonitor, true);
|
||||
const dedupKey = uuid.v4();
|
||||
action.params = getPagerDutyActionParams(translations, dedupKey);
|
||||
recoveredAction.params = getPagerDutyActionParams(translations, dedupKey, true);
|
||||
actions.push(recoveredAction);
|
||||
break;
|
||||
case SERVER_LOG_ACTION_ID:
|
||||
action.params = getServerLogActionParams(selectedMonitor);
|
||||
recoveredAction.params = getServerLogActionParams(selectedMonitor, true);
|
||||
action.params = getServerLogActionParams(translations);
|
||||
recoveredAction.params = getServerLogActionParams(translations, true);
|
||||
actions.push(recoveredAction);
|
||||
break;
|
||||
case INDEX_ACTION_ID:
|
||||
action.params = getIndexActionParams(selectedMonitor);
|
||||
recoveredAction.params = getIndexActionParams(selectedMonitor, true);
|
||||
action.params = getIndexActionParams(translations);
|
||||
recoveredAction.params = getIndexActionParams(translations, true);
|
||||
actions.push(recoveredAction);
|
||||
break;
|
||||
case SERVICE_NOW_ACTION_ID:
|
||||
action.params = getServiceNowActionParams();
|
||||
action.params = getServiceNowActionParams(translations);
|
||||
// Recovery action for service now is not implemented yet
|
||||
break;
|
||||
case JIRA_ACTION_ID:
|
||||
action.params = getJiraActionParams();
|
||||
action.params = getJiraActionParams(translations);
|
||||
// Recovery action for Jira is not implemented yet
|
||||
break;
|
||||
case WEBHOOK_ACTION_ID:
|
||||
action.params = getWebhookActionParams(selectedMonitor);
|
||||
recoveredAction.params = getWebhookActionParams(selectedMonitor, true);
|
||||
action.params = getWebhookActionParams(translations);
|
||||
recoveredAction.params = getWebhookActionParams(translations, true);
|
||||
actions.push(recoveredAction);
|
||||
break;
|
||||
case SLACK_ACTION_ID:
|
||||
case TEAMS_ACTION_ID:
|
||||
action.params = {
|
||||
message: MonitorStatusTranslations.defaultActionMessage,
|
||||
message: translations.defaultActionMessage,
|
||||
};
|
||||
actions.push(recoveredAction);
|
||||
break;
|
||||
case EMAIL_ACTION_ID:
|
||||
if (defaultEmail) {
|
||||
action.params = getEmailActionParams(defaultEmail, selectedMonitor);
|
||||
action.params = getEmailActionParams(translations, defaultEmail);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
action.params = {
|
||||
message: MonitorStatusTranslations.defaultActionMessage,
|
||||
message: translations.defaultActionMessage,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -121,14 +119,14 @@ export function populateAlertActions({
|
|||
return actions;
|
||||
}
|
||||
|
||||
function getIndexActionParams(selectedMonitor: Ping, recovery = false): IndexActionParams {
|
||||
function getIndexActionParams(translations: Translations, recovery = false): IndexActionParams {
|
||||
if (recovery) {
|
||||
return {
|
||||
documents: [
|
||||
{
|
||||
monitorName: '{{context.monitorName}}',
|
||||
monitorUrl: '{{{context.monitorUrl}}}',
|
||||
statusMessage: getRecoveryMessage(selectedMonitor),
|
||||
statusMessage: translations.defaultRecoveryMessage,
|
||||
latestErrorMessage: '',
|
||||
observerLocation: '{{context.observerLocation}}',
|
||||
},
|
||||
|
@ -150,50 +148,58 @@ function getIndexActionParams(selectedMonitor: Ping, recovery = false): IndexAct
|
|||
};
|
||||
}
|
||||
|
||||
function getServerLogActionParams(selectedMonitor: Ping, recovery = false): ServerLogActionParams {
|
||||
function getServerLogActionParams(
|
||||
{ defaultActionMessage, defaultRecoveryMessage }: Translations,
|
||||
recovery = false
|
||||
): ServerLogActionParams {
|
||||
if (recovery) {
|
||||
return {
|
||||
level: 'info',
|
||||
message: getRecoveryMessage(selectedMonitor),
|
||||
message: defaultRecoveryMessage,
|
||||
};
|
||||
}
|
||||
return {
|
||||
level: 'warn',
|
||||
message: MonitorStatusTranslations.defaultActionMessage,
|
||||
message: defaultActionMessage,
|
||||
};
|
||||
}
|
||||
|
||||
function getWebhookActionParams(selectedMonitor: Ping, recovery = false): WebhookActionParams {
|
||||
function getWebhookActionParams(
|
||||
{ defaultActionMessage, defaultRecoveryMessage }: Translations,
|
||||
recovery = false
|
||||
): WebhookActionParams {
|
||||
return {
|
||||
body: recovery
|
||||
? getRecoveryMessage(selectedMonitor)
|
||||
: MonitorStatusTranslations.defaultActionMessage,
|
||||
body: recovery ? defaultRecoveryMessage : defaultActionMessage,
|
||||
};
|
||||
}
|
||||
|
||||
function getPagerDutyActionParams(selectedMonitor: Ping, recovery = false): PagerDutyActionParams {
|
||||
function getPagerDutyActionParams(
|
||||
{ defaultActionMessage, defaultRecoveryMessage }: Translations,
|
||||
dedupKey: string,
|
||||
recovery = false
|
||||
): PagerDutyActionParams {
|
||||
if (recovery) {
|
||||
return {
|
||||
dedupKey: selectedMonitor.monitor.id + MONITOR_STATUS.id,
|
||||
dedupKey,
|
||||
eventAction: 'resolve',
|
||||
summary: getRecoveryMessage(selectedMonitor),
|
||||
summary: defaultRecoveryMessage,
|
||||
};
|
||||
}
|
||||
return {
|
||||
dedupKey: selectedMonitor.monitor.id + MONITOR_STATUS.id,
|
||||
dedupKey,
|
||||
eventAction: 'trigger',
|
||||
severity: 'error',
|
||||
summary: MonitorStatusTranslations.defaultActionMessage,
|
||||
summary: defaultActionMessage,
|
||||
};
|
||||
}
|
||||
|
||||
function getServiceNowActionParams(): ServiceNowActionParams {
|
||||
function getServiceNowActionParams({ defaultActionMessage }: Translations): ServiceNowActionParams {
|
||||
return {
|
||||
subAction: 'pushToService',
|
||||
subActionParams: {
|
||||
incident: {
|
||||
short_description: MonitorStatusTranslations.defaultActionMessage,
|
||||
description: MonitorStatusTranslations.defaultActionMessage,
|
||||
short_description: defaultActionMessage,
|
||||
description: defaultActionMessage,
|
||||
impact: '2',
|
||||
severity: '2',
|
||||
urgency: '2',
|
||||
|
@ -208,14 +214,14 @@ function getServiceNowActionParams(): ServiceNowActionParams {
|
|||
};
|
||||
}
|
||||
|
||||
function getJiraActionParams(): JiraActionParams {
|
||||
function getJiraActionParams({ defaultActionMessage }: Translations): JiraActionParams {
|
||||
return {
|
||||
subAction: 'pushToService',
|
||||
subActionParams: {
|
||||
incident: {
|
||||
summary: MonitorStatusTranslations.defaultActionMessage,
|
||||
summary: defaultActionMessage,
|
||||
externalId: null,
|
||||
description: MonitorStatusTranslations.defaultActionMessage,
|
||||
description: defaultActionMessage,
|
||||
issueType: null,
|
||||
priority: '2',
|
||||
labels: null,
|
||||
|
@ -227,19 +233,13 @@ function getJiraActionParams(): JiraActionParams {
|
|||
}
|
||||
|
||||
function getEmailActionParams(
|
||||
defaultEmail: DefaultEmail,
|
||||
selectedMonitor: Ping
|
||||
{ defaultActionMessage, defaultSubjectMessage }: Translations,
|
||||
defaultEmail: DefaultEmail
|
||||
): EmailActionParams {
|
||||
return {
|
||||
to: defaultEmail.to,
|
||||
subject: i18n.translate('xpack.synthetics.monitor.simpleStatusAlert.email.subject', {
|
||||
defaultMessage: 'Monitor {monitor} with url {url} is down',
|
||||
values: {
|
||||
monitor: selectedMonitor?.monitor?.name || selectedMonitor?.monitor?.id,
|
||||
url: selectedMonitor?.url?.full,
|
||||
},
|
||||
}),
|
||||
message: MonitorStatusTranslations.defaultActionMessage,
|
||||
subject: defaultSubjectMessage,
|
||||
message: defaultActionMessage,
|
||||
cc: defaultEmail.cc ?? [],
|
||||
bcc: defaultEmail.bcc ?? [],
|
||||
kibanaFooterLink: {
|
12
x-pack/plugins/synthetics/common/rules/status_rule.ts
Normal file
12
x-pack/plugins/synthetics/common/rules/status_rule.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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, TypeOf } from '@kbn/config-schema';
|
||||
|
||||
export const StatusRulePramsSchema = schema.object({});
|
||||
|
||||
export type StatusRuleParams = TypeOf<typeof StatusRulePramsSchema>;
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const SyntheticsMonitorStatusTranslations = {
|
||||
defaultActionMessage: i18n.translate(
|
||||
'xpack.synthetics.alerts.syntheticsMonitorStatus.defaultActionMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'The monitor {monitorName} checking {monitorUrl} from {locationName} last ran at {checkedAt} and is {status}. The last error received is: {lastErrorMessage}.',
|
||||
values: {
|
||||
monitorName: '{{context.monitorName}}',
|
||||
monitorUrl: '{{{context.monitorUrl}}}',
|
||||
status: '{{{context.status}}}',
|
||||
lastErrorMessage: '{{{context.lastErrorMessage}}}',
|
||||
locationName: '{{context.locationName}}',
|
||||
checkedAt: '{{context.checkedAt}}',
|
||||
},
|
||||
}
|
||||
),
|
||||
defaultSubjectMessage: i18n.translate(
|
||||
'xpack.synthetics.alerts.syntheticsMonitorStatus.defaultSubjectMessage',
|
||||
{
|
||||
defaultMessage: 'The monitor {monitorName} checking {monitorUrl} is down.',
|
||||
values: {
|
||||
monitorName: '{{context.monitorName}}',
|
||||
monitorUrl: '{{{context.monitorUrl}}}',
|
||||
},
|
||||
}
|
||||
),
|
||||
defaultRecoveryMessage: i18n.translate(
|
||||
'xpack.synthetics.alerts.syntheticsMonitorStatus.defaultRecoveryMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'The alert for the monitor {monitorName} checking {monitorUrl} from {locationName} is no longer active: {recoveryReason}.',
|
||||
values: {
|
||||
monitorName: '{{context.monitorName}}',
|
||||
monitorUrl: '{{{context.monitorUrl}}}',
|
||||
locationName: '{{context.locationName}}',
|
||||
recoveryReason: '{{context.recoveryReason}}',
|
||||
},
|
||||
}
|
||||
),
|
||||
name: i18n.translate('xpack.synthetics.alerts.syntheticsMonitorStatus.clientName', {
|
||||
defaultMessage: 'Monitor status',
|
||||
}),
|
||||
description: i18n.translate('xpack.synthetics.alerts.syntheticsMonitorStatus.description', {
|
||||
defaultMessage: 'Alert when a monitor is down.',
|
||||
}),
|
||||
};
|
|
@ -17,6 +17,8 @@ import type {
|
|||
EmailConnectorTypeId,
|
||||
} from '@kbn/stack-connectors-plugin/server/connector_types';
|
||||
|
||||
import type { ActionConnector as RawActionConnector } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
|
||||
export type ActionTypeId =
|
||||
| typeof SlackConnectorTypeId
|
||||
| typeof PagerDutyConnectorTypeId
|
||||
|
@ -27,3 +29,5 @@ export type ActionTypeId =
|
|||
| typeof JiraConnectorTypeId
|
||||
| typeof WebhookConnectorTypeId
|
||||
| typeof EmailConnectorTypeId;
|
||||
|
||||
export type ActionConnector = Omit<RawActionConnector, 'secrets'>;
|
|
@ -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 * as t from 'io-ts';
|
||||
|
||||
export const SyntheticsCommonStateType = t.intersection([
|
||||
t.partial({
|
||||
firstTriggeredAt: t.string,
|
||||
lastTriggeredAt: t.string,
|
||||
lastResolvedAt: t.string,
|
||||
meta: t.record(t.string, t.unknown),
|
||||
idWithLocation: t.string,
|
||||
}),
|
||||
t.type({
|
||||
firstCheckedAt: t.string,
|
||||
lastCheckedAt: t.string,
|
||||
isTriggered: t.boolean,
|
||||
}),
|
||||
]);
|
||||
|
||||
export type SyntheticsCommonState = t.TypeOf<typeof SyntheticsCommonStateType>;
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
|
||||
export const AlertConfigCodec = t.intersection([
|
||||
t.interface({
|
||||
enabled: t.boolean,
|
||||
}),
|
||||
t.partial({
|
||||
groupBy: t.string,
|
||||
}),
|
||||
]);
|
||||
|
||||
export const AlertConfigsCodec = t.partial({
|
||||
tls: AlertConfigCodec,
|
||||
status: AlertConfigCodec,
|
||||
});
|
||||
|
||||
export type AlertConfig = t.TypeOf<typeof AlertConfigCodec>;
|
||||
export type AlertConfigs = t.TypeOf<typeof AlertConfigsCodec>;
|
||||
|
||||
export const toggleStatusAlert = (configs: AlertConfigs = {}): AlertConfigs => {
|
||||
if (configs.status?.enabled) {
|
||||
return {
|
||||
...configs,
|
||||
status: {
|
||||
...configs.status,
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...configs,
|
||||
status: {
|
||||
enabled: true,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const isStatusEnabled = (configs: AlertConfigs = {}): boolean => {
|
||||
return configs.status?.enabled ?? false;
|
||||
};
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { AlertConfigsCodec } from './alert_config';
|
||||
import { secretKeys } from '../../constants/monitor_management';
|
||||
import { ConfigKey } from './config_key';
|
||||
import { MonitorServiceLocationCodec, ServiceLocationErrors } from './locations';
|
||||
|
@ -88,6 +89,7 @@ export const CommonFieldsCodec = t.intersection([
|
|||
[ConfigKey.PROJECT_ID]: t.string,
|
||||
[ConfigKey.ORIGINAL_SPACE]: t.string,
|
||||
[ConfigKey.CUSTOM_HEARTBEAT_ID]: t.string,
|
||||
[ConfigKey.ALERT_CONFIG]: AlertConfigsCodec,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
@ -373,12 +375,16 @@ export type MonitorDefaults = t.TypeOf<typeof MonitorDefaultsCodec>;
|
|||
|
||||
export const MonitorManagementListResultCodec = t.type({
|
||||
monitors: t.array(
|
||||
t.interface({
|
||||
id: t.string,
|
||||
attributes: EncryptedSyntheticsMonitorCodec,
|
||||
updated_at: t.string,
|
||||
created_at: t.string,
|
||||
})
|
||||
t.intersection([
|
||||
t.interface({
|
||||
id: t.string,
|
||||
attributes: EncryptedSyntheticsMonitorCodec,
|
||||
}),
|
||||
t.partial({
|
||||
updated_at: t.string,
|
||||
created_at: t.string,
|
||||
}),
|
||||
])
|
||||
),
|
||||
page: t.number,
|
||||
perPage: t.number,
|
||||
|
@ -395,6 +401,7 @@ export const MonitorOverviewItemCodec = t.interface({
|
|||
configId: t.string,
|
||||
location: MonitorServiceLocationCodec,
|
||||
isEnabled: t.boolean,
|
||||
isStatusAlertEnabled: t.boolean,
|
||||
});
|
||||
|
||||
export type MonitorOverviewItem = t.TypeOf<typeof MonitorOverviewItemCodec>;
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { AlertConfigsCodec } from './alert_config';
|
||||
import { ScreenshotOptionCodec } from './monitor_configs';
|
||||
|
||||
export const ProjectMonitorThrottlingConfigCodec = t.interface({
|
||||
|
@ -36,6 +37,7 @@ export const ProjectMonitorCodec = t.intersection([
|
|||
}),
|
||||
params: t.record(t.string, t.unknown),
|
||||
enabled: t.boolean,
|
||||
alert: AlertConfigsCodec,
|
||||
urls: t.union([t.string, t.array(t.string)]),
|
||||
hosts: t.union([t.string, t.array(t.string)]),
|
||||
max_redirects: t.string,
|
||||
|
|
|
@ -6,12 +6,14 @@
|
|||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { PingType } from '..';
|
||||
|
||||
export const OverviewStatusMetaDataCodec = t.interface({
|
||||
monitorQueryId: t.string,
|
||||
configId: t.string,
|
||||
location: t.string,
|
||||
status: t.string,
|
||||
ping: PingType,
|
||||
});
|
||||
|
||||
export const OverviewStatusCodec = t.interface({
|
||||
|
|
|
@ -26,13 +26,24 @@ export const MonitorStatusTranslations = {
|
|||
'xpack.synthetics.alerts.monitorStatus.defaultActionMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'Monitor {monitorName} with url {monitorUrl} from {observerLocation} {statusMessage} The latest error message is {latestErrorMessage}',
|
||||
'Monitor {monitorName} with url {monitorUrl} from {observerLocation} {statusMessage} The latest error message is {latestErrorMessage}, checked at {checkedAt}',
|
||||
values: {
|
||||
monitorName: '{{context.monitorName}}',
|
||||
monitorUrl: '{{{context.monitorUrl}}}',
|
||||
statusMessage: '{{{context.statusMessage}}}',
|
||||
latestErrorMessage: '{{{context.latestErrorMessage}}}',
|
||||
observerLocation: '{{context.observerLocation}}',
|
||||
checkedAt: '{{context.checkedAt}}',
|
||||
},
|
||||
}
|
||||
),
|
||||
defaultSubjectMessage: i18n.translate(
|
||||
'xpack.synthetics.alerts.monitorStatus.defaultSubjectMessage',
|
||||
{
|
||||
defaultMessage: 'Monitor {monitorName} with url {monitorUrl} is down',
|
||||
values: {
|
||||
monitorName: '{{context.monitorName}}',
|
||||
monitorUrl: '{{{context.monitorUrl}}}',
|
||||
},
|
||||
}
|
||||
),
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { format } from './get_monitor_url';
|
||||
|
||||
export const getSyntheticsMonitorRouteFromMonitorId = ({
|
||||
configId,
|
||||
dateRangeStart,
|
||||
dateRangeEnd,
|
||||
locationId,
|
||||
}: {
|
||||
configId: string;
|
||||
dateRangeStart: string;
|
||||
dateRangeEnd: string;
|
||||
locationId: string;
|
||||
}) =>
|
||||
format({
|
||||
pathname: `/app/synthetics/monitor/${configId}/history`,
|
||||
query: {
|
||||
dateRangeEnd,
|
||||
dateRangeStart,
|
||||
locationId,
|
||||
},
|
||||
});
|
|
@ -6,10 +6,13 @@
|
|||
*/
|
||||
|
||||
import { FtrConfigProviderContext } from '@kbn/test';
|
||||
import { argv } from '@kbn/observability-plugin/e2e/parse_args_params';
|
||||
|
||||
import { CA_CERT_PATH } from '@kbn/dev-utils';
|
||||
import { readKibanaConfig } from './tasks/read_kibana_config';
|
||||
|
||||
const { watch } = argv;
|
||||
|
||||
const MANIFEST_KEY = 'xpack.uptime.service.manifestUrl';
|
||||
const SERVICE_PASSWORD = 'xpack.uptime.service.password';
|
||||
const SERVICE_USERNAME = 'xpack.uptime.service.username';
|
||||
|
@ -43,7 +46,9 @@ async function config({ readConfigFile }: FtrConfigProviderContext) {
|
|||
|
||||
kbnTestServer: {
|
||||
...xpackFunctionalTestsConfig.get('kbnTestServer'),
|
||||
sourceArgs: [...xpackFunctionalTestsConfig.get('kbnTestServer.sourceArgs'), '--no-watch'],
|
||||
sourceArgs: watch
|
||||
? []
|
||||
: [...xpackFunctionalTestsConfig.get('kbnTestServer.sourceArgs'), '--no-watch'],
|
||||
serverArgs: [
|
||||
...xpackFunctionalTestsConfig.get('kbnTestServer.serverArgs'),
|
||||
'--csp.strict=false',
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
* 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 { journey, step, before, after, expect } from '@elastic/synthetics';
|
||||
import { byTestId } from '@kbn/ux-plugin/e2e/journeys/utils';
|
||||
import { RetryService } from '@kbn/ftr-common-functional-services';
|
||||
import uuid from 'uuid';
|
||||
import { getReasonMessage } from '../../../../server/legacy_uptime/lib/alerts/status_check';
|
||||
import { syntheticsAppPageProvider } from '../../../page_objects/synthetics/synthetics_app';
|
||||
import { SyntheticsServices } from '../services/synthetics_services';
|
||||
|
||||
journey(`DefaultStatusAlert`, async ({ page, params }) => {
|
||||
page.setDefaultTimeout(60 * 1000);
|
||||
const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl });
|
||||
|
||||
const services = new SyntheticsServices(params);
|
||||
|
||||
const getService = params.getService;
|
||||
const retry: RetryService = getService('retry');
|
||||
|
||||
const firstCheckTime = new Date(Date.now()).toISOString();
|
||||
let downCheckTime = new Date(Date.now()).toISOString();
|
||||
|
||||
before(async () => {
|
||||
await services.cleaUpRules();
|
||||
await services.cleaUpAlerts();
|
||||
await services.cleanTestMonitors();
|
||||
await services.enableMonitorManagedViaApi();
|
||||
await services.addTestMonitor('Test Monitor', {
|
||||
type: 'http',
|
||||
urls: 'https://www.google.com',
|
||||
custom_heartbeat_id: 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
|
||||
locations: [
|
||||
{ id: 'Test private location', label: 'Test private location', isServiceManaged: true },
|
||||
],
|
||||
});
|
||||
await services.addTestSummaryDocument({ timestamp: firstCheckTime });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await services.cleaUpRules();
|
||||
await services.cleaUpAlerts();
|
||||
await services.cleanTestMonitors();
|
||||
});
|
||||
|
||||
step('Go to monitors page', async () => {
|
||||
await syntheticsApp.navigateToOverview(true);
|
||||
});
|
||||
|
||||
step('should create default status alert', async () => {
|
||||
await page.click(byTestId('xpack.synthetics.alertsPopover.toggleButton'));
|
||||
await page.isDisabled(byTestId('xpack.synthetics.toggleAlertFlyout'));
|
||||
await page.click(byTestId('xpack.synthetics.toggleAlertFlyout'));
|
||||
await page.waitForSelector('text=Edit rule');
|
||||
await page.selectOption(byTestId('intervalInputUnit'), { label: 'second' });
|
||||
await page.fill(byTestId('intervalInput'), '10');
|
||||
await page.click(byTestId('saveEditedRuleButton'));
|
||||
await page.waitForSelector("text=Updated 'Synthetics internal alert'");
|
||||
});
|
||||
|
||||
step('Monitor is as up in overview page', async () => {
|
||||
await retry.tryForTime(60 * 1000, async () => {
|
||||
const totalDown = await page.textContent(
|
||||
byTestId('xpack.uptime.synthetics.overview.status.up')
|
||||
);
|
||||
expect(totalDown).toBe('1Up');
|
||||
});
|
||||
|
||||
await page.hover('text=Test Monitor');
|
||||
await page.click('[aria-label="Open actions menu"]');
|
||||
});
|
||||
|
||||
step('Disable default alert for monitor', async () => {
|
||||
await page.click('text=Disable status alert');
|
||||
await page.waitForSelector(`text=Alerts are now disabled for the monitor "Test Monitor".`);
|
||||
await page.click('text=Enable status alert');
|
||||
});
|
||||
|
||||
step('set the monitor status as down', async () => {
|
||||
downCheckTime = new Date(Date.now()).toISOString();
|
||||
await services.addTestSummaryDocument({
|
||||
isDown: true,
|
||||
timestamp: downCheckTime,
|
||||
});
|
||||
await page.waitForTimeout(5 * 1000);
|
||||
|
||||
await page.click(byTestId('syntheticsMonitorManagementTab'));
|
||||
await page.click(byTestId('syntheticsMonitorOverviewTab'));
|
||||
|
||||
const totalDown = await page.textContent(
|
||||
byTestId('xpack.uptime.synthetics.overview.status.down')
|
||||
);
|
||||
expect(totalDown).toBe('1Down');
|
||||
});
|
||||
|
||||
step('verified that it generates an alert', async () => {
|
||||
await page.click(byTestId('observability-nav-observability-overview-alerts'));
|
||||
|
||||
const reasonMessage = getReasonMessage({
|
||||
name: 'Test Monitor',
|
||||
location: 'Test private location',
|
||||
timestamp: downCheckTime,
|
||||
status: 'is down.',
|
||||
});
|
||||
await retry.tryForTime(2 * 60 * 1000, async () => {
|
||||
await page.click(byTestId('querySubmitButton'));
|
||||
const text = await page.textContent(`${byTestId('dataGridRowCell')} .euiLink`, {
|
||||
timeout: 5 * 1000,
|
||||
});
|
||||
|
||||
expect(text).toBe(reasonMessage);
|
||||
expect(await page.isVisible(`text=1 Alert`)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
step('set monitor status to up and verify that alert recovers', async () => {
|
||||
await services.addTestSummaryDocument();
|
||||
|
||||
await retry.tryForTime(2 * 60 * 1000, async () => {
|
||||
await page.click(byTestId('querySubmitButton'));
|
||||
await page.isVisible(`text=Recovered`, { timeout: 5 * 1000 });
|
||||
await page.isVisible(`text=1 Alert`, { timeout: 5 * 1000 });
|
||||
});
|
||||
});
|
||||
|
||||
step('set the status down again to generate another alert', async () => {
|
||||
await services.addTestSummaryDocument({ isDown: true });
|
||||
|
||||
await retry.tryForTime(2 * 60 * 1000, async () => {
|
||||
await page.click(byTestId('querySubmitButton'));
|
||||
await page.isVisible(`text=Active`, { timeout: 5 * 1000 });
|
||||
await page.isVisible(`text=1 Alert`);
|
||||
});
|
||||
});
|
||||
|
||||
step('Adds another down monitor and it auto adds the alert', async () => {
|
||||
const monitorId = uuid.v4();
|
||||
const name = `Test Monitor 2`;
|
||||
await services.addTestMonitor(name, {
|
||||
type: 'http',
|
||||
urls: 'https://www.google.com',
|
||||
custom_heartbeat_id: monitorId,
|
||||
locations: [
|
||||
{ id: 'Test private location', label: 'Test private location', isServiceManaged: true },
|
||||
],
|
||||
});
|
||||
|
||||
downCheckTime = new Date(Date.now()).toISOString();
|
||||
|
||||
await services.addTestSummaryDocument({
|
||||
timestamp: downCheckTime,
|
||||
monitorId,
|
||||
isDown: true,
|
||||
name,
|
||||
});
|
||||
|
||||
const reasonMessage = getReasonMessage({
|
||||
name,
|
||||
location: 'Test private location',
|
||||
timestamp: downCheckTime,
|
||||
status: 'is down.',
|
||||
});
|
||||
|
||||
await retry.tryForTime(2 * 60 * 1000, async () => {
|
||||
await page.click(byTestId('querySubmitButton'));
|
||||
await page.isVisible(`text=2 Alerts`, { timeout: 5 * 1000 });
|
||||
const alertReasonElem = await page.waitForSelector(`text=${reasonMessage}`, {
|
||||
timeout: 5 * 1000,
|
||||
});
|
||||
|
||||
expect(await alertReasonElem?.innerText()).toBe(reasonMessage);
|
||||
});
|
||||
});
|
||||
|
||||
step('Deleting the monitor recovers the alert', async () => {
|
||||
await services.deleteTestMonitorByQuery('"Test Monitor 2"');
|
||||
await page.click(byTestId('alert-status-filter-recovered-button'));
|
||||
await retry.tryForTime(3 * 60 * 1000, async () => {
|
||||
await page.click(byTestId('querySubmitButton'));
|
||||
expect(await page.isVisible(`text=1 Alert`)).toBe(true);
|
||||
});
|
||||
|
||||
await page.click('[aria-label="View in app"]');
|
||||
await page.click(byTestId('syntheticsMonitorOverviewTab'));
|
||||
await page.waitForSelector('text=Monitor details');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,416 @@
|
|||
/*
|
||||
* 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 uuid from 'uuid';
|
||||
|
||||
export const firstUpHit = {
|
||||
summary: {
|
||||
up: 1,
|
||||
down: 0,
|
||||
},
|
||||
tcp: {
|
||||
rtt: {
|
||||
connect: {
|
||||
us: 22245,
|
||||
},
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
name: 'docker-fleet-server',
|
||||
id: 'dd39a87d-a1e5-45a1-8dd9-e78d6a1391c6',
|
||||
type: 'heartbeat',
|
||||
ephemeral_id: '264bb432-93f6-4aa6-a14d-266c53b9e7c7',
|
||||
version: '8.7.0',
|
||||
},
|
||||
resolve: {
|
||||
rtt: {
|
||||
us: 3101,
|
||||
},
|
||||
ip: '142.250.181.196',
|
||||
},
|
||||
elastic_agent: {
|
||||
id: 'dd39a87d-a1e5-45a1-8dd9-e78d6a1391c6',
|
||||
version: '8.7.0',
|
||||
snapshot: true,
|
||||
},
|
||||
monitor: {
|
||||
duration: {
|
||||
us: 155239,
|
||||
},
|
||||
ip: '142.250.181.196',
|
||||
origin: 'ui',
|
||||
name: 'Test Monitor',
|
||||
timespan: {
|
||||
lt: '2022-12-18T09:55:04.211Z',
|
||||
gte: '2022-12-18T09:52:04.211Z',
|
||||
},
|
||||
fleet_managed: true,
|
||||
id: 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
|
||||
check_group: 'a039fd21-7eb9-11ed-8949-0242ac120006',
|
||||
type: 'http',
|
||||
status: 'up',
|
||||
},
|
||||
url: {
|
||||
scheme: 'https',
|
||||
port: 443,
|
||||
domain: 'www.google.com',
|
||||
full: 'https://www.google.com',
|
||||
},
|
||||
observer: {
|
||||
geo: {
|
||||
name: 'Test private location',
|
||||
},
|
||||
name: 'Test private location',
|
||||
},
|
||||
'@timestamp': '2022-12-18T09:52:04.056Z',
|
||||
ecs: {
|
||||
version: '8.0.0',
|
||||
},
|
||||
config_id: 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
|
||||
data_stream: {
|
||||
namespace: 'default',
|
||||
type: 'synthetics',
|
||||
dataset: 'http',
|
||||
},
|
||||
tls: {
|
||||
cipher: 'TLS-AES-128-GCM-SHA256',
|
||||
certificate_not_valid_before: '2022-11-28T08:19:01.000Z',
|
||||
established: true,
|
||||
server: {
|
||||
x509: {
|
||||
not_after: '2023-02-20T08:19:00.000Z',
|
||||
subject: {
|
||||
distinguished_name: 'CN=www.google.com',
|
||||
common_name: 'www.google.com',
|
||||
},
|
||||
not_before: '2022-11-28T08:19:01.000Z',
|
||||
public_key_curve: 'P-256',
|
||||
public_key_algorithm: 'ECDSA',
|
||||
signature_algorithm: 'SHA256-RSA',
|
||||
serial_number: '173037077033925240295268439311466214245',
|
||||
issuer: {
|
||||
distinguished_name: 'CN=GTS CA 1C3,O=Google Trust Services LLC,C=US',
|
||||
common_name: 'GTS CA 1C3',
|
||||
},
|
||||
},
|
||||
hash: {
|
||||
sha1: 'ea1b44061b864526c45619230b3299117d11bf4e',
|
||||
sha256: 'a5686448de09cc82b9cdad1e96357f919552ab14244da7948dd412ec0fc37d2b',
|
||||
},
|
||||
},
|
||||
rtt: {
|
||||
handshake: {
|
||||
us: 35023,
|
||||
},
|
||||
},
|
||||
version: '1.3',
|
||||
certificate_not_valid_after: '2023-02-20T08:19:00.000Z',
|
||||
version_protocol: 'tls',
|
||||
},
|
||||
state: {
|
||||
duration_ms: 0,
|
||||
checks: 1,
|
||||
ends: null,
|
||||
started_at: '2022-12-18T09:52:10.30502451Z',
|
||||
up: 1,
|
||||
id: 'Test private location-18524a5e641-0',
|
||||
down: 0,
|
||||
flap_history: [],
|
||||
status: 'up',
|
||||
},
|
||||
event: {
|
||||
agent_id_status: 'verified',
|
||||
ingested: '2022-12-18T09:52:11Z',
|
||||
dataset: 'http',
|
||||
},
|
||||
};
|
||||
|
||||
export const firstDownHit = ({
|
||||
name,
|
||||
timestamp,
|
||||
monitorId,
|
||||
}: { timestamp?: string; monitorId?: string; name?: string } = {}) => ({
|
||||
summary: {
|
||||
up: 0,
|
||||
down: 1,
|
||||
},
|
||||
tcp: {
|
||||
rtt: {
|
||||
connect: {
|
||||
us: 20482,
|
||||
},
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
name: 'docker-fleet-server',
|
||||
id: 'dd39a87d-a1e5-45a1-8dd9-e78d6a1391c6',
|
||||
type: 'heartbeat',
|
||||
ephemeral_id: '264bb432-93f6-4aa6-a14d-266c53b9e7c7',
|
||||
version: '8.7.0',
|
||||
},
|
||||
resolve: {
|
||||
rtt: {
|
||||
us: 3234,
|
||||
},
|
||||
ip: '142.250.181.196',
|
||||
},
|
||||
elastic_agent: {
|
||||
id: 'dd39a87d-a1e5-45a1-8dd9-e78d6a1391c6',
|
||||
version: '8.7.0',
|
||||
snapshot: true,
|
||||
},
|
||||
monitor: {
|
||||
duration: {
|
||||
us: 152459,
|
||||
},
|
||||
origin: 'ui',
|
||||
ip: '142.250.181.196',
|
||||
name: name ?? 'Test Monitor',
|
||||
fleet_managed: true,
|
||||
check_group: uuid.v4(),
|
||||
timespan: {
|
||||
lt: '2022-12-18T09:52:50.128Z',
|
||||
gte: '2022-12-18T09:49:50.128Z',
|
||||
},
|
||||
id: monitorId ?? 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
|
||||
type: 'http',
|
||||
status: 'down',
|
||||
},
|
||||
error: {
|
||||
message: 'received status code 200 expecting [500]',
|
||||
type: 'validate',
|
||||
},
|
||||
url: {
|
||||
scheme: 'https',
|
||||
port: 443,
|
||||
domain: 'www.google.com',
|
||||
full: 'https://www.google.com',
|
||||
},
|
||||
observer: {
|
||||
geo: {
|
||||
name: 'Test private location',
|
||||
},
|
||||
name: 'Test private location',
|
||||
},
|
||||
'@timestamp': timestamp ?? '2022-12-18T09:49:49.976Z',
|
||||
ecs: {
|
||||
version: '8.0.0',
|
||||
},
|
||||
config_id: monitorId ?? 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
|
||||
data_stream: {
|
||||
namespace: 'default',
|
||||
type: 'synthetics',
|
||||
dataset: 'http',
|
||||
},
|
||||
tls: {
|
||||
established: true,
|
||||
cipher: 'TLS-AES-128-GCM-SHA256',
|
||||
certificate_not_valid_before: '2022-11-28T08:19:01.000Z',
|
||||
server: {
|
||||
x509: {
|
||||
not_after: '2023-02-20T08:19:00.000Z',
|
||||
subject: {
|
||||
distinguished_name: 'CN=www.google.com',
|
||||
common_name: 'www.google.com',
|
||||
},
|
||||
not_before: '2022-11-28T08:19:01.000Z',
|
||||
public_key_algorithm: 'ECDSA',
|
||||
public_key_curve: 'P-256',
|
||||
signature_algorithm: 'SHA256-RSA',
|
||||
serial_number: '173037077033925240295268439311466214245',
|
||||
issuer: {
|
||||
distinguished_name: 'CN=GTS CA 1C3,O=Google Trust Services LLC,C=US',
|
||||
common_name: 'GTS CA 1C3',
|
||||
},
|
||||
},
|
||||
hash: {
|
||||
sha1: 'ea1b44061b864526c45619230b3299117d11bf4e',
|
||||
sha256: 'a5686448de09cc82b9cdad1e96357f919552ab14244da7948dd412ec0fc37d2b',
|
||||
},
|
||||
},
|
||||
rtt: {
|
||||
handshake: {
|
||||
us: 28468,
|
||||
},
|
||||
},
|
||||
version: '1.3',
|
||||
certificate_not_valid_after: '2023-02-20T08:19:00.000Z',
|
||||
version_protocol: 'tls',
|
||||
},
|
||||
state: {
|
||||
duration_ms: 0,
|
||||
checks: 1,
|
||||
ends: null,
|
||||
started_at: '2022-12-18T09:49:56.007551998Z',
|
||||
id: 'Test private location-18524a3d9a7-0',
|
||||
up: 0,
|
||||
down: 1,
|
||||
flap_history: [],
|
||||
status: 'down',
|
||||
},
|
||||
event: {
|
||||
agent_id_status: 'verified',
|
||||
ingested: '2022-12-18T09:49:57Z',
|
||||
dataset: 'http',
|
||||
},
|
||||
});
|
||||
|
||||
const sampleHits = {
|
||||
took: 1,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 1,
|
||||
successful: 1,
|
||||
skipped: 0,
|
||||
failed: 0,
|
||||
},
|
||||
hits: {
|
||||
total: {
|
||||
value: 3,
|
||||
relation: 'eq',
|
||||
},
|
||||
max_score: null,
|
||||
hits: [
|
||||
firstUpHit,
|
||||
{
|
||||
_index: '.ds-synthetics-http-default-2022.12.18-000001',
|
||||
_id: 'TfCjJIUB1hhCUz-n3bWY',
|
||||
_score: null,
|
||||
_ignored: ['http.response.body.content'],
|
||||
_source: firstDownHit,
|
||||
sort: [1671356989976],
|
||||
},
|
||||
{
|
||||
_index: '.ds-synthetics-http-default-2022.12.18-000001',
|
||||
_id: 'fPCiJIUB1hhCUz-nVbMK',
|
||||
_score: null,
|
||||
_source: {
|
||||
summary: {
|
||||
up: 1,
|
||||
down: 0,
|
||||
},
|
||||
tcp: {
|
||||
rtt: {
|
||||
connect: {
|
||||
us: 26843,
|
||||
},
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
name: 'docker-fleet-server',
|
||||
id: 'dd39a87d-a1e5-45a1-8dd9-e78d6a1391c6',
|
||||
ephemeral_id: '264bb432-93f6-4aa6-a14d-266c53b9e7c7',
|
||||
type: 'heartbeat',
|
||||
version: '8.7.0',
|
||||
},
|
||||
resolve: {
|
||||
rtt: {
|
||||
us: 18153,
|
||||
},
|
||||
ip: '142.250.181.196',
|
||||
},
|
||||
elastic_agent: {
|
||||
id: 'dd39a87d-a1e5-45a1-8dd9-e78d6a1391c6',
|
||||
version: '8.7.0',
|
||||
snapshot: true,
|
||||
},
|
||||
monitor: {
|
||||
duration: {
|
||||
us: 179022,
|
||||
},
|
||||
ip: '142.250.181.196',
|
||||
origin: 'ui',
|
||||
name: 'Test Monitor',
|
||||
id: 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
|
||||
timespan: {
|
||||
lt: '2022-12-18T09:51:09.403Z',
|
||||
gte: '2022-12-18T09:48:09.403Z',
|
||||
},
|
||||
check_group: '144186b2-7eb9-11ed-8949-0242ac120006',
|
||||
fleet_managed: true,
|
||||
type: 'http',
|
||||
status: 'up',
|
||||
},
|
||||
url: {
|
||||
scheme: 'https',
|
||||
port: 443,
|
||||
domain: 'www.google.com',
|
||||
full: 'https://www.google.com',
|
||||
},
|
||||
observer: {
|
||||
geo: {
|
||||
name: 'Test private location',
|
||||
},
|
||||
name: 'Test private location',
|
||||
},
|
||||
'@timestamp': '2022-12-18T09:48:09.224Z',
|
||||
ecs: {
|
||||
version: '8.0.0',
|
||||
},
|
||||
config_id: 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
|
||||
data_stream: {
|
||||
namespace: 'default',
|
||||
type: 'synthetics',
|
||||
dataset: 'http',
|
||||
},
|
||||
tls: {
|
||||
cipher: 'TLS-AES-128-GCM-SHA256',
|
||||
certificate_not_valid_before: '2022-11-28T08:19:01.000Z',
|
||||
established: true,
|
||||
server: {
|
||||
x509: {
|
||||
not_after: '2023-02-20T08:19:00.000Z',
|
||||
subject: {
|
||||
distinguished_name: 'CN=www.google.com',
|
||||
common_name: 'www.google.com',
|
||||
},
|
||||
not_before: '2022-11-28T08:19:01.000Z',
|
||||
public_key_curve: 'P-256',
|
||||
public_key_algorithm: 'ECDSA',
|
||||
signature_algorithm: 'SHA256-RSA',
|
||||
serial_number: '173037077033925240295268439311466214245',
|
||||
issuer: {
|
||||
distinguished_name: 'CN=GTS CA 1C3,O=Google Trust Services LLC,C=US',
|
||||
common_name: 'GTS CA 1C3',
|
||||
},
|
||||
},
|
||||
hash: {
|
||||
sha1: 'ea1b44061b864526c45619230b3299117d11bf4e',
|
||||
sha256: 'a5686448de09cc82b9cdad1e96357f919552ab14244da7948dd412ec0fc37d2b',
|
||||
},
|
||||
},
|
||||
rtt: {
|
||||
handshake: {
|
||||
us: 34230,
|
||||
},
|
||||
},
|
||||
version: '1.3',
|
||||
certificate_not_valid_after: '2023-02-20T08:19:00.000Z',
|
||||
version_protocol: 'tls',
|
||||
},
|
||||
state: {
|
||||
duration_ms: 0,
|
||||
checks: 1,
|
||||
ends: null,
|
||||
started_at: '2022-12-18T09:48:15.335017193Z',
|
||||
id: 'Test private location-18524a25067-0',
|
||||
up: 1,
|
||||
down: 0,
|
||||
flap_history: [],
|
||||
status: 'up',
|
||||
},
|
||||
event: {
|
||||
agent_id_status: 'verified',
|
||||
ingested: '2022-12-18T09:48:16Z',
|
||||
dataset: 'http',
|
||||
},
|
||||
},
|
||||
sort: [1671356889224],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
|
@ -17,3 +17,4 @@ export * from './private_locations.journey';
|
|||
export * from './alerting_default.journey';
|
||||
export * from './global_parameters.journey';
|
||||
export * from './detail_flyout';
|
||||
export * from './alert_rules/default_status_alert.journey';
|
||||
|
|
|
@ -30,7 +30,7 @@ export const addTestMonitor = async (
|
|||
) => {
|
||||
const testData = {
|
||||
locations: [{ id: 'us_central', isServiceManaged: true }],
|
||||
...(params?.type !== 'browser' ? {} : data),
|
||||
...(params?.type !== 'browser' ? {} : testDataMonitor),
|
||||
...(params || {}),
|
||||
name,
|
||||
};
|
||||
|
@ -98,8 +98,9 @@ export const cleanTestParams = async (params: Record<string, any>) => {
|
|||
}
|
||||
};
|
||||
|
||||
const data = {
|
||||
export const testDataMonitor = {
|
||||
type: 'browser',
|
||||
alert: { status: { enabled: true } },
|
||||
form_monitor_type: 'single',
|
||||
enabled: true,
|
||||
schedule: { unit: 'm', number: '10' },
|
||||
|
|
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* 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 axios from 'axios';
|
||||
import type { Client } from '@elastic/elasticsearch';
|
||||
import { KbnClient, uriencode } from '@kbn/test';
|
||||
import pMap from 'p-map';
|
||||
import { SYNTHETICS_API_URLS } from '../../../../common/constants';
|
||||
import { firstDownHit, firstUpHit } from '../alert_rules/sample_docs/sample_docs';
|
||||
|
||||
export class SyntheticsServices {
|
||||
kibanaUrl: string;
|
||||
params: Record<string, any>;
|
||||
requester: KbnClient['requester'];
|
||||
constructor(params: Record<string, any>) {
|
||||
this.kibanaUrl = params.kibanaUrl;
|
||||
this.requester = params.getService('kibanaServer').requester;
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
async enableMonitorManagedViaApi() {
|
||||
try {
|
||||
await axios.post(this.kibanaUrl + '/internal/uptime/service/enablement', undefined, {
|
||||
auth: { username: 'elastic', password: 'changeme' },
|
||||
headers: { 'kbn-xsrf': 'true' },
|
||||
});
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
async addTestMonitor(name: string, data: Record<string, any> = { type: 'browser' }) {
|
||||
const testData = {
|
||||
alert: { status: { enabled: true } },
|
||||
locations: [{ id: 'us_central', isServiceManaged: true }],
|
||||
...(data?.type !== 'browser' ? {} : data),
|
||||
...(data || {}),
|
||||
name,
|
||||
};
|
||||
try {
|
||||
await axios.post(this.kibanaUrl + '/internal/uptime/service/monitors', testData, {
|
||||
auth: { username: 'elastic', password: 'changeme' },
|
||||
headers: { 'kbn-xsrf': 'true' },
|
||||
});
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(JSON.stringify(e));
|
||||
}
|
||||
}
|
||||
|
||||
async deleteTestMonitorByQuery(query: string) {
|
||||
const { data } = await this.requester.request({
|
||||
description: 'get monitors by name',
|
||||
path: uriencode`/internal/uptime/service/monitors`,
|
||||
query: {
|
||||
perPage: 10,
|
||||
page: 1,
|
||||
sortOrder: 'asc',
|
||||
sortField: 'name.keyword',
|
||||
query,
|
||||
},
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
const { monitors = [] } = data as any;
|
||||
await pMap(
|
||||
monitors,
|
||||
async (monitor: Record<string, any>) => {
|
||||
await this.requester.request({
|
||||
description: 'delete monitor',
|
||||
path: uriencode`/internal/uptime/service/monitors/${monitor.id}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
},
|
||||
{ concurrency: 10 }
|
||||
);
|
||||
}
|
||||
|
||||
async enableDefaultAlertingViaApi() {
|
||||
try {
|
||||
await axios.post(
|
||||
this.kibanaUrl + SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING,
|
||||
{ isDisabled: false },
|
||||
{
|
||||
auth: { username: 'elastic', password: 'changeme' },
|
||||
headers: { 'kbn-xsrf': 'true' },
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
async addTestSummaryDocument({
|
||||
isDown = false,
|
||||
timestamp = new Date(Date.now()).toISOString(),
|
||||
monitorId,
|
||||
name,
|
||||
}: { monitorId?: string; isDown?: boolean; timestamp?: string; name?: string } = {}) {
|
||||
const getService = this.params.getService;
|
||||
const es: Client = getService('es');
|
||||
await es.index({
|
||||
index: 'synthetics-http-default',
|
||||
document: {
|
||||
...(isDown ? firstDownHit({ timestamp, monitorId, name }) : firstUpHit),
|
||||
'@timestamp': timestamp,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async cleaUpAlerts() {
|
||||
const getService = this.params.getService;
|
||||
const es: Client = getService('es');
|
||||
const listOfIndices = await es.cat.indices({ format: 'json' });
|
||||
for (const index of listOfIndices) {
|
||||
if (index.index?.startsWith('.internal.alerts-observability.uptime.alerts')) {
|
||||
await es.deleteByQuery({ index: index.index, query: { match_all: {} } });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async cleaUpRules() {
|
||||
try {
|
||||
const { data: response } = await this.requester.request({
|
||||
description: 'get monitors by name',
|
||||
path: `/internal/alerting/rules/_find`,
|
||||
query: {
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
},
|
||||
method: 'GET',
|
||||
});
|
||||
const { data = [] } = response as any;
|
||||
|
||||
if (data.length > 0) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Deleting ${data.length} rules`);
|
||||
|
||||
await axios.patch(
|
||||
this.kibanaUrl + '/internal/alerting/rules/_bulk_delete',
|
||||
{
|
||||
ids: data.map((rule: any) => rule.id),
|
||||
},
|
||||
{ auth: { username: 'elastic', password: 'changeme' }, headers: { 'kbn-xsrf': 'true' } }
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
async cleanTestMonitors() {
|
||||
const getService = this.params.getService;
|
||||
const server = getService('kibanaServer');
|
||||
|
||||
try {
|
||||
await server.savedObjects.clean({ types: ['synthetics-monitor'] });
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { journey, step, expect, before } from '@elastic/synthetics';
|
||||
import { assertText, byTestId, waitForLoadingToFinish } from '@kbn/observability-plugin/e2e/utils';
|
||||
import { RetryService } from '@kbn/ftr-common-functional-services';
|
||||
import { loginPageProvider } from '../../../page_objects/login';
|
||||
|
||||
journey('StatusFlyoutInAlertingApp', async ({ page, params }) => {
|
||||
|
@ -14,7 +15,8 @@ journey('StatusFlyoutInAlertingApp', async ({ page, params }) => {
|
|||
before(async () => {
|
||||
await waitForLoadingToFinish({ page });
|
||||
});
|
||||
|
||||
const getService = params.getService;
|
||||
const retry: RetryService = getService('retry');
|
||||
const baseUrl = `${params.kibanaUrl}/app/management/insightsAndAlerting/triggersActions/rules`;
|
||||
|
||||
step('Go to Alerting app', async () => {
|
||||
|
@ -25,11 +27,17 @@ journey('StatusFlyoutInAlertingApp', async ({ page, params }) => {
|
|||
});
|
||||
|
||||
step('Open monitor status flyout', async () => {
|
||||
await page.click(byTestId('createFirstRuleButton'));
|
||||
await page.click('text=Create rule');
|
||||
await waitForLoadingToFinish({ page });
|
||||
await page.click(byTestId('"xpack.uptime.alerts.monitorStatus-SelectOption"'));
|
||||
await waitForLoadingToFinish({ page });
|
||||
await assertText({ page, text: 'This alert will apply to approximately 0 monitors.' });
|
||||
|
||||
await retry.tryForTime(60 * 1000, async () => {
|
||||
const text = await page.textContent(byTestId('alertSnapShotCount'));
|
||||
expect(text).toContain('This alert will apply to approximately');
|
||||
const getNUmber = text?.split('This alert will apply to approximately ')[1][0];
|
||||
expect(Number(getNUmber)).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
step('can add filters', async () => {
|
||||
|
@ -59,7 +67,7 @@ journey('StatusFlyoutInAlertingApp', async ({ page, params }) => {
|
|||
});
|
||||
|
||||
step('Open tls alert flyout', async () => {
|
||||
await page.click(byTestId('createFirstRuleButton'));
|
||||
await page.click('text=Create rule');
|
||||
await waitForLoadingToFinish({ page });
|
||||
await page.click(byTestId('"xpack.uptime.alerts.tlsCertificate-SelectOption"'));
|
||||
await waitForLoadingToFinish({ page });
|
||||
|
|
|
@ -25,7 +25,7 @@ journey('TlsFlyoutInAlertingApp', async ({ page, params }) => {
|
|||
});
|
||||
|
||||
step('Open tls alert flyout', async () => {
|
||||
await page.click(byTestId('createFirstRuleButton'));
|
||||
await page.click('text=Create rule');
|
||||
await waitForLoadingToFinish({ page });
|
||||
await page.click(byTestId('"xpack.uptime.alerts.tlsCertificate-SelectOption"'));
|
||||
await waitForLoadingToFinish({ page });
|
||||
|
|
|
@ -89,12 +89,14 @@ export function syntheticsAppPageProvider({ page, kibanaUrl }: { page: Page; kib
|
|||
async deleteMonitors() {
|
||||
let isSuccessful: boolean = false;
|
||||
while (true) {
|
||||
if ((await page.$(this.byTestId('syntheticsMonitorListActions'))) === null) {
|
||||
if ((await page.$(this.byTestId('euiCollapsedItemActionsButton'))) === null) {
|
||||
isSuccessful = true;
|
||||
break;
|
||||
}
|
||||
await page.click(this.byTestId('syntheticsMonitorListActions'), { delay: 800 });
|
||||
await page.click('text=delete', { delay: 800 });
|
||||
await page.click(this.byTestId('euiCollapsedItemActionsButton'), { delay: 800 });
|
||||
await page.click(`.euiContextMenuPanel ${this.byTestId('syntheticsMonitorDeleteAction')}`, {
|
||||
delay: 800,
|
||||
});
|
||||
await page.waitForSelector('[data-test-subj="confirmModalTitleText"]');
|
||||
await this.clickByTestSubj('confirmModalConfirmButton');
|
||||
isSuccessful = Boolean(await this.findByTestSubj('uptimeDeleteMonitorSuccess'));
|
||||
|
@ -106,8 +108,11 @@ export function syntheticsAppPageProvider({ page, kibanaUrl }: { page: Page; kib
|
|||
|
||||
async navigateToEditMonitor() {
|
||||
await page.waitForSelector('text=Showing');
|
||||
await this.clickByTestSubj('syntheticsMonitorListActions');
|
||||
await page.click('text=Edit', { timeout: 2 * 60 * 1000, delay: 800 });
|
||||
await this.clickByTestSubj('euiCollapsedItemActionsButton');
|
||||
await page.click(`.euiContextMenuPanel ${this.byTestId('syntheticsMonitorEditAction')}`, {
|
||||
timeout: 2 * 60 * 1000,
|
||||
delay: 800,
|
||||
});
|
||||
await this.findByText('Edit monitor');
|
||||
},
|
||||
|
||||
|
|
|
@ -15,5 +15,6 @@
|
|||
"@kbn/dev-utils",
|
||||
"@kbn/observability-plugin",
|
||||
"@kbn/ux-plugin",
|
||||
"@kbn/ftr-common-functional-services",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"kibanaVersion": "kibana",
|
||||
"optionalPlugins": ["cloud", "data", "fleet", "home", "ml", "telemetry"],
|
||||
"requiredPlugins": [
|
||||
"actions",
|
||||
"alerting",
|
||||
"cases",
|
||||
"embeddable",
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ToggleFlyoutTranslations = {
|
||||
toggleButtonAriaLabel: i18n.translate(
|
||||
'xpack.synthetics.alertsRulesPopover.toggleButton.ariaLabel',
|
||||
{
|
||||
defaultMessage: 'Open alerts and rules menu',
|
||||
}
|
||||
),
|
||||
|
||||
toggleMonitorStatusAriaLabel: i18n.translate('xpack.synthetics.toggleAlertFlyout.ariaLabel', {
|
||||
defaultMessage: 'Open add rule flyout',
|
||||
}),
|
||||
toggleMonitorStatusContent: i18n.translate('xpack.synthetics.toggleAlertButton.content', {
|
||||
defaultMessage: 'Monitor status rule',
|
||||
}),
|
||||
navigateToAlertingUIAriaLabel: i18n.translate('xpack.synthetics.app.navigateToAlertingUi', {
|
||||
defaultMessage: 'Leave Synthetics and go to Alerting Management page',
|
||||
}),
|
||||
navigateToAlertingButtonContent: i18n.translate(
|
||||
'xpack.synthetics.app.navigateToAlertingButton.content',
|
||||
{
|
||||
defaultMessage: 'Manage rules',
|
||||
}
|
||||
),
|
||||
alertsAndRules: i18n.translate('xpack.synthetics.alerts.toggleAlertFlyoutButtonText', {
|
||||
defaultMessage: 'Alerts and rules',
|
||||
}),
|
||||
};
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { useFetcher } from '@kbn/observability-plugin/public';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { Rule } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { setAlertFlyoutVisible } from '../../../state';
|
||||
import { enableDefaultAlertingAPI } from '../../../state/alert_rules/api';
|
||||
import { ClientPluginsStart } from '../../../../../plugin';
|
||||
|
||||
export const useSyntheticsAlert = (isOpen: boolean) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [alert, setAlert] = useState<Rule | null>(null);
|
||||
|
||||
const { data, loading } = useFetcher(() => {
|
||||
if (isOpen) {
|
||||
return enableDefaultAlertingAPI();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setAlert(data);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const { triggersActionsUi } = useKibana<ClientPluginsStart>().services;
|
||||
|
||||
const EditAlertFlyout = useMemo(() => {
|
||||
if (!alert) {
|
||||
return null;
|
||||
}
|
||||
return triggersActionsUi.getEditAlertFlyout({
|
||||
onClose: () => dispatch(setAlertFlyoutVisible(false)),
|
||||
initialRule: alert,
|
||||
});
|
||||
}, [alert, dispatch, triggersActionsUi]);
|
||||
|
||||
return useMemo(() => ({ loading, EditAlertFlyout }), [EditAlertFlyout, loading]);
|
||||
};
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* 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, { useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import {
|
||||
EuiContextMenu,
|
||||
EuiContextMenuPanelDescriptor,
|
||||
EuiContextMenuPanelItemDescriptor,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHeaderLink,
|
||||
EuiLoadingSpinner,
|
||||
EuiPopover,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ManageRulesLink } from '../common/links/manage_rules_link';
|
||||
import { ClientPluginsStart } from '../../../../plugin';
|
||||
import { ToggleFlyoutTranslations } from './hooks/translations';
|
||||
import { useSyntheticsAlert } from './hooks/use_synthetics_alert';
|
||||
import { selectAlertFlyoutVisibility, setAlertFlyoutVisible } from '../../state';
|
||||
|
||||
export const ToggleAlertFlyoutButton = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const { application } = useKibana<ClientPluginsStart>().services;
|
||||
const hasUptimeWrite = application?.capabilities.uptime?.save ?? false;
|
||||
|
||||
const { EditAlertFlyout, loading } = useSyntheticsAlert(isOpen);
|
||||
|
||||
const monitorStatusAlertContextMenuItem: EuiContextMenuPanelItemDescriptor = {
|
||||
'aria-label': ToggleFlyoutTranslations.toggleMonitorStatusAriaLabel,
|
||||
'data-test-subj': 'xpack.synthetics.toggleAlertFlyout',
|
||||
name: (
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem>{ToggleFlyoutTranslations.toggleMonitorStatusContent}</EuiFlexItem>
|
||||
{loading && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
onClick: () => {
|
||||
dispatch(setAlertFlyoutVisible(true));
|
||||
setIsOpen(false);
|
||||
},
|
||||
toolTipContent: !hasUptimeWrite ? noWritePermissionsTooltipContent : null,
|
||||
disabled: !hasUptimeWrite || loading,
|
||||
icon: 'bell',
|
||||
};
|
||||
|
||||
const managementContextItem: EuiContextMenuPanelItemDescriptor = {
|
||||
'aria-label': ToggleFlyoutTranslations.navigateToAlertingUIAriaLabel,
|
||||
'data-test-subj': 'xpack.synthetics.navigateToAlertingUi',
|
||||
name: <ManageRulesLink />,
|
||||
icon: 'tableOfContents',
|
||||
};
|
||||
|
||||
const panels: EuiContextMenuPanelDescriptor[] = [
|
||||
{
|
||||
id: 0,
|
||||
items: [monitorStatusAlertContextMenuItem, managementContextItem],
|
||||
},
|
||||
];
|
||||
|
||||
const alertFlyoutVisible = useSelector(selectAlertFlyoutVisibility);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiHeaderLink
|
||||
color="text"
|
||||
aria-label={ToggleFlyoutTranslations.toggleButtonAriaLabel}
|
||||
data-test-subj="xpack.synthetics.alertsPopover.toggleButton"
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{ToggleFlyoutTranslations.alertsAndRules}
|
||||
</EuiHeaderLink>
|
||||
}
|
||||
closePopover={() => setIsOpen(false)}
|
||||
isOpen={isOpen}
|
||||
ownFocus
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiContextMenu initialPanelId={0} panels={panels} />
|
||||
</EuiPopover>
|
||||
{alertFlyoutVisible && EditAlertFlyout}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const noWritePermissionsTooltipContent = i18n.translate(
|
||||
'xpack.synthetics.alertDropdown.noWritePermissions',
|
||||
{
|
||||
defaultMessage: 'You need read-write access to Uptime to create alerts in this app.',
|
||||
}
|
||||
);
|
|
@ -28,15 +28,4 @@ describe('ActionMenuContent', () => {
|
|||
expect(analyzeAnchor.getAttribute('href')).toContain('/app/observability/exploratory-view');
|
||||
expect(getByText('Explore data'));
|
||||
});
|
||||
|
||||
it('renders Add Data link', () => {
|
||||
const { getByLabelText, getByText } = render(<ActionMenuContent />);
|
||||
|
||||
const addDataAnchor = getByLabelText('Navigate to a tutorial about adding Uptime data');
|
||||
|
||||
// this href value is mocked, so it doesn't correspond to the real link
|
||||
// that Kibana core services will provide
|
||||
expect(addDataAnchor.getAttribute('href')).toBe('/home#/tutorial/uptimeMonitors');
|
||||
expect(getByText('Add data'));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,16 +11,12 @@ import { i18n } from '@kbn/i18n';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useHistory, useRouteMatch } from 'react-router-dom';
|
||||
import { createExploratoryViewUrl } from '@kbn/observability-plugin/public';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { useSyntheticsSettingsContext } from '../../../contexts';
|
||||
import { useGetUrlParams } from '../../../hooks';
|
||||
import { MONITOR_ROUTE, SETTINGS_ROUTE } from '../../../../../../common/constants';
|
||||
import { stringifyUrlParams } from '../../../utils/url_params';
|
||||
import { InspectorHeaderLink } from './inspector_header_link';
|
||||
|
||||
const ADD_DATA_LABEL = i18n.translate('xpack.synthetics.addDataButtonLabel', {
|
||||
defaultMessage: 'Add data',
|
||||
});
|
||||
import { ToggleAlertFlyoutButton } from '../../alerts/toggle_alert_flyout_button';
|
||||
|
||||
const ANALYZE_DATA = i18n.translate('xpack.synthetics.analyzeDataButtonLabel', {
|
||||
defaultMessage: 'Explore data',
|
||||
|
@ -32,7 +28,6 @@ const ANALYZE_MESSAGE = i18n.translate('xpack.synthetics.analyzeDataButtonLabel.
|
|||
});
|
||||
|
||||
export function ActionMenuContent(): React.ReactElement {
|
||||
const kibana = useKibana();
|
||||
const { basePath } = useSyntheticsSettingsContext();
|
||||
const params = useGetUrlParams();
|
||||
const { dateRangeStart, dateRangeEnd } = params;
|
||||
|
@ -71,7 +66,7 @@ export function ActionMenuContent(): React.ReactElement {
|
|||
|
||||
return (
|
||||
<EuiHeaderLinks gutterSize="xs">
|
||||
{/* <ManageMonitorsBtn /> TODO: See if it's needed for new Synthetics App */}
|
||||
<ToggleAlertFlyoutButton />
|
||||
|
||||
<EuiHeaderLink
|
||||
aria-label={i18n.translate('xpack.synthetics.page_header.settingsLink.label', {
|
||||
|
@ -104,16 +99,6 @@ export function ActionMenuContent(): React.ReactElement {
|
|||
</EuiHeaderLink>
|
||||
</EuiToolTip>
|
||||
|
||||
<EuiHeaderLink
|
||||
aria-label={i18n.translate('xpack.synthetics.page_header.addDataLink.label', {
|
||||
defaultMessage: 'Navigate to a tutorial about adding Uptime data',
|
||||
})}
|
||||
href={kibana.services?.application?.getUrlForApp('/home#/tutorial/uptimeMonitors')}
|
||||
color="primary"
|
||||
iconType="indexOpen"
|
||||
>
|
||||
{ADD_DATA_LABEL}
|
||||
</EuiHeaderLink>
|
||||
<InspectorHeaderLink />
|
||||
</EuiHeaderLinks>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 { EuiLink } from '@elastic/eui';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { ClientPluginsStart } from '../../../../../plugin';
|
||||
import { ToggleFlyoutTranslations } from '../../alerts/hooks/translations';
|
||||
|
||||
export const ManageRulesLink = () => {
|
||||
const { observability } = useKibana<ClientPluginsStart>().services;
|
||||
|
||||
const manageRulesUrl = observability.useRulesLink();
|
||||
|
||||
return (
|
||||
<EuiLink color="text" href={manageRulesUrl.href}>
|
||||
{ToggleFlyoutTranslations.navigateToAlertingButtonContent}
|
||||
</EuiLink>
|
||||
);
|
||||
};
|
|
@ -47,7 +47,7 @@ import {
|
|||
VerificationMode,
|
||||
FieldMeta,
|
||||
} from '../types';
|
||||
import { DEFAULT_BROWSER_ADVANCED_FIELDS } from '../constants';
|
||||
import { AlertConfigKey, DEFAULT_BROWSER_ADVANCED_FIELDS } from '../constants';
|
||||
import { HeaderField } from '../fields/header_field';
|
||||
import { RequestBodyField } from '../fields/request_body_field';
|
||||
import { ResponseBodyIndexField } from '../fields/index_response_body_field';
|
||||
|
@ -437,6 +437,27 @@ export const FIELD: Record<string, FieldMeta> = {
|
|||
},
|
||||
}),
|
||||
},
|
||||
[ConfigKey.ALERT_CONFIG]: {
|
||||
fieldKey: AlertConfigKey.STATUS_ENABLED,
|
||||
component: EuiSwitch,
|
||||
label: i18n.translate('xpack.synthetics.monitorConfig.enabledAlerting.label', {
|
||||
defaultMessage: 'Enable status alerts',
|
||||
}),
|
||||
controlled: true,
|
||||
props: ({ isEdit, setValue, field }) => ({
|
||||
id: 'syntheticsMonitorConfigIsAlertEnabled',
|
||||
label: isEdit
|
||||
? i18n.translate('xpack.synthetics.monitorConfig.edit.alertEnabled.label', {
|
||||
defaultMessage: 'Disabling will stop alerting on this monitor.',
|
||||
})
|
||||
: i18n.translate('xpack.synthetics.monitorConfig.create.alertEnabled.label', {
|
||||
defaultMessage: 'Enable status alerts on this monitor.',
|
||||
}),
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(AlertConfigKey.STATUS_ENABLED, !!event.target.checked);
|
||||
},
|
||||
}),
|
||||
},
|
||||
[ConfigKey.TAGS]: {
|
||||
fieldKey: ConfigKey.TAGS,
|
||||
component: ComboBox,
|
||||
|
@ -497,7 +518,7 @@ export const FIELD: Record<string, FieldMeta> = {
|
|||
}),
|
||||
helpText: i18n.translate('xpack.synthetics.monitorConfig.apmServiceName.helpText', {
|
||||
defaultMessage:
|
||||
'Corrseponds to the service.name ECS field from APM. Set this to enable integrations between APM and Synthetics data.',
|
||||
'Corresponds to the service.name ECS field from APM. Set this to enable integrations between APM and Synthetics data.',
|
||||
}),
|
||||
controlled: true,
|
||||
props: ({ field }) => ({
|
||||
|
|
|
@ -169,6 +169,7 @@ export const FORM_CONFIG: FieldConfig = {
|
|||
FIELD[ConfigKey.MAX_REDIRECTS],
|
||||
FIELD[ConfigKey.TIMEOUT],
|
||||
FIELD[ConfigKey.ENABLED],
|
||||
FIELD[ConfigKey.ALERT_CONFIG],
|
||||
],
|
||||
advanced: [
|
||||
DEFAULT_DATA_OPTIONS,
|
||||
|
@ -187,6 +188,7 @@ export const FORM_CONFIG: FieldConfig = {
|
|||
FIELD[ConfigKey.SCHEDULE],
|
||||
FIELD[ConfigKey.TIMEOUT],
|
||||
FIELD[ConfigKey.ENABLED],
|
||||
FIELD[ConfigKey.ALERT_CONFIG],
|
||||
],
|
||||
advanced: [
|
||||
DEFAULT_DATA_OPTIONS,
|
||||
|
@ -203,6 +205,7 @@ export const FORM_CONFIG: FieldConfig = {
|
|||
FIELD[ConfigKey.SCHEDULE],
|
||||
FIELD[ConfigKey.THROTTLING_CONFIG],
|
||||
FIELD[ConfigKey.ENABLED],
|
||||
FIELD[ConfigKey.ALERT_CONFIG],
|
||||
],
|
||||
step3: [FIELD[ConfigKey.SOURCE_INLINE], FIELD[ConfigKey.PARAMS]],
|
||||
scriptEdit: [FIELD[ConfigKey.SOURCE_INLINE]],
|
||||
|
@ -229,6 +232,7 @@ export const FORM_CONFIG: FieldConfig = {
|
|||
FIELD[ConfigKey.SCHEDULE],
|
||||
FIELD[ConfigKey.THROTTLING_CONFIG],
|
||||
FIELD[ConfigKey.ENABLED],
|
||||
FIELD[ConfigKey.ALERT_CONFIG],
|
||||
],
|
||||
advanced: [
|
||||
{
|
||||
|
|
|
@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { useFormContext } from 'react-hook-form';
|
||||
import { useFetcher, FETCH_STATUS } from '@kbn/observability-plugin/public';
|
||||
import { DeleteMonitor } from '../../monitors_page/management/monitor_list_table/delete_monitor';
|
||||
import { ConfigKey, SourceType, SyntheticsMonitor } from '../types';
|
||||
import { SyntheticsMonitor } from '../types';
|
||||
import { format } from './formatter';
|
||||
import {
|
||||
createMonitorAPI,
|
||||
|
@ -33,7 +33,9 @@ export const ActionBar = () => {
|
|||
formState: { errors },
|
||||
} = useFormContext();
|
||||
|
||||
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
|
||||
const [monitorPendingDeletion, setMonitorPendingDeletion] = useState<SyntheticsMonitor | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const [monitorData, setMonitorData] = useState<SyntheticsMonitor | undefined>(undefined);
|
||||
|
||||
|
@ -78,7 +80,7 @@ export const ActionBar = () => {
|
|||
|
||||
const formSubmitter = (formData: Record<string, any>) => {
|
||||
if (!Object.keys(errors).length) {
|
||||
setMonitorData(format(formData) as SyntheticsMonitor);
|
||||
setMonitorData(format(formData));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -88,12 +90,12 @@ export const ActionBar = () => {
|
|||
<>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={true}>
|
||||
{isEdit && (
|
||||
{isEdit && monitorObject && (
|
||||
<div>
|
||||
<EuiButton
|
||||
color="danger"
|
||||
onClick={() => {
|
||||
setIsDeleteModalVisible(true);
|
||||
setMonitorPendingDeletion(monitorObject?.attributes);
|
||||
}}
|
||||
>
|
||||
{DELETE_MONITOR_LABEL}
|
||||
|
@ -116,17 +118,13 @@ export const ActionBar = () => {
|
|||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{isDeleteModalVisible && (
|
||||
{monitorPendingDeletion && monitorObject && (
|
||||
<DeleteMonitor
|
||||
configId={monitorId}
|
||||
name={monitorObject?.attributes?.[ConfigKey.NAME] ?? ''}
|
||||
fields={monitorObject?.attributes}
|
||||
reloadPage={() => {
|
||||
history.push(MONITORS_ROUTE);
|
||||
}}
|
||||
isProjectMonitor={
|
||||
monitorObject?.attributes?.[ConfigKey.MONITOR_SOURCE_TYPE] === SourceType.PROJECT
|
||||
}
|
||||
setIsDeleteModalVisible={setIsDeleteModalVisible}
|
||||
setMonitorPendingDeletion={setMonitorPendingDeletion}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -18,8 +18,12 @@ import {
|
|||
selectorError,
|
||||
} from '../../../state';
|
||||
|
||||
export const useSelectedMonitor = () => {
|
||||
const { monitorId } = useParams<{ monitorId: string }>();
|
||||
export const useSelectedMonitor = (monId?: string) => {
|
||||
let monitorId = monId;
|
||||
const { monitorId: urlMonitorId } = useParams<{ monitorId: string }>();
|
||||
if (!monitorId) {
|
||||
monitorId = urlMonitorId;
|
||||
}
|
||||
const monitorsList = useSelector(selectEncryptedSyntheticsSavedMonitors);
|
||||
const { loading: monitorListLoading } = useSelector(selectMonitorListState);
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ import { EuiSpacer } from '@elastic/eui';
|
|||
|
||||
import type { useMonitorList } from '../hooks/use_monitor_list';
|
||||
import { MonitorAsyncError } from './monitor_errors/monitor_async_error';
|
||||
import { useInlineErrors } from '../hooks/use_inline_errors';
|
||||
import { useOverviewStatus } from '../hooks/use_overview_status';
|
||||
import { ListFilters } from './list_filters/list_filters';
|
||||
import { MonitorList } from './monitor_list_table/monitor_list';
|
||||
|
@ -33,11 +32,13 @@ export const MonitorListContainer = ({
|
|||
reloadPage,
|
||||
} = monitorListProps;
|
||||
|
||||
const { errorSummaries, loading: errorsLoading } = useInlineErrors({
|
||||
onlyInvalidMonitors: false,
|
||||
sortField: pageState.sortField,
|
||||
sortOrder: pageState.sortOrder,
|
||||
});
|
||||
// TODO: Display inline errors in the management table
|
||||
|
||||
// const { errorSummaries, loading: errorsLoading } = useInlineErrors({
|
||||
// onlyInvalidMonitors: false,
|
||||
// sortField: pageState.sortField,
|
||||
// sortOrder: pageState.sortOrder,
|
||||
// });
|
||||
|
||||
const overviewStatusArgs = useMemo(() => {
|
||||
return {
|
||||
|
@ -61,11 +62,10 @@ export const MonitorListContainer = ({
|
|||
total={total}
|
||||
pageState={pageState}
|
||||
error={error}
|
||||
loading={monitorsLoading || errorsLoading}
|
||||
status={status}
|
||||
errorSummaries={errorSummaries}
|
||||
loading={monitorsLoading}
|
||||
loadPage={loadPage}
|
||||
reloadPage={reloadPage}
|
||||
status={status}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,139 +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 React, { useContext, useState } from 'react';
|
||||
import { EuiThemeComputed } from '@elastic/eui/src/services/theme/types';
|
||||
import { EuiContextMenuPanel, EuiContextMenuItem, EuiPopover, EuiButtonEmpty } from '@elastic/eui';
|
||||
import { DeleteMonitor } from './delete_monitor';
|
||||
import { SyntheticsSettingsContext } from '../../../../contexts/synthetics_settings_context';
|
||||
|
||||
import * as labels from './labels';
|
||||
|
||||
interface Props {
|
||||
euiTheme: EuiThemeComputed;
|
||||
configId: string;
|
||||
name: string;
|
||||
canEditSynthetics: boolean;
|
||||
isProjectMonitor?: boolean;
|
||||
reloadPage: () => void;
|
||||
}
|
||||
|
||||
export const Actions = ({
|
||||
euiTheme,
|
||||
configId,
|
||||
name,
|
||||
reloadPage,
|
||||
canEditSynthetics,
|
||||
isProjectMonitor,
|
||||
}: Props) => {
|
||||
const { basePath } = useContext(SyntheticsSettingsContext);
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
|
||||
|
||||
// TODO: Move deletion logic to redux state
|
||||
|
||||
const openPopover = () => {
|
||||
setIsPopoverOpen(true);
|
||||
};
|
||||
|
||||
const closePopover = () => {
|
||||
setIsPopoverOpen(false);
|
||||
};
|
||||
|
||||
const handleDeleteMonitor = () => {
|
||||
setIsDeleteModalVisible(true);
|
||||
closePopover();
|
||||
};
|
||||
|
||||
const menuButton = (
|
||||
<EuiButtonEmpty
|
||||
iconType="boxesHorizontal"
|
||||
color="primary"
|
||||
iconSide="right"
|
||||
data-test-subj="syntheticsMonitorListActions"
|
||||
onClick={openPopover}
|
||||
/>
|
||||
);
|
||||
|
||||
/*
|
||||
TODO: Implement duplication functionality
|
||||
const duplicateMenuItem = (
|
||||
<EuiContextMenuItem key="xpack.synthetics.duplicateMonitor" icon="copy" onClick={closePopover}>
|
||||
{labels.DUPLICATE_LABEL}
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
*/
|
||||
|
||||
/*
|
||||
TODO: See if disable enabled is needed as an action menu item
|
||||
const disableEnableMenuItem = (
|
||||
isDisabled ? (
|
||||
<EuiContextMenuItem
|
||||
key="xpack.synthetics.enableMonitor"
|
||||
icon="play"
|
||||
onClick={handleEnableMonitor}
|
||||
>
|
||||
{labels.ENABLE_LABEL}
|
||||
</EuiContextMenuItem>
|
||||
) : (
|
||||
<EuiContextMenuItem
|
||||
key="xpack.synthetics.disableMonitor"
|
||||
icon="pause"
|
||||
onClick={handleDisableMonitor}
|
||||
>
|
||||
{labels.DISABLE_LABEL}
|
||||
</EuiContextMenuItem>
|
||||
)
|
||||
);
|
||||
*/
|
||||
|
||||
const menuItems = [
|
||||
<EuiContextMenuItem
|
||||
key="xpack.synthetics.editMonitor"
|
||||
icon="pencil"
|
||||
onClick={closePopover}
|
||||
href={`${basePath}/app/synthetics/edit-monitor/${configId}`}
|
||||
disabled={!canEditSynthetics}
|
||||
>
|
||||
{labels.EDIT_LABEL}
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
css={{ color: euiTheme.colors.danger }}
|
||||
key="xpack.synthetics.deleteMonitor"
|
||||
icon="trash"
|
||||
disabled={!canEditSynthetics}
|
||||
onClick={handleDeleteMonitor}
|
||||
>
|
||||
{labels.DELETE_LABEL}
|
||||
</EuiContextMenuItem>,
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPopover
|
||||
id={`xpack.synthetics.${configId}`}
|
||||
button={menuButton}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
panelPaddingSize="s"
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<EuiContextMenuPanel size="s" items={menuItems} />
|
||||
</EuiPopover>
|
||||
|
||||
{isDeleteModalVisible && (
|
||||
<DeleteMonitor
|
||||
configId={configId}
|
||||
name={name}
|
||||
reloadPage={reloadPage}
|
||||
isProjectMonitor={isProjectMonitor}
|
||||
setIsDeleteModalVisible={setIsDeleteModalVisible}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -5,11 +5,18 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiBadge, EuiBasicTableColumn, EuiThemeComputed } from '@elastic/eui';
|
||||
import { EuiBadge, EuiBasicTableColumn } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { FETCH_STATUS } from '@kbn/observability-plugin/public';
|
||||
import {
|
||||
isStatusEnabled,
|
||||
toggleStatusAlert,
|
||||
} from '../../../../../../../common/runtime_types/monitor_management/alert_config';
|
||||
import { TagsBadges } from '../../../common/components/tag_badges';
|
||||
import { useMonitorAlertEnable } from '../../../../hooks/use_monitor_alert_enable';
|
||||
import * as labels from './labels';
|
||||
import { MonitorDetailsLink } from './monitor_details_link';
|
||||
|
||||
import {
|
||||
|
@ -17,37 +24,35 @@ import {
|
|||
DataStream,
|
||||
EncryptedSyntheticsSavedMonitor,
|
||||
OverviewStatusState,
|
||||
Ping,
|
||||
ServiceLocations,
|
||||
SourceType,
|
||||
SyntheticsMonitorSchedule,
|
||||
} from '../../../../../../../common/runtime_types';
|
||||
|
||||
import { getFrequencyLabel } from './labels';
|
||||
import { Actions } from './actions';
|
||||
import { MonitorEnabled } from './monitor_enabled';
|
||||
import { MonitorLocations } from './monitor_locations';
|
||||
|
||||
export function useMonitorListColumns({
|
||||
basePath,
|
||||
euiTheme,
|
||||
canEditSynthetics,
|
||||
reloadPage,
|
||||
loading,
|
||||
status,
|
||||
setMonitorPendingDeletion,
|
||||
}: {
|
||||
basePath: string;
|
||||
euiTheme: EuiThemeComputed;
|
||||
errorSummaries?: Ping[];
|
||||
errorSummariesById: Map<string, Ping>;
|
||||
canEditSynthetics: boolean;
|
||||
syntheticsMonitors: EncryptedSyntheticsSavedMonitor[];
|
||||
loading: boolean;
|
||||
status: OverviewStatusState | null;
|
||||
reloadPage: () => void;
|
||||
}) {
|
||||
setMonitorPendingDeletion: (config: EncryptedSyntheticsSavedMonitor) => void;
|
||||
}): Array<EuiBasicTableColumn<EncryptedSyntheticsSavedMonitor>> {
|
||||
const history = useHistory();
|
||||
|
||||
const { alertStatus, updateAlertEnabledState } = useMonitorAlertEnable();
|
||||
|
||||
const isActionLoading = (fields: EncryptedSyntheticsSavedMonitor) => {
|
||||
return alertStatus(fields[ConfigKey.CONFIG_ID]) === FETCH_STATUS.LOADING;
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
align: 'left' as const,
|
||||
|
@ -57,7 +62,7 @@ export function useMonitorListColumns({
|
|||
}),
|
||||
sortable: true,
|
||||
render: (_: string, monitor: EncryptedSyntheticsSavedMonitor) => (
|
||||
<MonitorDetailsLink basePath={basePath} monitor={monitor} />
|
||||
<MonitorDetailsLink monitor={monitor} />
|
||||
),
|
||||
},
|
||||
{
|
||||
|
@ -131,16 +136,64 @@ export function useMonitorListColumns({
|
|||
name: i18n.translate('xpack.synthetics.management.monitorList.actions', {
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
render: (fields: EncryptedSyntheticsSavedMonitor) => (
|
||||
<Actions
|
||||
euiTheme={euiTheme}
|
||||
configId={fields[ConfigKey.CONFIG_ID]}
|
||||
name={fields[ConfigKey.NAME]}
|
||||
canEditSynthetics={canEditSynthetics}
|
||||
reloadPage={reloadPage}
|
||||
isProjectMonitor={fields[ConfigKey.MONITOR_SOURCE_TYPE] === SourceType.PROJECT}
|
||||
/>
|
||||
),
|
||||
actions: [
|
||||
{
|
||||
'data-test-subj': 'syntheticsMonitorEditAction',
|
||||
isPrimary: true,
|
||||
name: labels.EDIT_LABEL,
|
||||
description: labels.EDIT_LABEL,
|
||||
icon: 'pencil',
|
||||
type: 'icon',
|
||||
enabled: (fields) => canEditSynthetics && !isActionLoading(fields),
|
||||
onClick: (fields) => {
|
||||
history.push({
|
||||
pathname: `/edit-monitor/${fields[ConfigKey.CONFIG_ID]}`,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
'data-test-subj': 'syntheticsMonitorDeleteAction',
|
||||
isPrimary: true,
|
||||
name: labels.DELETE_LABEL,
|
||||
description: labels.DELETE_LABEL,
|
||||
icon: 'trash',
|
||||
type: 'icon',
|
||||
color: 'danger',
|
||||
enabled: (fields) => canEditSynthetics && !isActionLoading(fields),
|
||||
onClick: (fields) => {
|
||||
setMonitorPendingDeletion(fields);
|
||||
},
|
||||
},
|
||||
{
|
||||
description: labels.DISABLE_STATUS_ALERT,
|
||||
name: (fields) =>
|
||||
isStatusEnabled(fields[ConfigKey.ALERT_CONFIG])
|
||||
? labels.DISABLE_STATUS_ALERT
|
||||
: labels.ENABLE_STATUS_ALERT,
|
||||
icon: (fields) =>
|
||||
isStatusEnabled(fields[ConfigKey.ALERT_CONFIG]) ? 'bellSlash' : 'bell',
|
||||
type: 'icon',
|
||||
color: 'danger',
|
||||
enabled: (fields) => canEditSynthetics && !isActionLoading(fields),
|
||||
onClick: (fields) => {
|
||||
updateAlertEnabledState({
|
||||
monitor: {
|
||||
[ConfigKey.ALERT_CONFIG]: toggleStatusAlert(fields[ConfigKey.ALERT_CONFIG]),
|
||||
},
|
||||
name: fields[ConfigKey.NAME],
|
||||
configId: fields[ConfigKey.CONFIG_ID],
|
||||
});
|
||||
},
|
||||
},
|
||||
/*
|
||||
TODO: Implement duplication functionality
|
||||
const duplicateMenuItem = (
|
||||
<EuiContextMenuItem key="xpack.synthetics.duplicateMonitor" icon="copy" onClick={closePopover}>
|
||||
{labels.DUPLICATE_LABEL}
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
*/
|
||||
],
|
||||
},
|
||||
] as Array<EuiBasicTableColumn<EncryptedSyntheticsSavedMonitor>>;
|
||||
];
|
||||
}
|
||||
|
|
|
@ -12,23 +12,29 @@ import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
ConfigKey,
|
||||
EncryptedSyntheticsSavedMonitor,
|
||||
SourceType,
|
||||
SyntheticsMonitor,
|
||||
} from '../../../../../../../common/runtime_types';
|
||||
import { fetchDeleteMonitor } from '../../../../state';
|
||||
import { kibanaService } from '../../../../../../utils/kibana_service';
|
||||
import * as labels from './labels';
|
||||
|
||||
export const DeleteMonitor = ({
|
||||
configId,
|
||||
name,
|
||||
fields,
|
||||
reloadPage,
|
||||
isProjectMonitor,
|
||||
setIsDeleteModalVisible,
|
||||
setMonitorPendingDeletion,
|
||||
}: {
|
||||
configId: string;
|
||||
name: string;
|
||||
fields: SyntheticsMonitor | EncryptedSyntheticsSavedMonitor;
|
||||
reloadPage: () => void;
|
||||
isProjectMonitor?: boolean;
|
||||
setIsDeleteModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setMonitorPendingDeletion: (val: null) => void;
|
||||
}) => {
|
||||
const configId = fields[ConfigKey.CONFIG_ID];
|
||||
const name = fields[ConfigKey.NAME];
|
||||
const isProjectMonitor = fields[ConfigKey.MONITOR_SOURCE_TYPE] === SourceType.PROJECT;
|
||||
|
||||
const [isDeleting, setIsDeleting] = useState<boolean>(false);
|
||||
|
||||
const handleConfirmDelete = () => {
|
||||
|
@ -37,7 +43,7 @@ export const DeleteMonitor = ({
|
|||
|
||||
const { status: monitorDeleteStatus } = useFetcher(() => {
|
||||
if (isDeleting) {
|
||||
return fetchDeleteMonitor({ id: configId });
|
||||
return fetchDeleteMonitor({ configId });
|
||||
}
|
||||
}, [configId, isDeleting]);
|
||||
|
||||
|
@ -78,9 +84,9 @@ export const DeleteMonitor = ({
|
|||
monitorDeleteStatus === FETCH_STATUS.FAILURE
|
||||
) {
|
||||
setIsDeleting(false);
|
||||
setIsDeleteModalVisible(false);
|
||||
setMonitorPendingDeletion(null);
|
||||
}
|
||||
}, [setIsDeleting, isDeleting, reloadPage, monitorDeleteStatus, setIsDeleteModalVisible, name]);
|
||||
}, [setIsDeleting, isDeleting, reloadPage, monitorDeleteStatus, setMonitorPendingDeletion, name]);
|
||||
|
||||
return (
|
||||
<EuiConfirmModal
|
||||
|
@ -88,7 +94,7 @@ export const DeleteMonitor = ({
|
|||
defaultMessage: 'Delete "{name}" monitor?',
|
||||
values: { name },
|
||||
})}
|
||||
onCancel={() => setIsDeleteModalVisible(false)}
|
||||
onCancel={() => setMonitorPendingDeletion(null)}
|
||||
onConfirm={handleConfirmDelete}
|
||||
cancelButtonText={labels.NO_LABEL}
|
||||
confirmButtonText={labels.YES_LABEL}
|
||||
|
|
|
@ -52,6 +52,17 @@ export const EDIT_LABEL = i18n.translate('xpack.synthetics.management.editLabel'
|
|||
defaultMessage: 'Edit',
|
||||
});
|
||||
|
||||
export const ENABLE_STATUS_ALERT = i18n.translate('xpack.synthetics.management.enableStatusAlert', {
|
||||
defaultMessage: 'Enable status alerts',
|
||||
});
|
||||
|
||||
export const DISABLE_STATUS_ALERT = i18n.translate(
|
||||
'xpack.synthetics.management.disableStatusAlert',
|
||||
{
|
||||
defaultMessage: 'Disable status alerts',
|
||||
}
|
||||
);
|
||||
|
||||
export const DUPLICATE_LABEL = i18n.translate('xpack.synthetics.management.duplicateLabel', {
|
||||
defaultMessage: 'Duplicate',
|
||||
});
|
||||
|
|
|
@ -17,13 +17,7 @@ import {
|
|||
import { useMonitorDetailLocator } from '../../hooks/use_monitor_detail_locator';
|
||||
import * as labels from './labels';
|
||||
|
||||
export const MonitorDetailsLink = ({
|
||||
basePath,
|
||||
monitor,
|
||||
}: {
|
||||
basePath: string;
|
||||
monitor: EncryptedSyntheticsSavedMonitor;
|
||||
}) => {
|
||||
export const MonitorDetailsLink = ({ monitor }: { monitor: EncryptedSyntheticsSavedMonitor }) => {
|
||||
const lastSelectedLocationId = useSelector(selectSelectedLocationId);
|
||||
const monitorHasLocation = monitor[ConfigKey.LOCATIONS]?.find(
|
||||
(loc) => loc.id === lastSelectedLocationId
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useContext, useMemo } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import {
|
||||
Criteria,
|
||||
EuiBasicTable,
|
||||
|
@ -16,16 +16,15 @@ import {
|
|||
useIsWithinMinBreakpoint,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DeleteMonitor } from './delete_monitor';
|
||||
import { IHttpSerializedFetchError } from '../../../../state/utils/http_error';
|
||||
import { MonitorListPageState } from '../../../../state';
|
||||
import { useCanEditSynthetics } from '../../../../../../hooks/use_capabilities';
|
||||
import {
|
||||
ConfigKey,
|
||||
Ping,
|
||||
EncryptedSyntheticsSavedMonitor,
|
||||
OverviewStatusState,
|
||||
} from '../../../../../../../common/runtime_types';
|
||||
import { SyntheticsSettingsContext } from '../../../../contexts/synthetics_settings_context';
|
||||
import { useMonitorListColumns } from './columns';
|
||||
import * as labels from './labels';
|
||||
|
||||
|
@ -37,7 +36,6 @@ interface Props {
|
|||
loading: boolean;
|
||||
loadPage: (state: MonitorListPageState) => void;
|
||||
reloadPage: () => void;
|
||||
errorSummaries?: Ping[];
|
||||
status: OverviewStatusState | null;
|
||||
}
|
||||
|
||||
|
@ -50,23 +48,13 @@ export const MonitorList = ({
|
|||
status,
|
||||
loadPage,
|
||||
reloadPage,
|
||||
errorSummaries,
|
||||
}: Props) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const { basePath } = useContext(SyntheticsSettingsContext);
|
||||
const isXl = useIsWithinMinBreakpoint('xxl');
|
||||
const canEditSynthetics = useCanEditSynthetics();
|
||||
|
||||
const errorSummariesById = useMemo(
|
||||
() =>
|
||||
(errorSummaries ?? []).reduce((acc, cur) => {
|
||||
if (cur.config_id) {
|
||||
acc.set(cur.config_id, cur);
|
||||
}
|
||||
return acc;
|
||||
}, new Map<string, Ping>()),
|
||||
[errorSummaries]
|
||||
);
|
||||
const [monitorPendingDeletion, setMonitorPendingDeletion] =
|
||||
useState<EncryptedSyntheticsSavedMonitor | null>(null);
|
||||
|
||||
const handleOnChange = useCallback(
|
||||
({
|
||||
|
@ -107,39 +95,44 @@ export const MonitorList = ({
|
|||
});
|
||||
|
||||
const columns = useMonitorListColumns({
|
||||
basePath,
|
||||
euiTheme,
|
||||
errorSummaries,
|
||||
errorSummariesById,
|
||||
canEditSynthetics,
|
||||
syntheticsMonitors,
|
||||
loading,
|
||||
reloadPage,
|
||||
status,
|
||||
setMonitorPendingDeletion,
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="none">
|
||||
{recordRangeLabel}
|
||||
<EuiSpacer size="s" />
|
||||
<hr style={{ border: `1px solid ${euiTheme.colors.lightShade}` }} />
|
||||
<EuiBasicTable
|
||||
aria-label={i18n.translate('xpack.synthetics.management.monitorList.title', {
|
||||
defaultMessage: 'Synthetics monitors list',
|
||||
})}
|
||||
error={error?.body?.message}
|
||||
loading={loading}
|
||||
isExpandable={true}
|
||||
hasActions={true}
|
||||
itemId="monitor_id"
|
||||
items={syntheticsMonitors}
|
||||
columns={columns}
|
||||
tableLayout={isXl ? 'auto' : 'fixed'}
|
||||
pagination={pagination}
|
||||
sorting={sorting}
|
||||
onChange={handleOnChange}
|
||||
noItemsMessage={loading ? labels.LOADING : labels.NO_DATA_MESSAGE}
|
||||
/>
|
||||
</EuiPanel>
|
||||
<>
|
||||
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="none">
|
||||
{recordRangeLabel}
|
||||
<EuiSpacer size="s" />
|
||||
<hr style={{ border: `1px solid ${euiTheme.colors.lightShade}` }} />
|
||||
<EuiBasicTable
|
||||
aria-label={i18n.translate('xpack.synthetics.management.monitorList.title', {
|
||||
defaultMessage: 'Synthetics monitors list',
|
||||
})}
|
||||
error={error?.body?.message}
|
||||
loading={loading}
|
||||
isExpandable={true}
|
||||
hasActions={true}
|
||||
itemId="monitor_id"
|
||||
items={syntheticsMonitors}
|
||||
columns={columns}
|
||||
tableLayout={isXl ? 'auto' : 'fixed'}
|
||||
pagination={pagination}
|
||||
sorting={sorting}
|
||||
onChange={handleOnChange}
|
||||
noItemsMessage={loading ? labels.LOADING : labels.NO_DATA_MESSAGE}
|
||||
/>
|
||||
</EuiPanel>
|
||||
{monitorPendingDeletion && (
|
||||
<DeleteMonitor
|
||||
fields={monitorPendingDeletion}
|
||||
reloadPage={reloadPage}
|
||||
setMonitorPendingDeletion={setMonitorPendingDeletion}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -25,6 +25,7 @@ describe('ActionsPopover', () => {
|
|||
isServiceManaged: true,
|
||||
},
|
||||
isEnabled: true,
|
||||
isStatusAlertEnabled: true,
|
||||
name: 'Monitor 1',
|
||||
id: 'somelongstring',
|
||||
configId: '1lkjelre',
|
||||
|
|
|
@ -6,11 +6,21 @@
|
|||
*/
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiPopover, EuiButtonIcon, EuiContextMenu, useEuiShadow, EuiPanel } from '@elastic/eui';
|
||||
import {
|
||||
EuiPopover,
|
||||
EuiButtonIcon,
|
||||
EuiContextMenu,
|
||||
useEuiShadow,
|
||||
EuiPanel,
|
||||
EuiLoadingSpinner,
|
||||
} from '@elastic/eui';
|
||||
import { FETCH_STATUS } from '@kbn/observability-plugin/public';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import { MonitorOverviewItem } from '../../../../../../../common/runtime_types';
|
||||
import { toggleStatusAlert } from '../../../../../../../common/runtime_types/monitor_management/alert_config';
|
||||
import { useSelectedMonitor } from '../../../monitor_details/hooks/use_selected_monitor';
|
||||
import { useMonitorAlertEnable } from '../../../../hooks/use_monitor_alert_enable';
|
||||
import { ConfigKey, MonitorOverviewItem } from '../../../../../../../common/runtime_types';
|
||||
import { useMonitorEnableHandler } from '../../../../hooks/use_monitor_enable_handler';
|
||||
import { setFlyoutConfig } from '../../../../state/overview/actions';
|
||||
import { useEditMonitorLocator } from '../../hooks/use_edit_monitor_locator';
|
||||
|
@ -92,6 +102,8 @@ export function ActionsPopover({
|
|||
});
|
||||
const editUrl = useEditMonitorLocator({ configId: monitor.configId });
|
||||
|
||||
const { monitor: monitorFields } = useSelectedMonitor(monitor.configId);
|
||||
|
||||
const labels = useMemo(
|
||||
() => ({
|
||||
enabledSuccessLabel: enabledSuccessLabel(monitor.name),
|
||||
|
@ -106,6 +118,8 @@ export function ActionsPopover({
|
|||
labels,
|
||||
});
|
||||
|
||||
const { alertStatus, updateAlertEnabledState } = useMonitorAlertEnable();
|
||||
|
||||
const [enableLabel, setEnableLabel] = useState(
|
||||
monitor.isEnabled ? disableMonitorLabel : enableMonitorLabel
|
||||
);
|
||||
|
@ -133,6 +147,8 @@ export function ActionsPopover({
|
|||
},
|
||||
};
|
||||
|
||||
const alertLoading = alertStatus(monitor.configId) === FETCH_STATUS.LOADING;
|
||||
|
||||
let popoverItems = [
|
||||
{
|
||||
name: actionsMenuGoToMonitorName,
|
||||
|
@ -160,6 +176,27 @@ export function ActionsPopover({
|
|||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: monitor.isStatusAlertEnabled ? disableAlertLabel : enableMonitorAlertLabel,
|
||||
icon: alertLoading ? (
|
||||
<EuiLoadingSpinner size="s" />
|
||||
) : monitor.isStatusAlertEnabled ? (
|
||||
'bellSlash'
|
||||
) : (
|
||||
'bell'
|
||||
),
|
||||
onClick: () => {
|
||||
if (!alertLoading) {
|
||||
updateAlertEnabledState({
|
||||
monitor: {
|
||||
[ConfigKey.ALERT_CONFIG]: toggleStatusAlert(monitorFields?.[ConfigKey.ALERT_CONFIG]),
|
||||
},
|
||||
configId: monitor.configId,
|
||||
name: monitor.name,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
if (isInspectView) popoverItems = popoverItems.filter((i) => i !== quickInspectPopoverItem);
|
||||
|
||||
|
@ -256,6 +293,20 @@ const disableMonitorLabel = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
const disableAlertLabel = i18n.translate(
|
||||
'xpack.synthetics.overview.actions.disableLabelDisableAlert',
|
||||
{
|
||||
defaultMessage: 'Disable status alerts',
|
||||
}
|
||||
);
|
||||
|
||||
const enableMonitorAlertLabel = i18n.translate(
|
||||
'xpack.synthetics.overview.actions.enableLabelDisableAlert',
|
||||
{
|
||||
defaultMessage: 'Enable status alerts',
|
||||
}
|
||||
);
|
||||
|
||||
const enabledSuccessLabel = (name: string) =>
|
||||
i18n.translate('xpack.synthetics.overview.actions.enabledSuccessLabel', {
|
||||
defaultMessage: 'Monitor "{name}" enabled successfully',
|
||||
|
|
|
@ -29,6 +29,7 @@ describe('Overview Grid', () => {
|
|||
},
|
||||
name: `Monitor ${i}`,
|
||||
isEnabled: true,
|
||||
isStatusAlertEnabled: true,
|
||||
});
|
||||
data.push({
|
||||
id: `${i}`,
|
||||
|
@ -39,6 +40,7 @@ describe('Overview Grid', () => {
|
|||
},
|
||||
name: `Monitor ${i}`,
|
||||
isEnabled: true,
|
||||
isStatusAlertEnabled: true,
|
||||
});
|
||||
}
|
||||
return data;
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { isStatusEnabled } from '../../../../common/runtime_types/monitor_management/alert_config';
|
||||
import { ConfigKey, EncryptedSyntheticsMonitor } from '../components/monitors_page/overview/types';
|
||||
import { enableMonitorAlertAction, selectMonitorUpsertStatuses } from '../state';
|
||||
|
||||
export interface EnableStateMonitorLabels {
|
||||
failureLabel: string;
|
||||
enabledSuccessLabel: string;
|
||||
disabledSuccessLabel: string;
|
||||
}
|
||||
|
||||
export function useMonitorAlertEnable() {
|
||||
const dispatch = useDispatch();
|
||||
const upsertStatuses = useSelector(selectMonitorUpsertStatuses);
|
||||
const alertStatus = useCallback(
|
||||
(configId: string) => upsertStatuses[configId]?.alertStatus,
|
||||
[upsertStatuses]
|
||||
);
|
||||
|
||||
const updateAlertEnabledState = useCallback(
|
||||
({
|
||||
monitor,
|
||||
name,
|
||||
configId,
|
||||
}: {
|
||||
monitor: Partial<EncryptedSyntheticsMonitor>;
|
||||
configId: string;
|
||||
name: string;
|
||||
}) => {
|
||||
dispatch(
|
||||
enableMonitorAlertAction.get({
|
||||
configId,
|
||||
monitor,
|
||||
success: {
|
||||
message: isStatusEnabled(monitor[ConfigKey.ALERT_CONFIG])
|
||||
? enabledSuccessLabel(name)
|
||||
: disabledSuccessLabel(name),
|
||||
lifetimeMs: 3000,
|
||||
testAttribute: 'uptimeMonitorAlertUpdateSuccess',
|
||||
},
|
||||
error: {
|
||||
message: {
|
||||
title: enabledFailLabel(name),
|
||||
},
|
||||
lifetimeMs: 10000,
|
||||
testAttribute: 'uptimeMonitorAlertEnabledUpdateFailure',
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return { updateAlertEnabledState, alertStatus };
|
||||
}
|
||||
const enabledSuccessLabel = (name: string) =>
|
||||
i18n.translate('xpack.synthetics.overview.actions.enabledSuccessLabel.alert', {
|
||||
defaultMessage: 'Alerts are now enabled for the monitor "{name}".',
|
||||
values: { name },
|
||||
});
|
||||
|
||||
const disabledSuccessLabel = (name: string) =>
|
||||
i18n.translate('xpack.synthetics.overview.actions.disabledSuccessLabel.alert', {
|
||||
defaultMessage: 'Alerts are now disabled for the monitor "{name}".',
|
||||
values: { name },
|
||||
});
|
||||
|
||||
const enabledFailLabel = (name: string) =>
|
||||
i18n.translate('xpack.synthetics.overview.actions.enabledFailLabel.alert', {
|
||||
defaultMessage: 'Unable to enable status alerts for monitor "{name}".',
|
||||
values: { name },
|
||||
});
|
|
@ -42,7 +42,7 @@ export function useMonitorEnableHandler({
|
|||
(enabled: boolean) => {
|
||||
dispatch(
|
||||
fetchUpsertMonitorAction({
|
||||
id: configId,
|
||||
configId,
|
||||
monitor: { [ConfigKey.ENABLED]: enabled },
|
||||
success: {
|
||||
message: enabled ? labels.enabledSuccessLabel : labels.disabledSuccessLabel,
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { CoreStart } from '@kbn/core/public';
|
||||
import { ObservabilityRuleTypeModel } from '@kbn/observability-plugin/public';
|
||||
import { ClientPluginsStart } from '../../../../plugin';
|
||||
import { initMonitorStatusAlertType } from './monitor_status';
|
||||
|
||||
export type AlertTypeInitializer<TAlertTypeModel = ObservabilityRuleTypeModel> = (dependencies: {
|
||||
core: CoreStart;
|
||||
plugins: ClientPluginsStart;
|
||||
}) => TAlertTypeModel;
|
||||
|
||||
export const syntheticsAlertTypeInitializers: AlertTypeInitializer[] = [initMonitorStatusAlertType];
|
|
@ -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 React from 'react';
|
||||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { kibanaService } from '../../../../../utils/kibana_service';
|
||||
import { ClientPluginsStart } from '../../../../../plugin';
|
||||
import { store } from '../../../state';
|
||||
import { StatusRuleParams } from '../../../../../../common/rules/status_rule';
|
||||
|
||||
interface Props {
|
||||
core: CoreStart;
|
||||
plugins: ClientPluginsStart;
|
||||
params: RuleTypeParamsExpressionProps<StatusRuleParams>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function MonitorStatusAlert({ core, plugins, params }: Props) {
|
||||
kibanaService.core = core;
|
||||
return (
|
||||
<ReduxProvider store={store}>
|
||||
<KibanaContextProvider services={{ ...core, ...plugins }}>
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.alertRule.monitorStatus.description"
|
||||
defaultMessage="Manage synthetics monitor status rule actions."
|
||||
/>
|
||||
</EuiText>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
</KibanaContextProvider>
|
||||
</ReduxProvider>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
|
||||
import {
|
||||
ALERT_END,
|
||||
ALERT_START,
|
||||
ALERT_STATUS,
|
||||
ALERT_STATUS_ACTIVE,
|
||||
ALERT_REASON,
|
||||
} from '@kbn/rule-data-utils';
|
||||
|
||||
import { ObservabilityRuleTypeModel } from '@kbn/observability-plugin/public';
|
||||
import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { getSyntheticsMonitorRouteFromMonitorId } from '../../../../../common/utils/get_synthetics_monitor_url';
|
||||
import { SyntheticsMonitorStatusTranslations } from '../../../../../common/rules/synthetics/translations';
|
||||
import { StatusRuleParams } from '../../../../../common/rules/status_rule';
|
||||
import { SYNTHETICS_ALERT_RULE_TYPES } from '../../../../../common/constants/synthetics_alerts';
|
||||
import { AlertTypeInitializer } from '.';
|
||||
const { defaultActionMessage, defaultRecoveryMessage, description } =
|
||||
SyntheticsMonitorStatusTranslations;
|
||||
|
||||
const MonitorStatusAlert = React.lazy(() => import('./lazy_wrapper/monitor_status'));
|
||||
|
||||
export const initMonitorStatusAlertType: AlertTypeInitializer = ({
|
||||
core,
|
||||
plugins,
|
||||
}): ObservabilityRuleTypeModel => ({
|
||||
id: SYNTHETICS_ALERT_RULE_TYPES.MONITOR_STATUS,
|
||||
description,
|
||||
iconClass: 'uptimeApp',
|
||||
documentationUrl(docLinks) {
|
||||
return `${docLinks.links.observability.monitorStatus}`;
|
||||
},
|
||||
ruleParamsExpression: (paramProps: RuleTypeParamsExpressionProps<StatusRuleParams>) => (
|
||||
<MonitorStatusAlert core={core} plugins={plugins} params={paramProps} />
|
||||
),
|
||||
validate: (ruleParams: StatusRuleParams) => {
|
||||
return { errors: {} };
|
||||
},
|
||||
defaultActionMessage,
|
||||
defaultRecoveryMessage,
|
||||
requiresAppContext: true,
|
||||
format: ({ fields }) => ({
|
||||
reason: fields[ALERT_REASON] || '',
|
||||
link: getSyntheticsMonitorRouteFromMonitorId({
|
||||
configId: fields.configId,
|
||||
dateRangeEnd: fields[ALERT_STATUS] === ALERT_STATUS_ACTIVE ? 'now' : fields[ALERT_END]!,
|
||||
dateRangeStart: moment(new Date(fields[ALERT_START]!)).subtract('5', 'm').toISOString(),
|
||||
locationId: fields['location.id'],
|
||||
}),
|
||||
}),
|
||||
});
|
|
@ -159,6 +159,7 @@ const getRoutes = (
|
|||
/>
|
||||
),
|
||||
isSelected: true,
|
||||
'data-test-subj': 'syntheticsMonitorOverviewTab',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
|
@ -168,6 +169,7 @@ const getRoutes = (
|
|||
/>
|
||||
),
|
||||
href: `${syntheticsPath}${MONITORS_ROUTE}`,
|
||||
'data-test-subj': 'syntheticsMonitorManagementTab',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -192,6 +194,7 @@ const getRoutes = (
|
|||
/>
|
||||
),
|
||||
href: `${syntheticsPath}${OVERVIEW_ROUTE}`,
|
||||
'data-test-subj': 'syntheticsMonitorOverviewTab',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
|
@ -201,6 +204,7 @@ const getRoutes = (
|
|||
/>
|
||||
),
|
||||
isSelected: true,
|
||||
'data-test-subj': 'syntheticsMonitorManagementTab',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -364,6 +368,7 @@ const getMonitorSummaryHeader = (
|
|||
}),
|
||||
isSelected: selectedTab === 'overview',
|
||||
href: `${syntheticsPath}${MONITOR_ROUTE.replace(':monitorId?', monitorId)}${search}`,
|
||||
'data-test-subj': 'syntheticsMonitorOverviewTab',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.synthetics.monitorHistoryTab.title', {
|
||||
|
@ -371,6 +376,7 @@ const getMonitorSummaryHeader = (
|
|||
}),
|
||||
isSelected: selectedTab === 'history',
|
||||
href: `${syntheticsPath}${MONITOR_HISTORY_ROUTE.replace(':monitorId', monitorId)}${search}`,
|
||||
'data-test-subj': 'syntheticsMonitorHistoryTab',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.synthetics.monitorErrorsTab.title', {
|
||||
|
@ -379,6 +385,7 @@ const getMonitorSummaryHeader = (
|
|||
prepend: <EuiIcon type="alert" color="danger" />,
|
||||
isSelected: selectedTab === 'errors',
|
||||
href: `${syntheticsPath}${MONITOR_ERRORS_ROUTE.replace(':monitorId', monitorId)}${search}`,
|
||||
'data-test-subj': 'syntheticsMonitorErrorsTab',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -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 { Rule } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { createAsyncAction } from '../utils/actions';
|
||||
|
||||
export const enableDefaultAlertingAction = createAsyncAction<void, Rule>(
|
||||
'enableDefaultAlertingAction'
|
||||
);
|
||||
|
||||
export const updateDefaultAlertingAction = createAsyncAction<void, Rule>(
|
||||
'updateDefaultAlertingAction'
|
||||
);
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { Rule } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { SYNTHETICS_API_URLS } from '../../../../../common/constants';
|
||||
import { apiService } from '../../../../utils/api_service';
|
||||
|
||||
export async function enableDefaultAlertingAPI(): Promise<Rule> {
|
||||
return apiService.post(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING);
|
||||
}
|
||||
|
||||
export async function updateDefaultAlertingAPI(): Promise<Rule> {
|
||||
return apiService.put(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING);
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { takeLeading } from 'redux-saga/effects';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { enableDefaultAlertingAction, updateDefaultAlertingAction } from './actions';
|
||||
import { fetchEffectFactory } from '../utils/fetch_effect';
|
||||
import { enableDefaultAlertingAPI, updateDefaultAlertingAPI } from './api';
|
||||
|
||||
export function* enableDefaultAlertingEffect() {
|
||||
yield takeLeading(
|
||||
enableDefaultAlertingAction.get,
|
||||
fetchEffectFactory(
|
||||
enableDefaultAlertingAPI,
|
||||
enableDefaultAlertingAction.success,
|
||||
enableDefaultAlertingAction.fail,
|
||||
successMessage,
|
||||
failureMessage
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function* updateDefaultAlertingEffect() {
|
||||
yield takeLeading(
|
||||
updateDefaultAlertingAction.get,
|
||||
fetchEffectFactory(
|
||||
updateDefaultAlertingAPI,
|
||||
updateDefaultAlertingAction.success,
|
||||
updateDefaultAlertingAction.fail,
|
||||
successMessage,
|
||||
failureMessage
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const successMessage = i18n.translate('xpack.synthetics.settings.enableAlerting', {
|
||||
defaultMessage:
|
||||
'Monitor status rule type successfully updated. Next rule alerts will take the changes into account.',
|
||||
});
|
||||
|
||||
const failureMessage = i18n.translate('xpack.synthetics.settings.enabledAlert.fail', {
|
||||
defaultMessage: 'Failed to update monitor status rule type.',
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
import { IHttpSerializedFetchError } from '..';
|
||||
import { enableDefaultAlertingAction, updateDefaultAlertingAction } from './actions';
|
||||
|
||||
export interface DefaultAlertingState {
|
||||
success: boolean | null;
|
||||
loading: boolean;
|
||||
error: IHttpSerializedFetchError | null;
|
||||
}
|
||||
|
||||
const initialSettingState: DefaultAlertingState = {
|
||||
success: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
export const defaultAlertingReducer = createReducer(initialSettingState, (builder) => {
|
||||
builder
|
||||
.addCase(enableDefaultAlertingAction.get, (state) => {
|
||||
state.loading = true;
|
||||
})
|
||||
.addCase(enableDefaultAlertingAction.success, (state, action) => {
|
||||
state.success = Boolean(action.payload);
|
||||
state.loading = false;
|
||||
})
|
||||
.addCase(enableDefaultAlertingAction.fail, (state, action) => {
|
||||
state.error = action.payload;
|
||||
state.loading = false;
|
||||
state.success = false;
|
||||
})
|
||||
.addCase(updateDefaultAlertingAction.get, (state) => {
|
||||
state.loading = true;
|
||||
})
|
||||
.addCase(updateDefaultAlertingAction.success, (state, action) => {
|
||||
state.success = Boolean(action.payload);
|
||||
state.loading = false;
|
||||
})
|
||||
.addCase(updateDefaultAlertingAction.fail, (state, action) => {
|
||||
state.error = action.payload;
|
||||
state.loading = false;
|
||||
state.success = false;
|
||||
});
|
||||
});
|
||||
|
||||
export * from './actions';
|
||||
export * from './effects';
|
|
@ -7,9 +7,11 @@
|
|||
|
||||
import { ErrorToastOptions } from '@kbn/core-notifications-browser';
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { UpsertMonitorResponse } from '..';
|
||||
import {
|
||||
EncryptedSyntheticsMonitor,
|
||||
MonitorManagementListResult,
|
||||
SyntheticsMonitor,
|
||||
} from '../../../../../common/runtime_types';
|
||||
import { createAsyncAction } from '../utils/actions';
|
||||
import { IHttpSerializedFetchError } from '../utils/http_error';
|
||||
|
@ -28,8 +30,8 @@ interface ToastParams<MessageType> {
|
|||
}
|
||||
|
||||
export interface UpsertMonitorRequest {
|
||||
id: string;
|
||||
monitor: Partial<EncryptedSyntheticsMonitor>;
|
||||
configId: string;
|
||||
monitor: Partial<SyntheticsMonitor> | Partial<EncryptedSyntheticsMonitor>;
|
||||
success: ToastParams<string>;
|
||||
error: ToastParams<ErrorToastOptions>;
|
||||
/**
|
||||
|
@ -39,13 +41,24 @@ export interface UpsertMonitorRequest {
|
|||
shouldQuietFetchAfterSuccess?: boolean;
|
||||
}
|
||||
|
||||
interface UpsertMonitorError {
|
||||
configId: string;
|
||||
error: IHttpSerializedFetchError;
|
||||
}
|
||||
|
||||
export const fetchUpsertMonitorAction = createAction<UpsertMonitorRequest>('fetchUpsertMonitor');
|
||||
export const fetchUpsertSuccessAction = createAction<{
|
||||
id: string;
|
||||
attributes: { enabled: boolean };
|
||||
}>('fetchUpsertMonitorSuccess');
|
||||
export const fetchUpsertFailureAction = createAction<{
|
||||
id: string;
|
||||
error: IHttpSerializedFetchError;
|
||||
}>('fetchUpsertMonitorFailure');
|
||||
export const fetchUpsertFailureAction = createAction<UpsertMonitorError>(
|
||||
'fetchUpsertMonitorFailure'
|
||||
);
|
||||
|
||||
export const enableMonitorAlertAction = createAsyncAction<
|
||||
UpsertMonitorRequest,
|
||||
UpsertMonitorResponse,
|
||||
UpsertMonitorError
|
||||
>('enableMonitorAlertAction');
|
||||
|
||||
export const clearMonitorUpsertStatus = createAction<string>('clearMonitorUpsertStatus');
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SavedObject } from '@kbn/core-saved-objects-common';
|
||||
import { UpsertMonitorRequest } from '..';
|
||||
import { API_URLS } from '../../../../../common/constants';
|
||||
import {
|
||||
EncryptedSyntheticsMonitor,
|
||||
|
@ -46,19 +48,20 @@ export const fetchMonitorManagementList = async (
|
|||
);
|
||||
};
|
||||
|
||||
export const fetchDeleteMonitor = async ({ id }: { id: string }): Promise<void> => {
|
||||
return await apiService.delete(`${API_URLS.SYNTHETICS_MONITORS}/${id}`);
|
||||
export const fetchDeleteMonitor = async ({ configId }: { configId: string }): Promise<void> => {
|
||||
return await apiService.delete(`${API_URLS.SYNTHETICS_MONITORS}/${configId}`);
|
||||
};
|
||||
|
||||
export type UpsertMonitorResponse =
|
||||
| { attributes: { errors: ServiceLocationErrors }; id: string }
|
||||
| SavedObject<SyntheticsMonitor>;
|
||||
|
||||
export const fetchUpsertMonitor = async ({
|
||||
monitor,
|
||||
id,
|
||||
}: {
|
||||
monitor: Partial<SyntheticsMonitor> | Partial<EncryptedSyntheticsMonitor>;
|
||||
id?: string;
|
||||
}): Promise<{ attributes: { errors: ServiceLocationErrors } } | SyntheticsMonitor> => {
|
||||
if (id) {
|
||||
return await apiService.put(`${API_URLS.SYNTHETICS_MONITORS}/${id}`, monitor);
|
||||
configId,
|
||||
}: UpsertMonitorRequest): Promise<UpsertMonitorResponse> => {
|
||||
if (configId) {
|
||||
return await apiService.put(`${API_URLS.SYNTHETICS_MONITORS}/${configId}`, monitor);
|
||||
} else {
|
||||
return await apiService.post(API_URLS.SYNTHETICS_MONITORS, monitor);
|
||||
}
|
||||
|
|
|
@ -7,14 +7,17 @@
|
|||
|
||||
import { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { call, put, takeEvery, takeLeading, select } from 'redux-saga/effects';
|
||||
import { SavedObject } from '@kbn/core-saved-objects-common';
|
||||
import { enableDefaultAlertingAction } from '../alert_rules';
|
||||
import { kibanaService } from '../../../../utils/kibana_service';
|
||||
import { MonitorOverviewPageState } from '../overview';
|
||||
import { quietFetchOverviewAction } from '../overview/actions';
|
||||
import { selectOverviewState } from '../overview/selectors';
|
||||
import { fetchEffectFactory } from '../utils/fetch_effect';
|
||||
import { fetchEffectFactory, sendErrorToast, sendSuccessToast } from '../utils/fetch_effect';
|
||||
import { serializeHttpFetchError } from '../utils/http_error';
|
||||
import {
|
||||
clearMonitorUpsertStatus,
|
||||
enableMonitorAlertAction,
|
||||
fetchMonitorListAction,
|
||||
fetchUpsertFailureAction,
|
||||
fetchUpsertMonitorAction,
|
||||
|
@ -23,6 +26,7 @@ import {
|
|||
} from './actions';
|
||||
import { fetchMonitorManagementList, fetchUpsertMonitor } from './api';
|
||||
import { toastTitle } from './toast_title';
|
||||
import { ConfigKey, SyntheticsMonitor } from '../../../../../common/runtime_types';
|
||||
|
||||
export function* fetchMonitorListEffect() {
|
||||
yield takeLeading(
|
||||
|
@ -35,6 +39,33 @@ export function* fetchMonitorListEffect() {
|
|||
);
|
||||
}
|
||||
|
||||
export function* enableMonitorAlertEffect() {
|
||||
yield takeEvery(
|
||||
enableMonitorAlertAction.get,
|
||||
function* (action: PayloadAction<UpsertMonitorRequest>): Generator {
|
||||
try {
|
||||
const response = yield call(fetchUpsertMonitor, action.payload);
|
||||
yield put(enableMonitorAlertAction.success(response as SavedObject<SyntheticsMonitor>));
|
||||
sendSuccessToast(action.payload.success);
|
||||
if (
|
||||
(response as SavedObject<SyntheticsMonitor>).attributes[ConfigKey.ALERT_CONFIG]?.status
|
||||
?.enabled
|
||||
) {
|
||||
yield put(enableDefaultAlertingAction.get());
|
||||
}
|
||||
} catch (error) {
|
||||
sendErrorToast(action.payload.error, error);
|
||||
yield put(
|
||||
enableMonitorAlertAction.fail({
|
||||
configId: action.payload.configId,
|
||||
error: serializeHttpFetchError(error),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function* upsertMonitorEffect() {
|
||||
yield takeEvery(
|
||||
fetchUpsertMonitorAction,
|
||||
|
@ -57,7 +88,10 @@ export function* upsertMonitorEffect() {
|
|||
toastLifeTimeMs: action.payload.error.lifetimeMs,
|
||||
});
|
||||
yield put(
|
||||
fetchUpsertFailureAction({ id: action.payload.id, error: serializeHttpFetchError(error) })
|
||||
fetchUpsertFailureAction({
|
||||
configId: action.payload.configId,
|
||||
error: serializeHttpFetchError(error),
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
if (action.payload.shouldQuietFetchAfterSuccess !== false) {
|
||||
|
@ -68,7 +102,7 @@ export function* upsertMonitorEffect() {
|
|||
);
|
||||
}
|
||||
}
|
||||
yield put(clearMonitorUpsertStatus(action.payload.id));
|
||||
yield put(clearMonitorUpsertStatus(action.payload.configId));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -9,13 +9,19 @@ import { isEqual } from 'lodash';
|
|||
import { createReducer } from '@reduxjs/toolkit';
|
||||
import { FETCH_STATUS } from '@kbn/observability-plugin/public';
|
||||
|
||||
import { ConfigKey, MonitorManagementListResult } from '../../../../../common/runtime_types';
|
||||
import { SavedObject } from '@kbn/core-saved-objects-common';
|
||||
import {
|
||||
ConfigKey,
|
||||
MonitorManagementListResult,
|
||||
SyntheticsMonitor,
|
||||
} from '../../../../../common/runtime_types';
|
||||
|
||||
import { IHttpSerializedFetchError } from '../utils/http_error';
|
||||
|
||||
import { MonitorListPageState } from './models';
|
||||
import {
|
||||
clearMonitorUpsertStatus,
|
||||
enableMonitorAlertAction,
|
||||
fetchMonitorListAction,
|
||||
fetchUpsertFailureAction,
|
||||
fetchUpsertMonitorAction,
|
||||
|
@ -24,7 +30,10 @@ import {
|
|||
|
||||
export interface MonitorListState {
|
||||
data: MonitorManagementListResult;
|
||||
monitorUpsertStatuses: Record<string, { status: FETCH_STATUS; enabled?: boolean }>;
|
||||
monitorUpsertStatuses: Record<
|
||||
string,
|
||||
{ status: FETCH_STATUS; enabled?: boolean; alertStatus?: FETCH_STATUS }
|
||||
>;
|
||||
pageState: MonitorListPageState;
|
||||
loading: boolean;
|
||||
loaded: boolean;
|
||||
|
@ -64,7 +73,7 @@ export const monitorListReducer = createReducer(initialState, (builder) => {
|
|||
state.error = action.payload;
|
||||
})
|
||||
.addCase(fetchUpsertMonitorAction, (state, action) => {
|
||||
state.monitorUpsertStatuses[action.payload.id] = {
|
||||
state.monitorUpsertStatuses[action.payload.configId] = {
|
||||
status: FETCH_STATUS.LOADING,
|
||||
};
|
||||
})
|
||||
|
@ -75,7 +84,33 @@ export const monitorListReducer = createReducer(initialState, (builder) => {
|
|||
};
|
||||
})
|
||||
.addCase(fetchUpsertFailureAction, (state, action) => {
|
||||
state.monitorUpsertStatuses[action.payload.id] = { status: FETCH_STATUS.FAILURE };
|
||||
state.monitorUpsertStatuses[action.payload.configId] = { status: FETCH_STATUS.FAILURE };
|
||||
})
|
||||
.addCase(enableMonitorAlertAction.get, (state, action) => {
|
||||
state.monitorUpsertStatuses[action.payload.configId] = {
|
||||
...state.monitorUpsertStatuses[action.payload.configId],
|
||||
alertStatus: FETCH_STATUS.LOADING,
|
||||
};
|
||||
})
|
||||
.addCase(enableMonitorAlertAction.success, (state, action) => {
|
||||
state.monitorUpsertStatuses[action.payload.id] = {
|
||||
...state.monitorUpsertStatuses[action.payload.id],
|
||||
alertStatus: FETCH_STATUS.SUCCESS,
|
||||
};
|
||||
if ('updated_at' in action.payload) {
|
||||
state.data.monitors = state.data.monitors.map((monitor) => {
|
||||
if (monitor.id === action.payload.id) {
|
||||
return action.payload as SavedObject<SyntheticsMonitor>;
|
||||
}
|
||||
return monitor;
|
||||
});
|
||||
}
|
||||
})
|
||||
.addCase(enableMonitorAlertAction.fail, (state, action) => {
|
||||
state.monitorUpsertStatuses[action.payload.configId] = {
|
||||
...state.monitorUpsertStatuses[action.payload.configId],
|
||||
alertStatus: FETCH_STATUS.FAILURE,
|
||||
};
|
||||
})
|
||||
.addCase(clearMonitorUpsertStatus, (state, action) => {
|
||||
if (state.monitorUpsertStatuses[action.payload]) {
|
||||
|
|
|
@ -14,7 +14,7 @@ export const selectMonitorListState = (state: SyntheticsAppState) => state.monit
|
|||
export const selectEncryptedSyntheticsSavedMonitors = createSelector(
|
||||
selectMonitorListState,
|
||||
(state) =>
|
||||
state.data.monitors.map((monitor) => ({
|
||||
state?.data.monitors.map((monitor) => ({
|
||||
...monitor.attributes,
|
||||
id: monitor.attributes[ConfigKey.MONITOR_QUERY_ID],
|
||||
updated_at: monitor.updated_at,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
|
||||
import { isStatusEnabled } from '../../../../../common/runtime_types/monitor_management/alert_config';
|
||||
import { MonitorOverviewState } from './models';
|
||||
|
||||
import {
|
||||
|
@ -17,6 +17,8 @@ import {
|
|||
setFlyoutConfig,
|
||||
setOverviewPageStateAction,
|
||||
} from './actions';
|
||||
import { enableMonitorAlertAction } from '../monitor_list/actions';
|
||||
import { ConfigKey } from '../../components/monitor_add_edit/types';
|
||||
|
||||
const initialState: MonitorOverviewState = {
|
||||
data: {
|
||||
|
@ -77,6 +79,24 @@ export const monitorOverviewReducer = createReducer(initialState, (builder) => {
|
|||
allConfigs: { ...action.payload.upConfigs, ...action.payload.downConfigs },
|
||||
};
|
||||
})
|
||||
.addCase(enableMonitorAlertAction.success, (state, action) => {
|
||||
const attrs = action.payload.attributes;
|
||||
if (!('errors' in attrs)) {
|
||||
const isStatusAlertEnabled = isStatusEnabled(attrs[ConfigKey.ALERT_CONFIG]);
|
||||
state.data.monitors = state.data.monitors.map((monitor) => {
|
||||
if (
|
||||
monitor.id === action.payload.id ||
|
||||
attrs[ConfigKey.MONITOR_QUERY_ID] === monitor.id
|
||||
) {
|
||||
return {
|
||||
...monitor,
|
||||
isStatusAlertEnabled,
|
||||
};
|
||||
}
|
||||
return monitor;
|
||||
});
|
||||
}
|
||||
})
|
||||
.addCase(fetchOverviewStatusAction.fail, (state, action) => {
|
||||
state.statusError = action.payload;
|
||||
})
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { all, fork } from 'redux-saga/effects';
|
||||
import { enableDefaultAlertingEffect, updateDefaultAlertingEffect } from './alert_rules/effects';
|
||||
import {
|
||||
fetchAlertConnectorsEffect,
|
||||
fetchDynamicSettingsEffect,
|
||||
|
@ -17,7 +18,11 @@ import { fetchNetworkEventsEffect } from './network_events/effects';
|
|||
import { fetchSyntheticsMonitorEffect } from './monitor_details';
|
||||
import { fetchIndexStatusEffect } from './index_status';
|
||||
import { fetchSyntheticsEnablementEffect } from './synthetics_enablement';
|
||||
import { fetchMonitorListEffect, upsertMonitorEffect } from './monitor_list';
|
||||
import {
|
||||
enableMonitorAlertEffect,
|
||||
fetchMonitorListEffect,
|
||||
upsertMonitorEffect,
|
||||
} from './monitor_list';
|
||||
import { fetchMonitorOverviewEffect, fetchOverviewStatusEffect } from './overview';
|
||||
import { fetchServiceLocationsEffect } from './service_locations';
|
||||
import { browserJourneyEffects } from './browser_journey';
|
||||
|
@ -42,5 +47,8 @@ export const rootEffect = function* root(): Generator {
|
|||
fork(fetchAgentPoliciesEffect),
|
||||
fork(fetchAlertConnectorsEffect),
|
||||
fork(syncGlobalParamsEffect),
|
||||
fork(enableDefaultAlertingEffect),
|
||||
fork(enableMonitorAlertEffect),
|
||||
fork(updateDefaultAlertingEffect),
|
||||
]);
|
||||
};
|
||||
|
|
|
@ -7,8 +7,10 @@
|
|||
|
||||
import { combineReducers } from '@reduxjs/toolkit';
|
||||
|
||||
import { dynamicSettingsReducer, DynamicSettingsState } from './settings';
|
||||
import { settingsReducer, SettingsState } from './settings';
|
||||
import { browserJourneyReducer } from './browser_journey';
|
||||
import { defaultAlertingReducer, DefaultAlertingState } from './alert_rules';
|
||||
import { dynamicSettingsReducer, DynamicSettingsState, settingsReducer } from './settings';
|
||||
import { SettingsState } from './settings';
|
||||
import { agentPoliciesReducer, AgentPoliciesState } from './private_locations';
|
||||
import { networkEventsReducer, NetworkEventsState } from './network_events';
|
||||
import { monitorDetailsReducer, MonitorDetailsState } from './monitor_details';
|
||||
|
@ -19,8 +21,7 @@ import { monitorListReducer, MonitorListState } from './monitor_list';
|
|||
import { serviceLocationsReducer, ServiceLocationsState } from './service_locations';
|
||||
import { monitorOverviewReducer, MonitorOverviewState } from './overview';
|
||||
import { BrowserJourneyState } from './browser_journey/models';
|
||||
import { browserJourneyReducer } from './browser_journey';
|
||||
import { PingStatusState, pingStatusReducer } from './ping_status';
|
||||
import { pingStatusReducer, PingStatusState } from './ping_status';
|
||||
|
||||
export interface SyntheticsAppState {
|
||||
ui: UiState;
|
||||
|
@ -34,22 +35,24 @@ export interface SyntheticsAppState {
|
|||
monitorDetails: MonitorDetailsState;
|
||||
browserJourney: BrowserJourneyState;
|
||||
serviceLocations: ServiceLocationsState;
|
||||
syntheticsEnablement: SyntheticsEnablementState;
|
||||
dynamicSettings: DynamicSettingsState;
|
||||
defaultAlerting: DefaultAlertingState;
|
||||
syntheticsEnablement: SyntheticsEnablementState;
|
||||
}
|
||||
|
||||
export const rootReducer = combineReducers<SyntheticsAppState>({
|
||||
ui: uiReducer,
|
||||
indexStatus: indexStatusReducer,
|
||||
syntheticsEnablement: syntheticsEnablementReducer,
|
||||
monitorList: monitorListReducer,
|
||||
serviceLocations: serviceLocationsReducer,
|
||||
monitorDetails: monitorDetailsReducer,
|
||||
overview: monitorOverviewReducer,
|
||||
browserJourney: browserJourneyReducer,
|
||||
networkEvents: networkEventsReducer,
|
||||
pingStatus: pingStatusReducer,
|
||||
agentPolicies: agentPoliciesReducer,
|
||||
dynamicSettings: dynamicSettingsReducer,
|
||||
settings: settingsReducer,
|
||||
pingStatus: pingStatusReducer,
|
||||
monitorList: monitorListReducer,
|
||||
indexStatus: indexStatusReducer,
|
||||
overview: monitorOverviewReducer,
|
||||
networkEvents: networkEventsReducer,
|
||||
agentPolicies: agentPoliciesReducer,
|
||||
monitorDetails: monitorDetailsReducer,
|
||||
browserJourney: browserJourneyReducer,
|
||||
defaultAlerting: defaultAlertingReducer,
|
||||
dynamicSettings: dynamicSettingsReducer,
|
||||
serviceLocations: serviceLocationsReducer,
|
||||
syntheticsEnablement: syntheticsEnablementReducer,
|
||||
});
|
||||
|
|
|
@ -17,8 +17,7 @@ import {
|
|||
DynamicSettingsSaveType,
|
||||
DynamicSettingsType,
|
||||
} from '../../../../../common/runtime_types';
|
||||
import { API_URLS } from '../../../../../common/constants';
|
||||
import { SYNTHETICS_API_URLS } from '../../../../../common/constants';
|
||||
import { API_URLS, SYNTHETICS_API_URLS } from '../../../../../common/constants';
|
||||
|
||||
const apiPath = API_URLS.DYNAMIC_SETTINGS;
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { takeLeading, put, call, takeLatest } from 'redux-saga/effects';
|
||||
import { Action } from 'redux-actions';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { updateDefaultAlertingAction } from '../alert_rules';
|
||||
import { DynamicSettings } from '../../../../../common/runtime_types';
|
||||
import { kibanaService } from '../../../../utils/kibana_service';
|
||||
import { getConnectorsAction, setDynamicSettingsAction, getDynamicSettingsAction } from './actions';
|
||||
|
@ -61,6 +62,7 @@ export function* setDynamicSettingsEffect() {
|
|||
function* (action: Action<DynamicSettings>) {
|
||||
try {
|
||||
yield call(setDynamicSettings, { settings: action.payload });
|
||||
yield put(updateDefaultAlertingAction.get());
|
||||
yield put(setDynamicSettingsAction.success(action.payload));
|
||||
kibanaService.core.notifications.toasts.addSuccess(
|
||||
i18n.translate('xpack.synthetics.settings.saveSuccess', {
|
||||
|
|
|
@ -8,11 +8,15 @@
|
|||
import { createAction } from '@reduxjs/toolkit';
|
||||
import type { IHttpSerializedFetchError } from './http_error';
|
||||
|
||||
export function createAsyncAction<Payload, SuccessPayload>(actionStr: string) {
|
||||
export function createAsyncAction<
|
||||
Payload,
|
||||
SuccessPayload,
|
||||
FailurePayload = IHttpSerializedFetchError
|
||||
>(actionStr: string) {
|
||||
return {
|
||||
get: createAction(actionStr, (payload: Payload) => prepareForTimestamp(payload)),
|
||||
success: createAction<SuccessPayload>(`${actionStr}_SUCCESS`),
|
||||
fail: createAction<IHttpSerializedFetchError>(`${actionStr}_FAIL`),
|
||||
fail: createAction<FailurePayload>(`${actionStr}_FAIL`),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -8,9 +8,39 @@
|
|||
import { call, put } from 'redux-saga/effects';
|
||||
import { PayloadAction } from '@reduxjs/toolkit';
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import { ErrorToastOptions } from '@kbn/core-notifications-browser';
|
||||
import { toastTitle } from '../monitor_list/toast_title';
|
||||
import { kibanaService } from '../../../../utils/kibana_service';
|
||||
import { IHttpSerializedFetchError, serializeHttpFetchError } from './http_error';
|
||||
|
||||
interface ToastParams<MessageType> {
|
||||
message: MessageType;
|
||||
lifetimeMs?: number;
|
||||
testAttribute?: string;
|
||||
}
|
||||
|
||||
interface ActionMessages {
|
||||
success: ToastParams<string>;
|
||||
error: ToastParams<ErrorToastOptions>;
|
||||
}
|
||||
|
||||
export const sendSuccessToast = (payload: ToastParams<string>) => {
|
||||
kibanaService.toasts.addSuccess({
|
||||
title: toastTitle({
|
||||
title: payload.message,
|
||||
testAttribute: payload.testAttribute,
|
||||
}),
|
||||
toastLifeTimeMs: payload.lifetimeMs,
|
||||
});
|
||||
};
|
||||
|
||||
export const sendErrorToast = (payload: ToastParams<ErrorToastOptions>, error: Error) => {
|
||||
kibanaService.toasts.addError(error, {
|
||||
...payload.message,
|
||||
toastLifeTimeMs: payload.lifetimeMs,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Factory function for a fetch effect. It expects three action creators,
|
||||
* one to call for a fetch, one to call for success, and one to handle failures.
|
||||
|
@ -48,6 +78,17 @@ export function fetchEffectFactory<T, R, S, F>(
|
|||
}
|
||||
} else {
|
||||
yield put(success(response as R));
|
||||
const successMessage = (action.payload as unknown as ActionMessages)?.success;
|
||||
if (successMessage?.message) {
|
||||
kibanaService.toasts.addSuccess({
|
||||
title: toastTitle({
|
||||
title: successMessage.message,
|
||||
testAttribute: successMessage.testAttribute,
|
||||
}),
|
||||
toastLifeTimeMs: successMessage.lifetimeMs,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof onSuccess === 'function') {
|
||||
onSuccess?.(response as R);
|
||||
} else if (typeof onSuccess === 'string') {
|
||||
|
@ -57,6 +98,15 @@ export function fetchEffectFactory<T, R, S, F>(
|
|||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
const errorMessage = (action.payload as unknown as ActionMessages)?.error;
|
||||
|
||||
if (errorMessage?.message) {
|
||||
kibanaService.toasts.addError(error, {
|
||||
...errorMessage.message,
|
||||
toastLifeTimeMs: errorMessage.lifetimeMs,
|
||||
});
|
||||
}
|
||||
|
||||
yield put(fail(serializeHttpFetchError(error)));
|
||||
if (typeof onFailure === 'function') {
|
||||
onFailure?.(error);
|
||||
|
|
|
@ -31,7 +31,6 @@ import { store, storage, setBasePath } from './state';
|
|||
import { kibanaService } from '../../utils/kibana_service';
|
||||
import { ActionMenu } from './components/common/header/action_menu';
|
||||
|
||||
// added a comment to trigger test
|
||||
const Application = (props: SyntheticsAppProps) => {
|
||||
const {
|
||||
basePath,
|
||||
|
|
|
@ -124,6 +124,11 @@ export const mockState: SyntheticsAppState = {
|
|||
dynamicSettings: {
|
||||
loading: false,
|
||||
},
|
||||
defaultAlerting: {
|
||||
loading: false,
|
||||
error: null,
|
||||
success: null,
|
||||
},
|
||||
};
|
||||
|
||||
function getBrowserJourneyMockSlice() {
|
||||
|
|
|
@ -65,6 +65,7 @@ export const commonNormalizers: CommonNormalizerMap = {
|
|||
[ConfigKey.NAME]: (fields) => fields?.[ConfigKey.NAME]?.value ?? '',
|
||||
[ConfigKey.LOCATIONS]: getCommonNormalizer(ConfigKey.LOCATIONS),
|
||||
[ConfigKey.ENABLED]: getCommonNormalizer(ConfigKey.ENABLED),
|
||||
[ConfigKey.ALERT_CONFIG]: getCommonNormalizer(ConfigKey.ENABLED),
|
||||
[ConfigKey.MONITOR_TYPE]: getCommonNormalizer(ConfigKey.MONITOR_TYPE),
|
||||
[ConfigKey.LOCATIONS]: getCommonNormalizer(ConfigKey.LOCATIONS),
|
||||
[ConfigKey.SCHEDULE]: (fields) => {
|
||||
|
|
|
@ -10,7 +10,7 @@ import React, { useCallback, useContext, useState } from 'react';
|
|||
import { EuiButton, EuiContextMenu, EuiIcon, EuiPopover } from '@elastic/eui';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { CLIENT_ALERT_TYPES } from '../../../../../common/constants/alerts';
|
||||
import { CLIENT_ALERT_TYPES } from '../../../../../common/constants/uptime_alerts';
|
||||
import {
|
||||
canDeleteMLJobSelector,
|
||||
hasMLJobSelector,
|
||||
|
|
|
@ -31,7 +31,7 @@ import { useGetUrlParams } from '../../../hooks';
|
|||
import { getDynamicSettings } from '../../../state/actions/dynamic_settings';
|
||||
import { useMonitorId } from '../../../hooks';
|
||||
import { kibanaService } from '../../../state/kibana_service';
|
||||
import { CLIENT_ALERT_TYPES } from '../../../../../common/constants/alerts';
|
||||
import { CLIENT_ALERT_TYPES } from '../../../../../common/constants/uptime_alerts';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
|
|
|
@ -13,9 +13,7 @@ import { selectAlertFlyoutVisibility, selectAlertFlyoutType } from '../../../../
|
|||
|
||||
export const UptimeAlertsFlyoutWrapper: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const setAddFlyoutVisibility = (value: React.SetStateAction<boolean>) =>
|
||||
// @ts-ignore the value here is a boolean, and it works with the action creator function
|
||||
dispatch(setAlertFlyoutVisible(value));
|
||||
const setAddFlyoutVisibility = (value: boolean) => dispatch(setAlertFlyoutVisible(value));
|
||||
|
||||
const alertFlyoutVisible = useSelector(selectAlertFlyoutVisibility);
|
||||
const alertTypeId = useSelector(selectAlertFlyoutType);
|
||||
|
|
|
@ -69,7 +69,7 @@ export const AlertMonitorStatusComponent: React.FC<AlertMonitorStatusProps> = (p
|
|||
<EuiCallOut
|
||||
size="s"
|
||||
title={
|
||||
<span>
|
||||
<span data-test-subj="alertSnapShotCount">
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.alerts.monitorStatus.monitorCallOut.title"
|
||||
defaultMessage="This alert will apply to approximately {snapshotCount} monitors."
|
||||
|
|
|
@ -17,7 +17,7 @@ import React, { useState } from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { CLIENT_ALERT_TYPES } from '../../../../../common/constants/alerts';
|
||||
import { CLIENT_ALERT_TYPES } from '../../../../../common/constants/uptime_alerts';
|
||||
import { ClientPluginsStart } from '../../../../plugin';
|
||||
|
||||
import { ToggleFlyoutTranslations } from './translations';
|
||||
|
|
|
@ -12,7 +12,7 @@ import { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-
|
|||
interface Props {
|
||||
alertFlyoutVisible: boolean;
|
||||
alertTypeId?: string;
|
||||
setAlertFlyoutVisibility: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setAlertFlyoutVisibility: (value: boolean) => void;
|
||||
}
|
||||
|
||||
interface KibanaDeps {
|
||||
|
|
|
@ -13,7 +13,7 @@ import type { CoreTheme } from '@kbn/core/public';
|
|||
import { EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { RedirectAppLinks, toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
import type { Rule } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { ActionConnector } from '../../state/alerts/alerts';
|
||||
import { ActionConnector } from '../../../../common/rules/types';
|
||||
import { kibanaService } from '../../state/kibana_service';
|
||||
import { getUrlForAlert } from './common';
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import { ALERT_END, ALERT_STATUS, ALERT_STATUS_ACTIVE, ALERT_REASON } from '@kbn
|
|||
import { ObservabilityRuleTypeModel } from '@kbn/observability-plugin/public';
|
||||
import { AlertTypeInitializer } from '.';
|
||||
import { getMonitorRouteFromMonitorId } from '../../../../common/utils/get_monitor_url';
|
||||
import { CLIENT_ALERT_TYPES } from '../../../../common/constants/alerts';
|
||||
import { CLIENT_ALERT_TYPES } from '../../../../common/constants/uptime_alerts';
|
||||
import { DurationAnomalyTranslations } from '../../../../common/translations';
|
||||
|
||||
const { defaultActionMessage, defaultRecoveryMessage, description } = DurationAnomalyTranslations;
|
||||
|
|
|
@ -14,12 +14,12 @@ import { initTlsLegacyAlertType } from './tls_legacy';
|
|||
import { ClientPluginsStart } from '../../../plugin';
|
||||
import { initDurationAnomalyAlertType } from './duration_anomaly';
|
||||
|
||||
export type AlertTypeInitializer<TAlertTypeModel = ObservabilityRuleTypeModel> = (dependenies: {
|
||||
export type AlertTypeInitializer<TAlertTypeModel = ObservabilityRuleTypeModel> = (dependencies: {
|
||||
core: CoreStart;
|
||||
plugins: ClientPluginsStart;
|
||||
}) => TAlertTypeModel;
|
||||
|
||||
export const alertTypeInitializers: AlertTypeInitializer[] = [
|
||||
export const uptimeAlertTypeInitializers: AlertTypeInitializer[] = [
|
||||
initMonitorStatusAlertType,
|
||||
initTlsAlertType,
|
||||
initDurationAnomalyAlertType,
|
||||
|
|
|
@ -202,7 +202,7 @@ describe('monitor status alert type', () => {
|
|||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"defaultActionMessage": "Monitor {{context.monitorName}} with url {{{context.monitorUrl}}} from {{context.observerLocation}} {{{context.statusMessage}}} The latest error message is {{{context.latestErrorMessage}}}",
|
||||
"defaultActionMessage": "Monitor {{context.monitorName}} with url {{{context.monitorUrl}}} from {{context.observerLocation}} {{{context.statusMessage}}} The latest error message is {{{context.latestErrorMessage}}}, checked at {{context.checkedAt}}",
|
||||
"defaultRecoveryMessage": "Alert for monitor {{context.monitorName}} with url {{{context.monitorUrl}}} from {{context.observerLocation}} has recovered",
|
||||
"description": "Alert when a monitor is down or an availability threshold is breached.",
|
||||
"documentationUrl": [Function],
|
||||
|
|
|
@ -21,7 +21,7 @@ import { ValidationResult } from '@kbn/triggers-actions-ui-plugin/public';
|
|||
import { AlertTypeInitializer } from '.';
|
||||
import { getMonitorRouteFromMonitorId } from '../../../../common/utils/get_monitor_url';
|
||||
import { MonitorStatusTranslations } from '../../../../common/translations';
|
||||
import { CLIENT_ALERT_TYPES } from '../../../../common/constants/alerts';
|
||||
import { CLIENT_ALERT_TYPES } from '../../../../common/constants/uptime_alerts';
|
||||
|
||||
const { defaultActionMessage, defaultRecoveryMessage, description } = MonitorStatusTranslations;
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import React from 'react';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
import { ObservabilityRuleTypeModel } from '@kbn/observability-plugin/public';
|
||||
import { CLIENT_ALERT_TYPES } from '../../../../common/constants/alerts';
|
||||
import { CLIENT_ALERT_TYPES } from '../../../../common/constants/uptime_alerts';
|
||||
import { TlsTranslations } from '../../../../common/translations';
|
||||
import { AlertTypeInitializer } from '.';
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { RuleTypeModel } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { CLIENT_ALERT_TYPES } from '../../../../common/constants/alerts';
|
||||
import { CLIENT_ALERT_TYPES } from '../../../../common/constants/uptime_alerts';
|
||||
import { TlsTranslationsLegacy } from '../../../../common/translations';
|
||||
import { AlertTypeInitializer } from '.';
|
||||
|
||||
|
|
|
@ -8,10 +8,8 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { handleActions, Action } from 'redux-actions';
|
||||
import { call, put, select, takeLatest } from 'redux-saga/effects';
|
||||
import type {
|
||||
ActionConnector as RawActionConnector,
|
||||
Rule,
|
||||
} from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import type { Rule } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { ActionConnector } from '../../../../common/rules/types';
|
||||
import { createAsyncAction } from '../actions/utils';
|
||||
import { asyncInitState, handleAsyncAction } from '../reducers/utils';
|
||||
import type { AppState } from '..';
|
||||
|
@ -30,8 +28,6 @@ import { monitorIdSelector } from '../selectors';
|
|||
import { AlertsResult, MonitorIdParam } from '../actions/types';
|
||||
import { simpleAlertEnabled } from '../../lib/alert_types/alert_messages';
|
||||
|
||||
export type ActionConnector = Omit<RawActionConnector, 'secrets'>;
|
||||
|
||||
/**
|
||||
* TODO: Use actual AlertType Params type that's specific to Uptime instead of `any`
|
||||
*/
|
||||
|
|
|
@ -7,15 +7,16 @@
|
|||
|
||||
import type { ActionType, AsApiContract, Rule } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { RuleTypeParams } from '@kbn/alerting-plugin/common';
|
||||
import { CLIENT_ALERT_TYPES } from '../../../../common/constants/alerts';
|
||||
import { MonitorStatusTranslations } from '../../../../common/translations';
|
||||
import { ActionConnector } from '../../../../common/rules/types';
|
||||
import { CLIENT_ALERT_TYPES, MONITOR_STATUS } from '../../../../common/constants/uptime_alerts';
|
||||
import { apiService } from './utils';
|
||||
import { ActionConnector } from '../alerts/alerts';
|
||||
|
||||
import { AlertsResult, MonitorIdParam } from '../actions/types';
|
||||
import { API_URLS } from '../../../../common/constants';
|
||||
import { AtomicStatusCheckParams } from '../../../../common/runtime_types/alerts';
|
||||
|
||||
import { populateAlertActions, RuleAction } from './alert_actions';
|
||||
import { populateAlertActions, RuleAction } from '../../../../common/rules/alert_actions';
|
||||
import { Ping } from '../../../../common/runtime_types/ping';
|
||||
import { DefaultEmail } from '../../../../common/runtime_types';
|
||||
|
||||
|
@ -79,8 +80,13 @@ export const createAlert = async ({
|
|||
}: NewAlertParams): Promise<Rule> => {
|
||||
const actions: RuleAction[] = populateAlertActions({
|
||||
defaultActions,
|
||||
selectedMonitor,
|
||||
defaultEmail,
|
||||
groupId: MONITOR_STATUS.id,
|
||||
translations: {
|
||||
defaultActionMessage: MonitorStatusTranslations.defaultActionMessage,
|
||||
defaultRecoveryMessage: MonitorStatusTranslations.defaultRecoveryMessage,
|
||||
defaultSubjectMessage: MonitorStatusTranslations.defaultSubjectMessage,
|
||||
},
|
||||
});
|
||||
|
||||
const data: NewMonitorStatusAlert = {
|
||||
|
|
|
@ -52,12 +52,13 @@ import {
|
|||
import { LazySyntheticsCustomAssetsExtension } from './legacy_uptime/components/fleet_package/lazy_synthetics_custom_assets_extension';
|
||||
import { uptimeOverviewNavigatorParams } from './apps/locators/overview';
|
||||
import {
|
||||
alertTypeInitializers,
|
||||
uptimeAlertTypeInitializers,
|
||||
legacyAlertTypeInitializers,
|
||||
} from './legacy_uptime/lib/alert_types';
|
||||
import { monitorDetailNavigatorParams } from './apps/locators/monitor_detail';
|
||||
import { editMonitorNavigatorParams } from './apps/locators/edit_monitor';
|
||||
import { setStartServices } from './kibana_services';
|
||||
import { syntheticsAlertTypeInitializers } from './apps/synthetics/lib/alert_types';
|
||||
|
||||
export interface ClientPluginsSetup {
|
||||
home?: HomePublicPluginSetup;
|
||||
|
@ -69,7 +70,7 @@ export interface ClientPluginsSetup {
|
|||
}
|
||||
|
||||
export interface ClientPluginsStart {
|
||||
fleet?: FleetStart;
|
||||
fleet: FleetStart;
|
||||
data: DataPublicPluginStart;
|
||||
unifiedSearch: UnifiedSearchPublicPluginStart;
|
||||
discover: DiscoverStart;
|
||||
|
@ -145,35 +146,7 @@ export class UptimePlugin
|
|||
|
||||
registerUptimeRoutesWithNavigation(core, plugins);
|
||||
|
||||
const { observabilityRuleTypeRegistry } = plugins.observability;
|
||||
|
||||
core.getStartServices().then(([coreStart, clientPluginsStart]) => {
|
||||
alertTypeInitializers.forEach((init) => {
|
||||
const alertInitializer = init({
|
||||
core: coreStart,
|
||||
plugins: clientPluginsStart,
|
||||
});
|
||||
if (
|
||||
clientPluginsStart.triggersActionsUi &&
|
||||
!clientPluginsStart.triggersActionsUi.ruleTypeRegistry.has(alertInitializer.id)
|
||||
) {
|
||||
observabilityRuleTypeRegistry.register(alertInitializer);
|
||||
}
|
||||
});
|
||||
|
||||
legacyAlertTypeInitializers.forEach((init) => {
|
||||
const alertInitializer = init({
|
||||
core: coreStart,
|
||||
plugins: clientPluginsStart,
|
||||
});
|
||||
if (
|
||||
clientPluginsStart.triggersActionsUi &&
|
||||
!clientPluginsStart.triggersActionsUi.ruleTypeRegistry.has(alertInitializer.id)
|
||||
) {
|
||||
plugins.triggersActionsUi.ruleTypeRegistry.register(alertInitializer);
|
||||
}
|
||||
});
|
||||
});
|
||||
core.getStartServices().then(([coreStart, clientPluginsStart]) => {});
|
||||
|
||||
const appKeywords = [
|
||||
'Synthetics',
|
||||
|
@ -236,30 +209,46 @@ export class UptimePlugin
|
|||
}
|
||||
}
|
||||
|
||||
public start(start: CoreStart, plugins: ClientPluginsStart): void {
|
||||
if (plugins.fleet) {
|
||||
const { registerExtension } = plugins.fleet;
|
||||
setStartServices(start);
|
||||
public start(coreStart: CoreStart, pluginsStart: ClientPluginsStart): void {
|
||||
const { triggersActionsUi } = pluginsStart;
|
||||
|
||||
registerExtension({
|
||||
package: 'synthetics',
|
||||
view: 'package-policy-create',
|
||||
Component: LazySyntheticsPolicyCreateExtension,
|
||||
});
|
||||
const { registerExtension } = pluginsStart.fleet;
|
||||
setStartServices(coreStart);
|
||||
registerUptimeFleetExtensions(registerExtension);
|
||||
|
||||
registerExtension({
|
||||
package: 'synthetics',
|
||||
view: 'package-policy-edit',
|
||||
useLatestPackageVersion: true,
|
||||
Component: LazySyntheticsPolicyEditExtension,
|
||||
});
|
||||
syntheticsAlertTypeInitializers.forEach((init) => {
|
||||
const { observabilityRuleTypeRegistry } = pluginsStart.observability;
|
||||
|
||||
registerExtension({
|
||||
package: 'synthetics',
|
||||
view: 'package-detail-assets',
|
||||
Component: LazySyntheticsCustomAssetsExtension,
|
||||
const alertInitializer = init({
|
||||
core: coreStart,
|
||||
plugins: pluginsStart,
|
||||
});
|
||||
}
|
||||
if (!triggersActionsUi.ruleTypeRegistry.has(alertInitializer.id)) {
|
||||
observabilityRuleTypeRegistry.register(alertInitializer);
|
||||
}
|
||||
});
|
||||
|
||||
uptimeAlertTypeInitializers.forEach((init) => {
|
||||
const { observabilityRuleTypeRegistry } = pluginsStart.observability;
|
||||
|
||||
const alertInitializer = init({
|
||||
core: coreStart,
|
||||
plugins: pluginsStart,
|
||||
});
|
||||
if (!triggersActionsUi.ruleTypeRegistry.has(alertInitializer.id)) {
|
||||
observabilityRuleTypeRegistry.register(alertInitializer);
|
||||
}
|
||||
});
|
||||
|
||||
legacyAlertTypeInitializers.forEach((init) => {
|
||||
const alertInitializer = init({
|
||||
core: coreStart,
|
||||
plugins: pluginsStart,
|
||||
});
|
||||
if (!triggersActionsUi.ruleTypeRegistry.has(alertInitializer.id)) {
|
||||
triggersActionsUi.ruleTypeRegistry.register(alertInitializer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public stop(): void {}
|
||||
|
@ -338,3 +327,24 @@ function registerUptimeRoutesWithNavigation(
|
|||
)
|
||||
);
|
||||
}
|
||||
|
||||
function registerUptimeFleetExtensions(registerExtension: FleetStart['registerExtension']) {
|
||||
registerExtension({
|
||||
package: 'synthetics',
|
||||
view: 'package-policy-create',
|
||||
Component: LazySyntheticsPolicyCreateExtension,
|
||||
});
|
||||
|
||||
registerExtension({
|
||||
package: 'synthetics',
|
||||
view: 'package-policy-edit',
|
||||
useLatestPackageVersion: true,
|
||||
Component: LazySyntheticsPolicyEditExtension,
|
||||
});
|
||||
|
||||
registerExtension({
|
||||
package: 'synthetics',
|
||||
view: 'package-detail-assets',
|
||||
Component: LazySyntheticsCustomAssetsExtension,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -27,6 +27,11 @@ const { argv } = yargs(process.argv.slice(2))
|
|||
type: 'boolean',
|
||||
description: 'Opens the Synthetics Test Runner',
|
||||
})
|
||||
.option('watch', {
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
description: 'Runs the server in watch mode, restarting on changes',
|
||||
})
|
||||
.option('pauseOnError', {
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 { UptimeCorePluginsSetup } from '../legacy_uptime/lib/adapters';
|
||||
import { commonMonitorStateI18, commonStateTranslations } from './translations';
|
||||
|
||||
export const MESSAGE = 'message';
|
||||
export const ALERT_REASON_MSG = 'reason';
|
||||
export const ALERT_DETAILS_URL = 'alertDetailsUrl';
|
||||
export const VIEW_IN_APP_URL = 'viewInAppUrl';
|
||||
export const RECOVERY_REASON = 'recoveryReason';
|
||||
|
||||
export const getActionVariables = ({ plugins }: { plugins: UptimeCorePluginsSetup }) => {
|
||||
return {
|
||||
context: [
|
||||
ACTION_VARIABLES[MESSAGE],
|
||||
...(plugins.observability.getAlertDetailsConfig()?.uptime.enabled
|
||||
? [ACTION_VARIABLES[ALERT_DETAILS_URL]]
|
||||
: []),
|
||||
ACTION_VARIABLES[ALERT_REASON_MSG],
|
||||
ACTION_VARIABLES[VIEW_IN_APP_URL],
|
||||
ACTION_VARIABLES[RECOVERY_REASON],
|
||||
...commonMonitorStateI18,
|
||||
],
|
||||
state: [...commonStateTranslations],
|
||||
};
|
||||
};
|
||||
|
||||
export const ACTION_VARIABLES = {
|
||||
[MESSAGE]: {
|
||||
name: MESSAGE,
|
||||
description: i18n.translate(
|
||||
'xpack.synthetics.alertRules.monitorStatus.actionVariables.context.message.description',
|
||||
{
|
||||
defaultMessage: 'A generated message summarizing the status of monitors currently down',
|
||||
}
|
||||
),
|
||||
},
|
||||
[ALERT_REASON_MSG]: {
|
||||
name: ALERT_REASON_MSG,
|
||||
description: i18n.translate(
|
||||
'xpack.synthetics.alertRules.monitorStatus.actionVariables.context.alertReasonMessage.description',
|
||||
{
|
||||
defaultMessage: 'A concise description of the reason for the alert',
|
||||
}
|
||||
),
|
||||
},
|
||||
[ALERT_DETAILS_URL]: {
|
||||
name: ALERT_DETAILS_URL,
|
||||
description: i18n.translate(
|
||||
'xpack.synthetics.alertRules.monitorStatus.actionVariables.context.alertDetailUrl.description',
|
||||
{
|
||||
defaultMessage: 'Link to a view showing further details and context on this alert',
|
||||
}
|
||||
),
|
||||
},
|
||||
[VIEW_IN_APP_URL]: {
|
||||
name: VIEW_IN_APP_URL,
|
||||
description: i18n.translate(
|
||||
'xpack.synthetics.alertRules.monitorStatus.actionVariables.context.viewInAppUrl.description',
|
||||
{
|
||||
defaultMessage: 'Open alert details and context in Synthetics app.',
|
||||
}
|
||||
),
|
||||
},
|
||||
[RECOVERY_REASON]: {
|
||||
name: RECOVERY_REASON,
|
||||
description: i18n.translate(
|
||||
'xpack.synthetics.alertRules.monitorStatus.actionVariables.context.recoveryReason.description',
|
||||
{
|
||||
defaultMessage: 'A concise description of the reason for the recovery',
|
||||
}
|
||||
),
|
||||
},
|
||||
};
|
182
x-pack/plugins/synthetics/server/alert_rules/common.test.ts
Normal file
182
x-pack/plugins/synthetics/server/alert_rules/common.test.ts
Normal file
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* 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 { updateState } from './common';
|
||||
import { SyntheticsCommonState } from '../../common/runtime_types/alert_rules/common';
|
||||
|
||||
describe('updateState', () => {
|
||||
let spy: jest.SpyInstance<string, []>;
|
||||
beforeEach(() => {
|
||||
spy = jest.spyOn(Date.prototype, 'toISOString');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('sets initial state values', () => {
|
||||
spy.mockImplementation(() => 'foo date string');
|
||||
const result = updateState({} as SyntheticsCommonState, false);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"firstCheckedAt": "foo date string",
|
||||
"firstTriggeredAt": undefined,
|
||||
"isTriggered": false,
|
||||
"lastCheckedAt": "foo date string",
|
||||
"lastResolvedAt": undefined,
|
||||
"lastTriggeredAt": undefined,
|
||||
"meta": Object {},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('updates the correct field in subsequent calls', () => {
|
||||
spy
|
||||
.mockImplementationOnce(() => 'first date string')
|
||||
.mockImplementationOnce(() => 'second date string');
|
||||
const firstState = updateState({} as SyntheticsCommonState, false);
|
||||
const secondState = updateState(firstState, true);
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
expect(firstState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": undefined,
|
||||
"isTriggered": false,
|
||||
"lastCheckedAt": "first date string",
|
||||
"lastResolvedAt": undefined,
|
||||
"lastTriggeredAt": undefined,
|
||||
"meta": Object {},
|
||||
}
|
||||
`);
|
||||
expect(secondState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": "second date string",
|
||||
"isTriggered": true,
|
||||
"lastCheckedAt": "second date string",
|
||||
"lastResolvedAt": undefined,
|
||||
"lastTriggeredAt": "second date string",
|
||||
"meta": undefined,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('correctly marks resolution times', () => {
|
||||
spy
|
||||
.mockImplementationOnce(() => 'first date string')
|
||||
.mockImplementationOnce(() => 'second date string')
|
||||
.mockImplementationOnce(() => 'third date string');
|
||||
const firstState = updateState({} as SyntheticsCommonState, true);
|
||||
const secondState = updateState(firstState, true);
|
||||
const thirdState = updateState(secondState, false);
|
||||
expect(spy).toHaveBeenCalledTimes(3);
|
||||
expect(firstState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": "first date string",
|
||||
"isTriggered": true,
|
||||
"lastCheckedAt": "first date string",
|
||||
"lastResolvedAt": undefined,
|
||||
"lastTriggeredAt": "first date string",
|
||||
"meta": Object {},
|
||||
}
|
||||
`);
|
||||
expect(secondState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": "first date string",
|
||||
"isTriggered": true,
|
||||
"lastCheckedAt": "second date string",
|
||||
"lastResolvedAt": undefined,
|
||||
"lastTriggeredAt": "second date string",
|
||||
"meta": undefined,
|
||||
}
|
||||
`);
|
||||
expect(thirdState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": "first date string",
|
||||
"isTriggered": false,
|
||||
"lastCheckedAt": "third date string",
|
||||
"lastResolvedAt": "third date string",
|
||||
"lastTriggeredAt": "second date string",
|
||||
"meta": undefined,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('correctly marks state fields across multiple triggers/resolutions', () => {
|
||||
spy
|
||||
.mockImplementationOnce(() => 'first date string')
|
||||
.mockImplementationOnce(() => 'second date string')
|
||||
.mockImplementationOnce(() => 'third date string')
|
||||
.mockImplementationOnce(() => 'fourth date string')
|
||||
.mockImplementationOnce(() => 'fifth date string');
|
||||
const firstState = updateState({} as SyntheticsCommonState, false);
|
||||
const secondState = updateState(firstState, true);
|
||||
const thirdState = updateState(secondState, false);
|
||||
const fourthState = updateState(thirdState, true);
|
||||
const fifthState = updateState(fourthState, false);
|
||||
expect(spy).toHaveBeenCalledTimes(5);
|
||||
expect(firstState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": undefined,
|
||||
"isTriggered": false,
|
||||
"lastCheckedAt": "first date string",
|
||||
"lastResolvedAt": undefined,
|
||||
"lastTriggeredAt": undefined,
|
||||
"meta": Object {},
|
||||
}
|
||||
`);
|
||||
expect(secondState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": "second date string",
|
||||
"isTriggered": true,
|
||||
"lastCheckedAt": "second date string",
|
||||
"lastResolvedAt": undefined,
|
||||
"lastTriggeredAt": "second date string",
|
||||
"meta": undefined,
|
||||
}
|
||||
`);
|
||||
expect(thirdState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": "second date string",
|
||||
"isTriggered": false,
|
||||
"lastCheckedAt": "third date string",
|
||||
"lastResolvedAt": "third date string",
|
||||
"lastTriggeredAt": "second date string",
|
||||
"meta": undefined,
|
||||
}
|
||||
`);
|
||||
expect(fourthState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": "second date string",
|
||||
"isTriggered": true,
|
||||
"lastCheckedAt": "fourth date string",
|
||||
"lastResolvedAt": "third date string",
|
||||
"lastTriggeredAt": "fourth date string",
|
||||
"meta": undefined,
|
||||
}
|
||||
`);
|
||||
expect(fifthState).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"firstCheckedAt": "first date string",
|
||||
"firstTriggeredAt": "second date string",
|
||||
"isTriggered": false,
|
||||
"lastCheckedAt": "fifth date string",
|
||||
"lastResolvedAt": "fifth date string",
|
||||
"lastTriggeredAt": "fourth date string",
|
||||
"meta": undefined,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
121
x-pack/plugins/synthetics/server/alert_rules/common.ts
Normal file
121
x-pack/plugins/synthetics/server/alert_rules/common.ts
Normal file
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isRight } from 'fp-ts/lib/Either';
|
||||
import Mustache from 'mustache';
|
||||
import { IBasePath } from '@kbn/core/server';
|
||||
import { RuleExecutorServices } from '@kbn/alerting-plugin/server';
|
||||
import { addSpaceIdToPath } from '@kbn/spaces-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
SyntheticsCommonState,
|
||||
SyntheticsCommonStateType,
|
||||
} from '../../common/runtime_types/alert_rules/common';
|
||||
import { ALERT_DETAILS_URL, RECOVERY_REASON } from './action_variables';
|
||||
import { AlertOverviewStatus } from './status_rule/status_rule_executor';
|
||||
|
||||
export const updateState = (
|
||||
state: SyntheticsCommonState,
|
||||
isTriggeredNow: boolean,
|
||||
meta?: SyntheticsCommonState['meta']
|
||||
): SyntheticsCommonState => {
|
||||
const now = new Date().toISOString();
|
||||
const decoded = SyntheticsCommonStateType.decode(state);
|
||||
if (!isRight(decoded)) {
|
||||
const triggerVal = isTriggeredNow ? now : undefined;
|
||||
return {
|
||||
firstCheckedAt: now,
|
||||
firstTriggeredAt: triggerVal,
|
||||
isTriggered: isTriggeredNow,
|
||||
lastTriggeredAt: triggerVal,
|
||||
lastCheckedAt: now,
|
||||
lastResolvedAt: undefined,
|
||||
meta: {},
|
||||
};
|
||||
}
|
||||
const {
|
||||
firstCheckedAt,
|
||||
firstTriggeredAt,
|
||||
lastTriggeredAt,
|
||||
// this is the stale trigger status, we're naming it `wasTriggered`
|
||||
// to differentiate it from the `isTriggeredNow` param
|
||||
isTriggered: wasTriggered,
|
||||
lastResolvedAt,
|
||||
} = decoded.right;
|
||||
|
||||
return {
|
||||
meta,
|
||||
firstCheckedAt: firstCheckedAt ?? now,
|
||||
firstTriggeredAt: isTriggeredNow && !firstTriggeredAt ? now : firstTriggeredAt,
|
||||
lastCheckedAt: now,
|
||||
lastTriggeredAt: isTriggeredNow ? now : lastTriggeredAt,
|
||||
lastResolvedAt: !isTriggeredNow && wasTriggered ? now : lastResolvedAt,
|
||||
isTriggered: isTriggeredNow,
|
||||
};
|
||||
};
|
||||
|
||||
export const generateAlertMessage = (messageTemplate: string, fields: Record<string, any>) => {
|
||||
return Mustache.render(messageTemplate, { context: { ...fields }, state: { ...fields } });
|
||||
};
|
||||
|
||||
export const getViewInAppUrl = (
|
||||
basePath: IBasePath,
|
||||
spaceId: string,
|
||||
relativeViewInAppUrl: string
|
||||
) => addSpaceIdToPath(basePath.publicBaseUrl, spaceId, relativeViewInAppUrl);
|
||||
|
||||
export const getAlertDetailsUrl = (
|
||||
basePath: IBasePath,
|
||||
spaceId: string,
|
||||
alertUuid: string | null
|
||||
) => addSpaceIdToPath(basePath.publicBaseUrl, spaceId, `/app/observability/alerts/${alertUuid}`);
|
||||
|
||||
export const setRecoveredAlertsContext = ({
|
||||
alertFactory,
|
||||
basePath,
|
||||
getAlertUuid,
|
||||
spaceId,
|
||||
staleDownConfigs,
|
||||
}: {
|
||||
alertFactory: RuleExecutorServices['alertFactory'];
|
||||
basePath?: IBasePath;
|
||||
getAlertUuid?: (alertId: string) => string | null;
|
||||
spaceId?: string;
|
||||
staleDownConfigs: AlertOverviewStatus['staleDownConfigs'];
|
||||
}) => {
|
||||
const { getRecoveredAlerts } = alertFactory.done();
|
||||
for (const alert of getRecoveredAlerts()) {
|
||||
const recoveredAlertId = alert.getId();
|
||||
const alertUuid = getAlertUuid?.(recoveredAlertId) || undefined;
|
||||
|
||||
const state = alert.getState() as SyntheticsCommonState;
|
||||
|
||||
let recoveryReason = '';
|
||||
|
||||
if (state?.idWithLocation && staleDownConfigs[state.idWithLocation]) {
|
||||
const { idWithLocation } = state;
|
||||
const downConfig = staleDownConfigs[idWithLocation];
|
||||
if (downConfig.isDeleted) {
|
||||
recoveryReason = i18n.translate('xpack.synthetics.alerts.monitorStatus.deleteMonitor', {
|
||||
defaultMessage: `Monitor has been deleted`,
|
||||
});
|
||||
} else if (downConfig.isLocationRemoved) {
|
||||
recoveryReason = i18n.translate('xpack.synthetics.alerts.monitorStatus.removedLocation', {
|
||||
defaultMessage: `Location has been removed from the monitor`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
alert.setContext({
|
||||
...state,
|
||||
...(recoveryReason ? { [RECOVERY_REASON]: recoveryReason } : {}),
|
||||
...(basePath && spaceId && alertUuid
|
||||
? { [ALERT_DETAILS_URL]: getAlertDetailsUrl(basePath, spaceId, alertUuid) }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue