[Uptime] One click simple monitor down alert (#73835)
* WIP * added anomaly alert * update types * update types * update * types * types * update ML part * update ML part * update ML part * unnecessary change * icon for disable * update test * update api * update labels * resolve conflicts * fix types * fix editing alert * fix types * added actions column * added code to add alert * update anomaly message * added anomaly alert test * update * update type * fix ml legacy scoped client * update * WIP * fix conflict * added aria label * Added deleteion loading * fix type * update * update tests * update * update type * fix types * WIP * added enabled alerts section * add data * update * update tests * fix test * update i18n * update i18n * update i18n * fix * update message * update * update * update * revert * update types * added component * update test * incorporate PR feedback * fix focus * update drawer * handle edge case * improve btn text * improve btn text * use switch instead of icons * update snapshot * use compressed form * fix type * update snapshot * update snapshot * update test * update test * PR feedback * fix test and type * remove delete action * remove unnecessary function Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
42942327e5
commit
a358c5768e
93 changed files with 1829 additions and 266 deletions
|
@ -21,6 +21,7 @@ export {
|
|||
AlertTypeParamsExpressionProps,
|
||||
ValidationResult,
|
||||
ActionVariable,
|
||||
ActionConnector,
|
||||
} from './types';
|
||||
export {
|
||||
ConnectorAddFlyout,
|
||||
|
|
|
@ -24,6 +24,9 @@ export enum API_URLS {
|
|||
ML_DELETE_JOB = `/api/ml/jobs/delete_jobs`,
|
||||
ML_CAPABILITIES = '/api/ml/ml_capabilities',
|
||||
ML_ANOMALIES_RESULT = `/api/ml/results/anomalies_table_data`,
|
||||
|
||||
ALERT_ACTIONS = '/api/actions',
|
||||
CREATE_ALERT = '/api/alerts/alert',
|
||||
ALERT = '/api/alerts/alert/',
|
||||
ALERTS_FIND = '/api/alerts/_find',
|
||||
}
|
||||
|
|
|
@ -10,4 +10,5 @@ export const DYNAMIC_SETTINGS_DEFAULTS: DynamicSettings = {
|
|||
heartbeatIndices: 'heartbeat-8*',
|
||||
certAgeThreshold: 730,
|
||||
certExpirationThreshold: 30,
|
||||
defaultConnectors: [],
|
||||
};
|
||||
|
|
|
@ -25,6 +25,7 @@ export const AtomicStatusCheckParamsType = t.intersection([
|
|||
search: t.string,
|
||||
filters: StatusCheckFiltersType,
|
||||
shouldCheckStatus: t.boolean,
|
||||
isAutoGenerated: t.boolean,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
@ -34,6 +35,7 @@ export const StatusCheckParamsType = t.intersection([
|
|||
t.partial({
|
||||
filters: t.string,
|
||||
shouldCheckStatus: t.boolean,
|
||||
isAutoGenerated: t.boolean,
|
||||
}),
|
||||
t.type({
|
||||
locations: t.array(t.string),
|
||||
|
|
|
@ -10,6 +10,7 @@ export const DynamicSettingsType = t.type({
|
|||
heartbeatIndices: t.string,
|
||||
certAgeThreshold: t.number,
|
||||
certExpirationThreshold: t.number,
|
||||
defaultConnectors: t.array(t.string),
|
||||
});
|
||||
|
||||
export const DynamicSettingsSaveType = t.intersection([
|
||||
|
|
|
@ -18,7 +18,6 @@ export type MonitorError = t.TypeOf<typeof MonitorErrorType>;
|
|||
|
||||
export const MonitorDetailsType = t.intersection([
|
||||
t.type({ monitorId: t.string }),
|
||||
t.partial({ error: MonitorErrorType }),
|
||||
t.partial({ timestamp: t.string }),
|
||||
t.partial({ error: MonitorErrorType, timestamp: t.string, alerts: t.unknown }),
|
||||
]);
|
||||
export type MonitorDetails = t.TypeOf<typeof MonitorDetailsType>;
|
||||
|
|
|
@ -8,10 +8,10 @@ import React from 'react';
|
|||
import { shallow, mount } from 'enzyme';
|
||||
import { EuiLink, EuiButton } from '@elastic/eui';
|
||||
|
||||
import '../../../../lib/__mocks__/react_router_history.mock';
|
||||
import '../../../../lib/__mocks__/ut_router_history.mock';
|
||||
|
||||
import { ReactRouterEuiLink, ReactRouterEuiButton } from '../link_for_eui';
|
||||
import { mockHistory } from '../../../../lib/__mocks__';
|
||||
import { mockHistory } from '../../../../lib/__mocks__/ut_router_history.mock';
|
||||
|
||||
describe('EUI & React Router Component Helpers', () => {
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -22,7 +22,7 @@ import { useMonitorId } from '../../../hooks';
|
|||
import { setAlertFlyoutType, setAlertFlyoutVisible } from '../../../state/actions';
|
||||
import { useAnomalyAlert } from './use_anomaly_alert';
|
||||
import { ConfirmAlertDeletion } from './confirm_alert_delete';
|
||||
import { deleteAlertAction } from '../../../state/actions/alerts';
|
||||
import { deleteAnomalyAlertAction } from '../../../state/alerts/alerts';
|
||||
|
||||
interface Props {
|
||||
hasMLJob: boolean;
|
||||
|
@ -52,7 +52,7 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro
|
|||
const [isConfirmAlertDeleteOpen, setIsConfirmAlertDeleteOpen] = useState(false);
|
||||
|
||||
const deleteAnomalyAlert = () =>
|
||||
dispatch(deleteAlertAction.get({ alertId: anomalyAlert?.id as string }));
|
||||
dispatch(deleteAnomalyAlertAction.get({ alertId: anomalyAlert?.id as string }));
|
||||
|
||||
const showLoading = isMLJobCreating || isMLJobLoading;
|
||||
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { getExistingAlertAction } from '../../../state/actions/alerts';
|
||||
import { alertSelector, selectAlertFlyoutVisibility } from '../../../state/selectors';
|
||||
import { selectAlertFlyoutVisibility } from '../../../state/selectors';
|
||||
import { UptimeRefreshContext } from '../../../contexts';
|
||||
import { useMonitorId } from '../../../hooks';
|
||||
import { anomalyAlertSelector, getAnomalyAlertAction } from '../../../state/alerts/alerts';
|
||||
|
||||
export const useAnomalyAlert = () => {
|
||||
const { lastRefresh } = useContext(UptimeRefreshContext);
|
||||
|
@ -18,12 +18,12 @@ export const useAnomalyAlert = () => {
|
|||
|
||||
const monitorId = useMonitorId();
|
||||
|
||||
const { data: anomalyAlert } = useSelector(alertSelector);
|
||||
const { data: anomalyAlert } = useSelector(anomalyAlertSelector);
|
||||
|
||||
const alertFlyoutVisible = useSelector(selectAlertFlyoutVisibility);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getExistingAlertAction.get({ monitorId }));
|
||||
dispatch(getAnomalyAlertAction.get({ monitorId }));
|
||||
}, [monitorId, lastRefresh, dispatch, alertFlyoutVisible]);
|
||||
|
||||
return anomalyAlert;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { getLayerList } from '../map_config';
|
||||
import { mockLayerList } from './__mocks__/mock';
|
||||
import { mockLayerList } from './__mocks__/poly_layer_mock';
|
||||
import { LocationPoint } from '../embedded_map';
|
||||
import { UptimeAppColors } from '../../../../../../apps/uptime_app';
|
||||
|
||||
|
|
|
@ -1055,9 +1055,26 @@ exports[`MonitorList component renders the monitor list 1`] = `
|
|||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
class="euiTableHeaderCell"
|
||||
data-test-subj="tableHeaderCell_Status alert_5"
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
style="width:150px"
|
||||
>
|
||||
<div
|
||||
class="euiTableCellContent euiTableCellContent--alignCenter"
|
||||
>
|
||||
<span
|
||||
class="euiTableCellContent__text"
|
||||
>
|
||||
Status alert
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<td
|
||||
class="euiTableHeaderCell"
|
||||
data-test-subj="tableHeaderCell_monitor_id_5"
|
||||
data-test-subj="tableHeaderCell_monitor_id_6"
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
style="width:24px"
|
||||
|
@ -1228,6 +1245,52 @@ exports[`MonitorList component renders the monitor list 1`] = `
|
|||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
style="width:150px"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
>
|
||||
Status alert
|
||||
</div>
|
||||
<div
|
||||
class="euiTableCellContent euiTableCellContent--alignCenter euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div
|
||||
class="euiPopover euiPopover--anchorDownCenter"
|
||||
>
|
||||
<div
|
||||
class="euiPopover__anchor"
|
||||
>
|
||||
<div
|
||||
class="euiSwitch euiSwitch--compressed"
|
||||
>
|
||||
<button
|
||||
aria-checked="false"
|
||||
aria-label="Enable status alert"
|
||||
class="euiSwitch__button"
|
||||
data-test-subj="uptimeDisplayDefineConnector"
|
||||
id="defineAlertSettingsSwitch"
|
||||
role="switch"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiSwitch__body"
|
||||
>
|
||||
<span
|
||||
class="euiSwitch__thumb"
|
||||
/>
|
||||
<span
|
||||
class="euiSwitch__track"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="euiTableRowCell euiTableRowCell--isExpander"
|
||||
style="width:24px"
|
||||
|
@ -1405,6 +1468,52 @@ exports[`MonitorList component renders the monitor list 1`] = `
|
|||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
style="width:150px"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
>
|
||||
Status alert
|
||||
</div>
|
||||
<div
|
||||
class="euiTableCellContent euiTableCellContent--alignCenter euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div
|
||||
class="euiPopover euiPopover--anchorDownCenter"
|
||||
>
|
||||
<div
|
||||
class="euiPopover__anchor"
|
||||
>
|
||||
<div
|
||||
class="euiSwitch euiSwitch--compressed"
|
||||
>
|
||||
<button
|
||||
aria-checked="false"
|
||||
aria-label="Enable status alert"
|
||||
class="euiSwitch__button"
|
||||
data-test-subj="uptimeDisplayDefineConnector"
|
||||
id="defineAlertSettingsSwitch"
|
||||
role="switch"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiSwitch__body"
|
||||
>
|
||||
<span
|
||||
class="euiSwitch__thumb"
|
||||
/>
|
||||
<span
|
||||
class="euiSwitch__track"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="euiTableRowCell euiTableRowCell--isExpander"
|
||||
style="width:24px"
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EnableAlertComponent renders without errors for valid props 1`] = `
|
||||
<div
|
||||
class="euiPopover euiPopover--anchorDownCenter"
|
||||
>
|
||||
<div
|
||||
class="euiPopover__anchor"
|
||||
>
|
||||
<div
|
||||
class="euiSwitch euiSwitch--compressed"
|
||||
>
|
||||
<button
|
||||
aria-checked="false"
|
||||
aria-label="Enable status alert"
|
||||
class="euiSwitch__button"
|
||||
data-test-subj="uptimeDisplayDefineConnector"
|
||||
id="defineAlertSettingsSwitch"
|
||||
role="switch"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiSwitch__body"
|
||||
>
|
||||
<span
|
||||
class="euiSwitch__thumb"
|
||||
/>
|
||||
<span
|
||||
class="euiSwitch__track"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`EnableAlertComponent shallow renders without errors for valid props 1`] = `
|
||||
<Provider
|
||||
store={
|
||||
Object {
|
||||
"dispatch": [MockFunction],
|
||||
"getState": [MockFunction],
|
||||
"replaceReducer": [MockFunction],
|
||||
"subscribe": [MockFunction],
|
||||
}
|
||||
}
|
||||
>
|
||||
<Router
|
||||
history={
|
||||
Object {
|
||||
"action": "POP",
|
||||
"block": [Function],
|
||||
"canGo": [Function],
|
||||
"createHref": [Function],
|
||||
"entries": Array [
|
||||
Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
],
|
||||
"go": [Function],
|
||||
"goBack": [Function],
|
||||
"goForward": [Function],
|
||||
"index": 0,
|
||||
"length": 1,
|
||||
"listen": [Function],
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"push": [Function],
|
||||
"replace": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<EnableMonitorAlert
|
||||
monitorId="testMonitor"
|
||||
monitorName="My website"
|
||||
/>
|
||||
</Router>
|
||||
</Provider>
|
||||
`;
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EnableMonitorAlert } from '../enable_alert';
|
||||
import * as redux from 'react-redux';
|
||||
import {
|
||||
mountWithRouterRedux,
|
||||
renderWithRouterRedux,
|
||||
shallowWithRouterRedux,
|
||||
} from '../../../../../lib';
|
||||
import { EuiPopover, EuiText } from '@elastic/eui';
|
||||
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../common/constants';
|
||||
|
||||
describe('EnableAlertComponent', () => {
|
||||
let defaultConnectors: string[] = [];
|
||||
let alerts: any = [];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(redux, 'useDispatch').mockReturnValue(jest.fn());
|
||||
|
||||
jest.spyOn(redux, 'useSelector').mockImplementation((fn, d) => {
|
||||
if (fn.name === 'selectDynamicSettings') {
|
||||
return {
|
||||
settings: Object.assign(DYNAMIC_SETTINGS_DEFAULTS, {
|
||||
defaultConnectors,
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (fn.name === 'alertsSelector') {
|
||||
return {
|
||||
data: {
|
||||
data: alerts,
|
||||
},
|
||||
loading: false,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
});
|
||||
});
|
||||
|
||||
it('shallow renders without errors for valid props', () => {
|
||||
const wrapper = shallowWithRouterRedux(
|
||||
<EnableMonitorAlert monitorId={'testMonitor'} monitorName={'My website'} />
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders without errors for valid props', () => {
|
||||
const wrapper = renderWithRouterRedux(
|
||||
<EnableMonitorAlert monitorId={'testMonitor'} monitorName={'My website'} />
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('displays define connectors when there is none', () => {
|
||||
defaultConnectors = [];
|
||||
const wrapper = mountWithRouterRedux(
|
||||
<EnableMonitorAlert monitorId={'testMonitor'} monitorName={'My website'} />
|
||||
);
|
||||
expect(wrapper.find(EuiPopover)).toHaveLength(1);
|
||||
wrapper.find('button').simulate('click');
|
||||
expect(wrapper.find(EuiText).text()).toBe(
|
||||
'To start enabling alerts, please define a default alert action connector in Settings'
|
||||
);
|
||||
});
|
||||
|
||||
it('does not displays define connectors when there is connector', () => {
|
||||
defaultConnectors = ['infra-slack-connector-id'];
|
||||
const wrapper = mountWithRouterRedux(
|
||||
<EnableMonitorAlert monitorId={'testMonitor'} monitorName={'My website'} />
|
||||
);
|
||||
|
||||
expect(wrapper.find(EuiPopover)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('displays disable when alert is there', () => {
|
||||
alerts = [{ id: 'test-alert', params: { search: 'testMonitor' } }];
|
||||
defaultConnectors = ['infra-slack-connector-id'];
|
||||
|
||||
const wrapper = mountWithRouterRedux(
|
||||
<EnableMonitorAlert monitorId={'testMonitor'} monitorName={'My website'} />
|
||||
);
|
||||
|
||||
expect(wrapper.find('button').prop('aria-label')).toBe('Disable status alert');
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { EuiPopover, EuiSwitch, EuiText } from '@elastic/eui';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { ReactRouterEuiLink } from '../../../common/react_router_helpers';
|
||||
import { MONITOR_ROUTE, SETTINGS_ROUTE } from '../../../../../common/constants';
|
||||
import { ENABLE_STATUS_ALERT } from './translations';
|
||||
import { SETTINGS_LINK_TEXT } from '../../../../pages/page_header';
|
||||
|
||||
export const DefineAlertConnectors = () => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
const onButtonClick = () => setIsPopoverOpen((val) => !val);
|
||||
const closePopover = () => setIsPopoverOpen(false);
|
||||
|
||||
const isMonitorPage = useRouteMatch(MONITOR_ROUTE);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiSwitch
|
||||
id={'defineAlertSettingsSwitch'}
|
||||
label={ENABLE_STATUS_ALERT}
|
||||
showLabel={!!isMonitorPage}
|
||||
aria-label={ENABLE_STATUS_ALERT}
|
||||
onChange={onButtonClick}
|
||||
checked={false}
|
||||
compressed={!isMonitorPage}
|
||||
data-test-subj={'uptimeDisplayDefineConnector'}
|
||||
/>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
>
|
||||
<EuiText style={{ width: '350px' }} data-test-subj={'uptimeSettingsDefineConnector'}>
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.monitorList.defineConnector.description"
|
||||
defaultMessage="To start enabling alerts, please define a default alert action connector in"
|
||||
/>{' '}
|
||||
<ReactRouterEuiLink
|
||||
to={SETTINGS_ROUTE + '?focusConnectorField=true'}
|
||||
data-test-subj={'uptimeSettingsLink'}
|
||||
>
|
||||
{SETTINGS_LINK_TEXT}
|
||||
</ReactRouterEuiLink>
|
||||
</EuiText>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { EuiLoadingSpinner, EuiToolTip, EuiSwitch } from '@elastic/eui';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { selectDynamicSettings } from '../../../../state/selectors';
|
||||
import {
|
||||
alertsSelector,
|
||||
connectorsSelector,
|
||||
createAlertAction,
|
||||
deleteAlertAction,
|
||||
isAlertDeletedSelector,
|
||||
newAlertSelector,
|
||||
} from '../../../../state/alerts/alerts';
|
||||
import { MONITOR_ROUTE } from '../../../../../common/constants';
|
||||
import { DefineAlertConnectors } from './define_connectors';
|
||||
import { DISABLE_STATUS_ALERT, ENABLE_STATUS_ALERT } from './translations';
|
||||
|
||||
interface Props {
|
||||
monitorId: string;
|
||||
monitorName?: string;
|
||||
}
|
||||
|
||||
export const EnableMonitorAlert = ({ monitorId, monitorName }: Props) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { settings } = useSelector(selectDynamicSettings);
|
||||
|
||||
const isMonitorPage = useRouteMatch(MONITOR_ROUTE);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { data: actionConnectors } = useSelector(connectorsSelector);
|
||||
|
||||
const { data: alerts, loading: alertsLoading } = useSelector(alertsSelector);
|
||||
|
||||
const { data: deletedAlertId } = useSelector(isAlertDeletedSelector);
|
||||
|
||||
const { data: newAlert } = useSelector(newAlertSelector);
|
||||
|
||||
const isNewAlert = newAlert?.params.search.includes(monitorId);
|
||||
|
||||
let hasAlert = (alerts?.data ?? []).find((alert) => alert.params.search.includes(monitorId));
|
||||
|
||||
if (isNewAlert) {
|
||||
// if it's newly created alert, we assign that quickly without waiting for find alert result
|
||||
hasAlert = newAlert!;
|
||||
}
|
||||
if (deletedAlertId === hasAlert?.id) {
|
||||
// if it just got deleted, we assign that quickly without waiting for find alert result
|
||||
hasAlert = undefined;
|
||||
}
|
||||
|
||||
const defaultActions = (actionConnectors ?? []).filter((act) =>
|
||||
settings?.defaultConnectors?.includes(act.id)
|
||||
);
|
||||
|
||||
const enableAlert = () => {
|
||||
dispatch(
|
||||
createAlertAction.get({
|
||||
defaultActions,
|
||||
monitorId,
|
||||
monitorName,
|
||||
})
|
||||
);
|
||||
setIsLoading(true);
|
||||
};
|
||||
|
||||
const disableAlert = () => {
|
||||
if (hasAlert) {
|
||||
dispatch(
|
||||
deleteAlertAction.get({
|
||||
alertId: hasAlert.id,
|
||||
})
|
||||
);
|
||||
setIsLoading(true);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(false);
|
||||
}, [hasAlert, deletedAlertId]);
|
||||
|
||||
const hasDefaultConnectors = (settings?.defaultConnectors ?? []).length > 0;
|
||||
|
||||
const showSpinner = isLoading || (alertsLoading && !alerts);
|
||||
|
||||
const onAlertClick = () => {
|
||||
if (hasAlert) {
|
||||
disableAlert();
|
||||
} else {
|
||||
enableAlert();
|
||||
}
|
||||
};
|
||||
const btnLabel = hasAlert ? DISABLE_STATUS_ALERT : ENABLE_STATUS_ALERT;
|
||||
|
||||
return hasDefaultConnectors || hasAlert ? (
|
||||
<div className="eui-displayInlineBlock" style={{ marginRight: 10 }}>
|
||||
{
|
||||
<EuiToolTip content={btnLabel}>
|
||||
<>
|
||||
<EuiSwitch
|
||||
id={'enableDisableAlertSwitch'}
|
||||
compressed={!isMonitorPage}
|
||||
disabled={showSpinner}
|
||||
label={btnLabel}
|
||||
showLabel={!!isMonitorPage}
|
||||
aria-label={btnLabel}
|
||||
onChange={onAlertClick}
|
||||
checked={!!hasAlert}
|
||||
data-test-subj={
|
||||
hasAlert
|
||||
? 'uptimeDisableSimpleDownAlert' + monitorId
|
||||
: 'uptimeEnableSimpleDownAlert' + monitorId
|
||||
}
|
||||
/>{' '}
|
||||
{showSpinner && <EuiLoadingSpinner className="eui-alignMiddle" />}
|
||||
</>
|
||||
</EuiToolTip>
|
||||
}
|
||||
</div>
|
||||
) : (
|
||||
<DefineAlertConnectors />
|
||||
);
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ENABLE_STATUS_ALERT = i18n.translate('xpack.uptime.monitorList.enableDownAlert', {
|
||||
defaultMessage: 'Enable status alert',
|
||||
});
|
||||
|
||||
export const DISABLE_STATUS_ALERT = i18n.translate('xpack.uptime.monitorList.disableDownAlert', {
|
||||
defaultMessage: 'Disable status alert',
|
||||
});
|
|
@ -31,6 +31,8 @@ import { MonitorList } from '../../../state/reducers/monitor_list';
|
|||
import { CertStatusColumn } from './cert_status_column';
|
||||
import { MonitorListHeader } from './monitor_list_header';
|
||||
import { URL_LABEL } from '../../common/translations';
|
||||
import { EnableMonitorAlert } from './columns/enable_alert';
|
||||
import { STATUS_ALERT_COLUMN } from './translations';
|
||||
|
||||
interface Props extends MonitorListProps {
|
||||
pageSize: number;
|
||||
|
@ -49,7 +51,13 @@ export const noItemsMessage = (loading: boolean, filters?: string) => {
|
|||
return !!filters ? labels.NO_MONITOR_ITEM_SELECTED : labels.NO_DATA_MESSAGE;
|
||||
};
|
||||
|
||||
export const MonitorListComponent: React.FC<Props> = ({
|
||||
export const MonitorListComponent: ({
|
||||
filters,
|
||||
monitorList: { list, error, loading },
|
||||
linkParameters,
|
||||
pageSize,
|
||||
setPageSize,
|
||||
}: Props) => any = ({
|
||||
filters,
|
||||
monitorList: { list, error, loading },
|
||||
linkParameters,
|
||||
|
@ -69,7 +77,7 @@ export const MonitorListComponent: React.FC<Props> = ({
|
|||
...map,
|
||||
[id]: (
|
||||
<MonitorListDrawer
|
||||
summary={items.find(({ monitor_id: monitorId }) => monitorId === id)}
|
||||
summary={items.find(({ monitor_id: monitorId }) => monitorId === id)!}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
@ -135,6 +143,18 @@ export const MonitorListComponent: React.FC<Props> = ({
|
|||
<MonitorBarSeries histogramSeries={histogramSeries} />
|
||||
),
|
||||
},
|
||||
{
|
||||
align: 'center' as const,
|
||||
field: '',
|
||||
name: STATUS_ALERT_COLUMN,
|
||||
width: '150px',
|
||||
render: (item: MonitorSummary) => (
|
||||
<EnableMonitorAlert
|
||||
monitorId={item.monitor_id}
|
||||
monitorName={item.state.monitor.name || item.monitor_id}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
align: 'right' as const,
|
||||
field: 'monitor_id',
|
||||
|
|
|
@ -86,6 +86,7 @@ exports[`MonitorListDrawer component renders a MonitorListDrawer when there are
|
|||
}
|
||||
>
|
||||
<MonitorListDrawerComponent
|
||||
loading={false}
|
||||
monitorDetails={
|
||||
Object {
|
||||
"error": Object {
|
||||
|
@ -261,6 +262,7 @@ exports[`MonitorListDrawer component renders a MonitorListDrawer when there is o
|
|||
}
|
||||
>
|
||||
<MonitorListDrawerComponent
|
||||
loading={false}
|
||||
monitorDetails={
|
||||
Object {
|
||||
"error": Object {
|
||||
|
|
|
@ -21,7 +21,7 @@ Array [
|
|||
>
|
||||
<a
|
||||
data-test-subj="monitor-page-link-bad-ssl"
|
||||
href="/monitor/YmFkLXNzbA==/?selectedPingStatus=down"
|
||||
href="/monitor/YmFkLXNzbA==/?selectedPingStatus=down&focusConnectorField=false"
|
||||
>
|
||||
Get https://expired.badssl.com: x509: certificate has expired or is not yet valid
|
||||
</a>
|
||||
|
|
|
@ -52,7 +52,11 @@ describe('MonitorListDrawer component', () => {
|
|||
|
||||
it('renders nothing when no summary data is present', () => {
|
||||
const component = shallowWithRouter(
|
||||
<MonitorListDrawerComponent summary={summary} monitorDetails={monitorDetails} />
|
||||
<MonitorListDrawerComponent
|
||||
summary={summary}
|
||||
monitorDetails={monitorDetails}
|
||||
loading={false}
|
||||
/>
|
||||
);
|
||||
expect(component).toEqual({});
|
||||
});
|
||||
|
@ -60,14 +64,22 @@ describe('MonitorListDrawer component', () => {
|
|||
it('renders nothing when no check data is present', () => {
|
||||
delete summary.state.summaryPings;
|
||||
const component = shallowWithRouter(
|
||||
<MonitorListDrawerComponent summary={summary} monitorDetails={monitorDetails} />
|
||||
<MonitorListDrawerComponent
|
||||
summary={summary}
|
||||
monitorDetails={monitorDetails}
|
||||
loading={false}
|
||||
/>
|
||||
);
|
||||
expect(component).toEqual({});
|
||||
});
|
||||
|
||||
it('renders a MonitorListDrawer when there is only one check', () => {
|
||||
const component = shallowWithRouter(
|
||||
<MonitorListDrawerComponent summary={summary} monitorDetails={monitorDetails} />
|
||||
<MonitorListDrawerComponent
|
||||
summary={summary}
|
||||
monitorDetails={monitorDetails}
|
||||
loading={false}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
@ -88,7 +100,11 @@ describe('MonitorListDrawer component', () => {
|
|||
}
|
||||
|
||||
const component = shallowWithRouter(
|
||||
<MonitorListDrawerComponent summary={summary} monitorDetails={monitorDetails} />
|
||||
<MonitorListDrawerComponent
|
||||
summary={summary}
|
||||
monitorDetails={monitorDetails}
|
||||
loading={false}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useContext } from 'react';
|
||||
import { EuiCallOut, EuiListGroup, EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { EuiListGroupItemProps } from '@elastic/eui/src/components/list_group/list_group_item';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { UptimeSettingsContext } from '../../../../contexts';
|
||||
import { Alert } from '../../../../../../triggers_actions_ui/public';
|
||||
|
||||
interface Props {
|
||||
monitorAlerts: Alert[];
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export const EnabledAlerts = ({ monitorAlerts, loading }: Props) => {
|
||||
const { basePath } = useContext(UptimeSettingsContext);
|
||||
|
||||
const listItems: EuiListGroupItemProps[] = [];
|
||||
|
||||
(monitorAlerts ?? []).forEach((alert, ind) => {
|
||||
listItems.push({
|
||||
label: alert.name,
|
||||
href: basePath + '/app/management/insightsAndAlerting/triggersActions/alert/' + alert.id,
|
||||
size: 's',
|
||||
'data-test-subj': 'uptimeMonitorListDrawerAlert' + ind,
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<span>
|
||||
<EuiText size="xs">
|
||||
<h3>
|
||||
{i18n.translate('xpack.uptime.monitorList.enabledAlerts.title', {
|
||||
defaultMessage: 'Enabled alerts:',
|
||||
description: 'Alerts enabled for this monitor',
|
||||
})}
|
||||
</h3>
|
||||
</EuiText>
|
||||
</span>
|
||||
{listItems.length === 0 && !loading && (
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
title={i18n.translate('xpack.uptime.monitorList.enabledAlerts.noAlert', {
|
||||
defaultMessage: 'No alert is enabled for this monitor.',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{loading ? <EuiLoadingSpinner /> : <EuiListGroup listItems={listItems} />}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -4,44 +4,52 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { AppState } from '../../../../state';
|
||||
import { monitorDetailsSelector } from '../../../../state/selectors';
|
||||
import { MonitorDetailsActionPayload } from '../../../../state/actions/types';
|
||||
import { monitorDetailsLoadingSelector, monitorDetailsSelector } from '../../../../state/selectors';
|
||||
import { getMonitorDetailsAction } from '../../../../state/actions/monitor';
|
||||
import { MonitorListDrawerComponent } from './monitor_list_drawer';
|
||||
import { useGetUrlParams } from '../../../../hooks';
|
||||
import { MonitorDetails, MonitorSummary } from '../../../../../common/runtime_types';
|
||||
import { MonitorSummary } from '../../../../../common/runtime_types';
|
||||
import { alertsSelector } from '../../../../state/alerts/alerts';
|
||||
import { UptimeRefreshContext } from '../../../../contexts';
|
||||
|
||||
interface ContainerProps {
|
||||
summary: MonitorSummary;
|
||||
monitorDetails: MonitorDetails;
|
||||
loadMonitorDetails: typeof getMonitorDetailsAction;
|
||||
}
|
||||
|
||||
const Container: React.FC<ContainerProps> = ({ summary, loadMonitorDetails, monitorDetails }) => {
|
||||
export const MonitorListDrawer: React.FC<ContainerProps> = ({ summary }) => {
|
||||
const { lastRefresh } = useContext(UptimeRefreshContext);
|
||||
|
||||
const monitorId = summary?.monitor_id;
|
||||
|
||||
const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = useGetUrlParams();
|
||||
|
||||
const monitorDetails = useSelector((state: AppState) => monitorDetailsSelector(state, summary));
|
||||
|
||||
const isLoading = useSelector(monitorDetailsLoadingSelector);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { data: alerts, loading: alertsLoading } = useSelector(alertsSelector);
|
||||
|
||||
const hasAlert = (alerts?.data ?? []).find((alert) => alert.params.search.includes(monitorId));
|
||||
|
||||
useEffect(() => {
|
||||
loadMonitorDetails({
|
||||
dateStart,
|
||||
dateEnd,
|
||||
monitorId,
|
||||
});
|
||||
}, [dateStart, dateEnd, monitorId, loadMonitorDetails]);
|
||||
return <MonitorListDrawerComponent monitorDetails={monitorDetails} summary={summary} />;
|
||||
dispatch(
|
||||
getMonitorDetailsAction.get({
|
||||
dateStart,
|
||||
dateEnd,
|
||||
monitorId,
|
||||
})
|
||||
);
|
||||
}, [dateStart, dateEnd, monitorId, dispatch, hasAlert, lastRefresh]);
|
||||
return (
|
||||
<MonitorListDrawerComponent
|
||||
monitorDetails={monitorDetails}
|
||||
summary={summary}
|
||||
loading={isLoading || alertsLoading}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: AppState, { summary }: any) => ({
|
||||
monitorDetails: monitorDetailsSelector(state, summary),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => ({
|
||||
loadMonitorDetails: (actionPayload: MonitorDetailsActionPayload) =>
|
||||
dispatch(getMonitorDetailsAction(actionPayload)),
|
||||
});
|
||||
|
||||
export const MonitorListDrawer = connect(mapStateToProps, mapDispatchToProps)(Container);
|
||||
|
|
|
@ -6,11 +6,13 @@
|
|||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { EuiLink, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui';
|
||||
import { EuiLink, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui';
|
||||
import { MostRecentError } from './most_recent_error';
|
||||
import { MonitorStatusList } from './monitor_status_list';
|
||||
import { MonitorDetails, MonitorSummary } from '../../../../../common/runtime_types';
|
||||
import { ActionsPopover } from './actions_popover/actions_popover_container';
|
||||
import { EnabledAlerts } from './enabled_alerts';
|
||||
import { Alert } from '../../../../../../triggers_actions_ui/public';
|
||||
|
||||
const ContainerDiv = styled.div`
|
||||
padding: 10px;
|
||||
|
@ -27,13 +29,18 @@ interface MonitorListDrawerProps {
|
|||
* Monitor details to be fetched from rest api using monitorId
|
||||
*/
|
||||
monitorDetails: MonitorDetails;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The elements shown when the user expands the monitor list rows.
|
||||
*/
|
||||
|
||||
export function MonitorListDrawerComponent({ summary, monitorDetails }: MonitorListDrawerProps) {
|
||||
export function MonitorListDrawerComponent({
|
||||
summary,
|
||||
monitorDetails,
|
||||
loading,
|
||||
}: MonitorListDrawerProps) {
|
||||
const monitorUrl = summary?.state?.url?.full || '';
|
||||
|
||||
return summary && summary.state.summaryPings ? (
|
||||
|
@ -51,8 +58,8 @@ export function MonitorListDrawerComponent({ summary, monitorDetails }: MonitorL
|
|||
<ActionsPopover summary={summary} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<MonitorStatusList summaryPings={summary.state.summaryPings} />
|
||||
<EnabledAlerts loading={loading} monitorAlerts={monitorDetails?.alerts as Alert[]} />
|
||||
{monitorDetails && monitorDetails.error && (
|
||||
<MostRecentError
|
||||
error={monitorDetails.error}
|
||||
|
|
|
@ -76,3 +76,7 @@ export const RESPONSE_ANOMALY_SCORE = i18n.translate(
|
|||
defaultMessage: 'Response Anomaly Score',
|
||||
}
|
||||
);
|
||||
|
||||
export const STATUS_ALERT_COLUMN = i18n.translate('xpack.uptime.monitorList.statusAlert.label', {
|
||||
defaultMessage: 'Status alert',
|
||||
});
|
||||
|
|
|
@ -91,6 +91,7 @@ exports[`CertificateForm shallow renders expected elements for valid props 1`] =
|
|||
Object {
|
||||
"certAgeThreshold": 36,
|
||||
"certExpirationThreshold": 7,
|
||||
"defaultConnectors": Array [],
|
||||
"heartbeatIndices": "heartbeat-8*",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -91,6 +91,7 @@ exports[`CertificateForm shallow renders expected elements for valid props 1`] =
|
|||
Object {
|
||||
"certAgeThreshold": 36,
|
||||
"certExpirationThreshold": 7,
|
||||
"defaultConnectors": Array [],
|
||||
"heartbeatIndices": "heartbeat-8*",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ describe('CertificateForm', () => {
|
|||
heartbeatIndices: 'heartbeat-8*',
|
||||
certExpirationThreshold: 7,
|
||||
certAgeThreshold: 36,
|
||||
defaultConnectors: [],
|
||||
}}
|
||||
fieldErrors={null}
|
||||
isDisabled={false}
|
||||
|
@ -37,6 +38,7 @@ describe('CertificateForm', () => {
|
|||
heartbeatIndices: 'heartbeat-8*',
|
||||
certExpirationThreshold: 7,
|
||||
certAgeThreshold: 36,
|
||||
defaultConnectors: [],
|
||||
}}
|
||||
fieldErrors={null}
|
||||
isDisabled={false}
|
||||
|
@ -90,6 +92,7 @@ describe('CertificateForm', () => {
|
|||
heartbeatIndices: 'heartbeat-8*',
|
||||
certExpirationThreshold: 7,
|
||||
certAgeThreshold: 36,
|
||||
defaultConnectors: [],
|
||||
}}
|
||||
fieldErrors={null}
|
||||
isDisabled={false}
|
||||
|
|
|
@ -19,6 +19,7 @@ describe('CertificateForm', () => {
|
|||
heartbeatIndices: 'heartbeat-8*',
|
||||
certAgeThreshold: 36,
|
||||
certExpirationThreshold: 7,
|
||||
defaultConnectors: [],
|
||||
}}
|
||||
fieldErrors={null}
|
||||
isDisabled={false}
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
import {
|
||||
ActionsConnectorsContextProvider,
|
||||
ConnectorAddFlyout,
|
||||
} from '../../../../triggers_actions_ui/public';
|
||||
|
||||
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
|
||||
import { getConnectorsAction } from '../../state/alerts/alerts';
|
||||
|
||||
interface Props {
|
||||
focusInput: () => void;
|
||||
}
|
||||
export const AddConnectorFlyout = ({ focusInput }: Props) => {
|
||||
const [addFlyoutVisible, setAddFlyoutVisibility] = useState<boolean>(false);
|
||||
|
||||
const {
|
||||
services: {
|
||||
triggers_actions_ui: { actionTypeRegistry },
|
||||
application,
|
||||
docLinks,
|
||||
http,
|
||||
notifications,
|
||||
},
|
||||
} = useKibana();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getConnectorsAction.get());
|
||||
focusInput();
|
||||
}, [addFlyoutVisible, dispatch, focusInput]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiButtonEmpty
|
||||
iconType="plusInCircleFilled"
|
||||
iconSide="left"
|
||||
onClick={() => setAddFlyoutVisibility(true)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.alerts.settings.createConnector"
|
||||
defaultMessage="Create connector"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
<ActionsConnectorsContextProvider
|
||||
value={{
|
||||
http,
|
||||
docLinks,
|
||||
actionTypeRegistry,
|
||||
toastNotifications: notifications?.toasts,
|
||||
capabilities: application?.capabilities,
|
||||
}}
|
||||
>
|
||||
<ConnectorAddFlyout
|
||||
addFlyoutVisible={addFlyoutVisible}
|
||||
setAddFlyoutVisibility={setAddFlyoutVisibility}
|
||||
/>
|
||||
</ActionsConnectorsContextProvider>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useRef, useCallback } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiDescribedFormGroup,
|
||||
EuiFormRow,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
EuiComboBox,
|
||||
EuiIcon,
|
||||
EuiComboBoxOptionOption,
|
||||
} from '@elastic/eui';
|
||||
import { useSelector } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import { SettingsFormProps } from '../../pages/settings';
|
||||
import { connectorsSelector } from '../../state/alerts/alerts';
|
||||
import { AddConnectorFlyout } from './add_connector_flyout';
|
||||
import { useGetUrlParams, useUrlParams } from '../../hooks';
|
||||
import { alertFormI18n } from './translations';
|
||||
import { useInitApp } from '../../hooks/use_init_app';
|
||||
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
|
||||
|
||||
type ConnectorOption = EuiComboBoxOptionOption<string>;
|
||||
|
||||
const ConnectorSpan = styled.span`
|
||||
.euiIcon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
> img {
|
||||
width: 16px;
|
||||
height: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const AlertDefaultsForm: React.FC<SettingsFormProps> = ({
|
||||
onChange,
|
||||
loading,
|
||||
formFields,
|
||||
fieldErrors,
|
||||
isDisabled,
|
||||
}) => {
|
||||
const {
|
||||
services: {
|
||||
triggers_actions_ui: { actionTypeRegistry },
|
||||
},
|
||||
} = useKibana();
|
||||
const { focusConnectorField } = useGetUrlParams();
|
||||
|
||||
const updateUrlParams = useUrlParams()[1];
|
||||
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
useInitApp();
|
||||
|
||||
useEffect(() => {
|
||||
if (focusConnectorField && inputRef.current && !loading) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [focusConnectorField, inputRef, loading]);
|
||||
|
||||
const { data = [] } = useSelector(connectorsSelector);
|
||||
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
const onBlur = () => {
|
||||
if (inputRef.current) {
|
||||
const { value } = inputRef.current;
|
||||
setError(value.length === 0 ? undefined : `"${value}" is not a valid option`);
|
||||
}
|
||||
if (inputRef.current && !loading && focusConnectorField) {
|
||||
updateUrlParams({ focusConnectorField: undefined });
|
||||
}
|
||||
};
|
||||
|
||||
const onSearchChange = (value: string, hasMatchingOptions?: boolean) => {
|
||||
setError(
|
||||
value.length === 0 || hasMatchingOptions ? undefined : `"${value}" is not a valid option`
|
||||
);
|
||||
};
|
||||
|
||||
const options = (data ?? []).map((connectorAction) => ({
|
||||
value: connectorAction.id,
|
||||
label: connectorAction.name,
|
||||
'data-test-subj': connectorAction.name,
|
||||
}));
|
||||
|
||||
const renderOption = (option: ConnectorOption) => {
|
||||
const { label, value } = option;
|
||||
|
||||
const { actionTypeId: type } = data?.find((dt) => dt.id === value) ?? {};
|
||||
return (
|
||||
<ConnectorSpan>
|
||||
<EuiIcon type={actionTypeRegistry.get(type as string).iconClass} />
|
||||
<span>{label}</span>
|
||||
</ConnectorSpan>
|
||||
);
|
||||
};
|
||||
|
||||
const onOptionChange = (selectedOptions: ConnectorOption[]) => {
|
||||
onChange({
|
||||
defaultConnectors: selectedOptions.map((item) => {
|
||||
const conOpt = data?.find((dt) => dt.id === item.value)!;
|
||||
return conOpt.id;
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.sourceConfiguration.alertDefaults"
|
||||
defaultMessage="Alert defaults"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.sourceConfiguration.alertConnectors"
|
||||
defaultMessage="Alert Connectors"
|
||||
/>
|
||||
</h4>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.sourceConfiguration.defaultConnectors.description"
|
||||
defaultMessage="Default connectors to be used to send an alert."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFormRow
|
||||
describedByIds={['defaultConnectors']}
|
||||
error={error}
|
||||
fullWidth
|
||||
isInvalid={!!error}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.sourceConfiguration.defaultConnectors"
|
||||
defaultMessage="Default connectors"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiComboBox
|
||||
placeholder={alertFormI18n.inputPlaceHolder}
|
||||
options={options}
|
||||
selectedOptions={options.filter((opt) =>
|
||||
formFields?.defaultConnectors?.includes(opt.value)
|
||||
)}
|
||||
inputRef={(input) => {
|
||||
inputRef.current = input;
|
||||
}}
|
||||
onSearchChange={onSearchChange}
|
||||
onBlur={onBlur}
|
||||
isLoading={loading}
|
||||
isDisabled={isDisabled}
|
||||
onChange={onOptionChange}
|
||||
data-test-subj={`default-connectors-input-${loading ? 'loading' : 'loaded'}`}
|
||||
renderOption={renderOption}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<span>
|
||||
<AddConnectorFlyout
|
||||
focusInput={useCallback(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [])}
|
||||
/>
|
||||
</span>
|
||||
</EuiDescribedFormGroup>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -22,3 +22,12 @@ export const certificateFormTranslations = {
|
|||
}
|
||||
),
|
||||
};
|
||||
|
||||
export const alertFormI18n = {
|
||||
inputPlaceHolder: i18n.translate(
|
||||
'xpack.uptime.sourceConfiguration.alertDefaultForm.selectConnector',
|
||||
{
|
||||
defaultMessage: 'Please select one or more connectors',
|
||||
}
|
||||
),
|
||||
};
|
||||
|
|
|
@ -209,7 +209,7 @@ exports[`useUrlParams deletes keys that do not have truthy values 1`] = `
|
|||
}
|
||||
>
|
||||
<div>
|
||||
{"absoluteDateRangeStart":20,"absoluteDateRangeEnd":20,"autorefreshInterval":60000,"autorefreshIsPaused":false,"dateRangeStart":"now-12","dateRangeEnd":"now","filters":"","search":"","selectedPingStatus":"","statusFilter":"","pagination":"foo"}
|
||||
{"absoluteDateRangeStart":20,"absoluteDateRangeEnd":20,"autorefreshInterval":60000,"autorefreshIsPaused":false,"dateRangeStart":"now-12","dateRangeEnd":"now","filters":"","search":"","selectedPingStatus":"","statusFilter":"","pagination":"foo","focusConnectorField":false}
|
||||
</div>
|
||||
<button
|
||||
id="setUrlParams"
|
||||
|
@ -433,7 +433,7 @@ exports[`useUrlParams gets the expected values using the context 1`] = `
|
|||
hook={[Function]}
|
||||
>
|
||||
<div>
|
||||
{"absoluteDateRangeStart":20,"absoluteDateRangeEnd":20,"autorefreshInterval":60000,"autorefreshIsPaused":false,"dateRangeStart":"now-15m","dateRangeEnd":"now","filters":"","search":"","selectedPingStatus":"","statusFilter":""}
|
||||
{"absoluteDateRangeStart":20,"absoluteDateRangeEnd":20,"autorefreshInterval":60000,"autorefreshIsPaused":false,"dateRangeStart":"now-15m","dateRangeEnd":"now","filters":"","search":"","selectedPingStatus":"","statusFilter":"","focusConnectorField":false}
|
||||
</div>
|
||||
<button
|
||||
id="setUrlParams"
|
||||
|
|
18
x-pack/plugins/uptime/public/hooks/use_init_app.ts
Normal file
18
x-pack/plugins/uptime/public/hooks/use_init_app.ts
Normal file
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { getConnectorsAction } from '../state/alerts/alerts';
|
||||
|
||||
// this hook will be use to reload all the data required in common view in app
|
||||
export const useInitApp = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getConnectorsAction.get());
|
||||
}, [dispatch]);
|
||||
};
|
|
@ -1,7 +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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { mockHistory } from './react_router_history.mock';
|
120
x-pack/plugins/uptime/public/lib/__mocks__/uptime_store.mock.ts
Normal file
120
x-pack/plugins/uptime/public/lib/__mocks__/uptime_store.mock.ts
Normal file
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants';
|
||||
|
||||
/**
|
||||
* NOTE: This variable name MUST start with 'mock*' in order for
|
||||
* Jest to accept its use within a jest.mock()
|
||||
*/
|
||||
export const mockStore = {
|
||||
overviewFilters: {
|
||||
filters: {
|
||||
locations: [],
|
||||
ports: [],
|
||||
schemes: [],
|
||||
tags: [],
|
||||
},
|
||||
errors: [],
|
||||
loading: false,
|
||||
},
|
||||
dynamicSettings: {
|
||||
settings: DYNAMIC_SETTINGS_DEFAULTS,
|
||||
loading: false,
|
||||
},
|
||||
monitor: {
|
||||
monitorDetailsList: [],
|
||||
monitorLocationsList: new Map(),
|
||||
loading: false,
|
||||
errors: [],
|
||||
},
|
||||
snapshot: {
|
||||
count: {
|
||||
up: 2,
|
||||
down: 0,
|
||||
total: 2,
|
||||
},
|
||||
errors: [],
|
||||
loading: false,
|
||||
},
|
||||
ui: {
|
||||
alertFlyoutVisible: false,
|
||||
basePath: 'yyz',
|
||||
esKuery: '',
|
||||
integrationsPopoverOpen: null,
|
||||
searchText: '',
|
||||
monitorId: '',
|
||||
},
|
||||
monitorStatus: {
|
||||
status: null,
|
||||
loading: false,
|
||||
},
|
||||
indexPattern: {
|
||||
index_pattern: null,
|
||||
loading: false,
|
||||
errors: [],
|
||||
},
|
||||
ping: {
|
||||
pingHistogram: null,
|
||||
loading: false,
|
||||
errors: [],
|
||||
},
|
||||
pingList: {
|
||||
loading: false,
|
||||
pingList: {
|
||||
total: 0,
|
||||
locations: [],
|
||||
pings: [],
|
||||
},
|
||||
},
|
||||
monitorDuration: {
|
||||
durationLines: null,
|
||||
loading: false,
|
||||
errors: [],
|
||||
},
|
||||
monitorList: {
|
||||
list: {
|
||||
prevPagePagination: null,
|
||||
nextPagePagination: null,
|
||||
summaries: [],
|
||||
totalSummaryCount: 0,
|
||||
},
|
||||
loading: false,
|
||||
},
|
||||
ml: {
|
||||
mlJob: {
|
||||
data: null,
|
||||
loading: false,
|
||||
},
|
||||
createJob: { data: null, loading: false },
|
||||
deleteJob: { data: null, loading: false },
|
||||
mlCapabilities: { data: null, loading: false },
|
||||
anomalies: {
|
||||
data: null,
|
||||
loading: false,
|
||||
},
|
||||
},
|
||||
indexStatus: {
|
||||
indexStatus: {
|
||||
data: null,
|
||||
loading: false,
|
||||
},
|
||||
},
|
||||
certificates: {
|
||||
certs: {
|
||||
data: null,
|
||||
loading: false,
|
||||
},
|
||||
},
|
||||
selectedFilters: null,
|
||||
alerts: {
|
||||
alertDeletion: { data: null, loading: false },
|
||||
anomalyAlert: { data: null, loading: false },
|
||||
alerts: { data: null, loading: false },
|
||||
connectors: { data: null, loading: false },
|
||||
newAlert: { data: null, loading: false },
|
||||
},
|
||||
};
|
|
@ -26,9 +26,9 @@ describe('monitor status alert type', () => {
|
|||
"errors": Object {
|
||||
"typeCheckFailure": "Provided parameters do not conform to the expected type.",
|
||||
"typeCheckParsingMessage": Array [
|
||||
"Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array<string>, observer.geo.name: Array<string>, tags: Array<string>, url.port: Array<string> }, shouldCheckStatus: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number",
|
||||
"Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array<string>, observer.geo.name: Array<string>, tags: Array<string>, url.port: Array<string> }, shouldCheckStatus: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/timerangeCount: number",
|
||||
"Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array<string>, observer.geo.name: Array<string>, tags: Array<string>, url.port: Array<string> }, shouldCheckStatus: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/timerangeUnit: string",
|
||||
"Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array<string>, observer.geo.name: Array<string>, tags: Array<string>, url.port: Array<string> }, shouldCheckStatus: boolean, isAutoGenerated: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number",
|
||||
"Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array<string>, observer.geo.name: Array<string>, tags: Array<string>, url.port: Array<string> }, shouldCheckStatus: boolean, isAutoGenerated: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/timerangeCount: number",
|
||||
"Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array<string>, observer.geo.name: Array<string>, tags: Array<string>, url.port: Array<string> }, shouldCheckStatus: boolean, isAutoGenerated: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/timerangeUnit: string",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
@ -151,7 +151,7 @@ describe('monitor status alert type', () => {
|
|||
"errors": Object {
|
||||
"typeCheckFailure": "Provided parameters do not conform to the expected type.",
|
||||
"typeCheckParsingMessage": Array [
|
||||
"Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array<string>, observer.geo.name: Array<string>, tags: Array<string>, url.port: Array<string> }, shouldCheckStatus: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number",
|
||||
"Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array<string>, observer.geo.name: Array<string>, tags: Array<string>, url.port: Array<string> }, shouldCheckStatus: boolean, isAutoGenerated: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
@ -164,7 +164,7 @@ describe('monitor status alert type', () => {
|
|||
"errors": Object {
|
||||
"typeCheckFailure": "Provided parameters do not conform to the expected type.",
|
||||
"typeCheckParsingMessage": Array [
|
||||
"Invalid value \\"this isn't a number\\" supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array<string>, observer.geo.name: Array<string>, tags: Array<string>, url.port: Array<string> }, shouldCheckStatus: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number",
|
||||
"Invalid value \\"this isn't a number\\" supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array<string>, observer.geo.name: Array<string>, tags: Array<string>, url.port: Array<string> }, shouldCheckStatus: boolean, isAutoGenerated: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public';
|
||||
import { ActionConnector } from '../../state/alerts/alerts';
|
||||
|
||||
export const simpleAlertEnabled = (defaultActions: ActionConnector[]) => {
|
||||
return {
|
||||
title: i18n.translate('xpack.uptime.overview.alerts.enabled.success', {
|
||||
defaultMessage: 'Alert successfully enabled ',
|
||||
}),
|
||||
text: toMountPoint(
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.overview.alerts.enabled.success.description"
|
||||
defaultMessage="Message will be send to {actionConnectors} when monitor is down."
|
||||
values={{
|
||||
actionConnectors: <strong>{defaultActions.map(({ name }) => name).join(', ')}</strong>,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
};
|
||||
};
|
|
@ -6,12 +6,13 @@
|
|||
|
||||
import React from 'react';
|
||||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
import { AppState } from '../../state';
|
||||
|
||||
export const MountWithReduxProvider: React.FC = ({ children }) => (
|
||||
export const MountWithReduxProvider: React.FC<{ store?: AppState }> = ({ children, store }) => (
|
||||
<ReduxProvider
|
||||
store={{
|
||||
dispatch: jest.fn(),
|
||||
getState: jest.fn().mockReturnValue({ selectedFilters: null }),
|
||||
getState: jest.fn().mockReturnValue(store || { selectedFilters: null }),
|
||||
subscribe: jest.fn(),
|
||||
replaceReducer: jest.fn(),
|
||||
}}
|
||||
|
|
|
@ -10,12 +10,16 @@ import { Router } from 'react-router-dom';
|
|||
import { MemoryHistory } from 'history/createMemoryHistory';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { mountWithIntl, renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { AppState } from '../../state';
|
||||
import { MountWithReduxProvider } from './helper_with_redux';
|
||||
|
||||
const helperWithRouter: <R>(
|
||||
helper: (node: ReactElement) => R,
|
||||
component: ReactElement,
|
||||
customHistory?: MemoryHistory
|
||||
) => R = (helper, component, customHistory) => {
|
||||
customHistory?: MemoryHistory,
|
||||
wrapReduxStore?: boolean,
|
||||
storeState?: AppState
|
||||
) => R = (helper, component, customHistory, wrapReduxStore, storeState) => {
|
||||
if (customHistory) {
|
||||
customHistory.location.key = 'TestKeyForTesting';
|
||||
return helper(<Router history={customHistory}>{component}</Router>);
|
||||
|
@ -23,6 +27,14 @@ const helperWithRouter: <R>(
|
|||
const history = createMemoryHistory();
|
||||
history.location.key = 'TestKeyForTesting';
|
||||
|
||||
if (wrapReduxStore) {
|
||||
return helper(
|
||||
<MountWithReduxProvider store={storeState}>
|
||||
<Router history={history}>{component}</Router>
|
||||
</MountWithReduxProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return helper(<Router history={history}>{component}</Router>);
|
||||
};
|
||||
|
||||
|
@ -37,3 +49,24 @@ export const shallowWithRouter = (component: ReactElement, customHistory?: Memor
|
|||
export const mountWithRouter = (component: ReactElement, customHistory?: MemoryHistory) => {
|
||||
return helperWithRouter(mountWithIntl, component, customHistory);
|
||||
};
|
||||
|
||||
export const renderWithRouterRedux = (component: ReactElement, customHistory?: MemoryHistory) => {
|
||||
return helperWithRouter(renderWithIntl, component, customHistory, true);
|
||||
};
|
||||
|
||||
export const shallowWithRouterRedux = (component: ReactElement, customHistory?: MemoryHistory) => {
|
||||
return helperWithRouter(shallowWithIntl, component, customHistory, true);
|
||||
};
|
||||
|
||||
export const mountWithRouterRedux = (
|
||||
component: ReactElement,
|
||||
options?: { customHistory?: MemoryHistory; storeState?: AppState }
|
||||
) => {
|
||||
return helperWithRouter(
|
||||
mountWithIntl,
|
||||
component,
|
||||
options?.customHistory,
|
||||
true,
|
||||
options?.storeState
|
||||
);
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@ Object {
|
|||
"dateRangeEnd": "now",
|
||||
"dateRangeStart": "now-15m",
|
||||
"filters": "",
|
||||
"focusConnectorField": false,
|
||||
"pagination": undefined,
|
||||
"search": "",
|
||||
"selectedPingStatus": "",
|
||||
|
@ -25,6 +26,7 @@ Object {
|
|||
"dateRangeEnd": "now",
|
||||
"dateRangeStart": "now-15m",
|
||||
"filters": "",
|
||||
"focusConnectorField": false,
|
||||
"pagination": undefined,
|
||||
"search": "",
|
||||
"selectedPingStatus": "",
|
||||
|
@ -41,6 +43,7 @@ Object {
|
|||
"dateRangeEnd": "now",
|
||||
"dateRangeStart": "now-15m",
|
||||
"filters": "",
|
||||
"focusConnectorField": false,
|
||||
"pagination": undefined,
|
||||
"search": "monitor.status: down",
|
||||
"selectedPingStatus": "up",
|
||||
|
@ -57,6 +60,7 @@ Object {
|
|||
"dateRangeEnd": "now",
|
||||
"dateRangeStart": "now-15m",
|
||||
"filters": "",
|
||||
"focusConnectorField": false,
|
||||
"pagination": undefined,
|
||||
"search": "",
|
||||
"selectedPingStatus": "",
|
||||
|
@ -73,6 +77,7 @@ Object {
|
|||
"dateRangeEnd": "now",
|
||||
"dateRangeStart": "now-18d",
|
||||
"filters": "",
|
||||
"focusConnectorField": false,
|
||||
"pagination": undefined,
|
||||
"search": "",
|
||||
"selectedPingStatus": "",
|
||||
|
|
|
@ -59,6 +59,8 @@ describe('getSupportedUrlParams', () => {
|
|||
dateRangeStart: DATE_RANGE_START,
|
||||
dateRangeEnd: DATE_RANGE_END,
|
||||
filters: FILTERS,
|
||||
focusConnectorField: false,
|
||||
pagination: undefined,
|
||||
search: SEARCH,
|
||||
selectedPingStatus: SELECTED_PING_LIST_STATUS,
|
||||
statusFilter: STATUS_FILTER,
|
||||
|
|
|
@ -21,6 +21,7 @@ export interface UptimeUrlParams {
|
|||
search: string;
|
||||
selectedPingStatus: string;
|
||||
statusFilter: string;
|
||||
focusConnectorField?: boolean;
|
||||
}
|
||||
|
||||
const {
|
||||
|
@ -75,6 +76,7 @@ export const getSupportedUrlParams = (params: {
|
|||
selectedPingStatus,
|
||||
statusFilter,
|
||||
pagination,
|
||||
focusConnectorField,
|
||||
} = filteredParams;
|
||||
|
||||
return {
|
||||
|
@ -97,5 +99,6 @@ export const getSupportedUrlParams = (params: {
|
|||
selectedPingStatus === undefined ? SELECTED_PING_LIST_STATUS : selectedPingStatus,
|
||||
statusFilter: statusFilter || STATUS_FILTER,
|
||||
pagination,
|
||||
focusConnectorField: !!focusConnectorField,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -5,4 +5,4 @@
|
|||
*/
|
||||
|
||||
export { MountWithReduxProvider } from './helper';
|
||||
export { renderWithRouter, shallowWithRouter, mountWithRouter } from './helper/helper_with_router';
|
||||
export * from './helper/helper_with_router';
|
||||
|
|
|
@ -2,25 +2,21 @@
|
|||
|
||||
exports[`PageHeader shallow renders extra links: page_header_with_extra_links 1`] = `
|
||||
Array [
|
||||
.c0 {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media only screen and (max-width:1024px) and (min-width:868px) {
|
||||
.c1.c1.c1 .euiSuperDatePicker__flexWrapper {
|
||||
@media only screen and (max-width:1024px) and (min-width:868px) {
|
||||
.c0.c0.c0 .euiSuperDatePicker__flexWrapper {
|
||||
width: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width:880px) {
|
||||
.c1.c1.c1 {
|
||||
.c0.c0.c0 {
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex-grow: 1;
|
||||
-ms-flex-positive: 1;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.c1.c1.c1 .euiSuperDatePicker__flexWrapper {
|
||||
.c0.c0.c0 .euiSuperDatePicker__flexWrapper {
|
||||
width: calc(100% + 8px);
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +28,7 @@ Array [
|
|||
class="euiFlexItem"
|
||||
>
|
||||
<h1
|
||||
class="c0 euiTitle euiTitle--medium"
|
||||
class="euiTitle euiTitle--medium eui-textNoWrap"
|
||||
>
|
||||
TestingHeading
|
||||
</h1>
|
||||
|
@ -124,7 +120,7 @@ Array [
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero c1"
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero c0"
|
||||
style="flex-basis:485px"
|
||||
>
|
||||
<div
|
||||
|
@ -234,25 +230,21 @@ Array [
|
|||
|
||||
exports[`PageHeader shallow renders with the date picker: page_header_with_date_picker 1`] = `
|
||||
Array [
|
||||
.c0 {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media only screen and (max-width:1024px) and (min-width:868px) {
|
||||
.c1.c1.c1 .euiSuperDatePicker__flexWrapper {
|
||||
@media only screen and (max-width:1024px) and (min-width:868px) {
|
||||
.c0.c0.c0 .euiSuperDatePicker__flexWrapper {
|
||||
width: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width:880px) {
|
||||
.c1.c1.c1 {
|
||||
.c0.c0.c0 {
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex-grow: 1;
|
||||
-ms-flex-positive: 1;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.c1.c1.c1 .euiSuperDatePicker__flexWrapper {
|
||||
.c0.c0.c0 .euiSuperDatePicker__flexWrapper {
|
||||
width: calc(100% + 8px);
|
||||
}
|
||||
}
|
||||
|
@ -264,16 +256,13 @@ Array [
|
|||
class="euiFlexItem"
|
||||
>
|
||||
<h1
|
||||
class="c0 euiTitle euiTitle--medium"
|
||||
class="euiTitle euiTitle--medium eui-textNoWrap"
|
||||
>
|
||||
TestingHeading
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
/>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero c1"
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero c0"
|
||||
style="flex-basis:485px"
|
||||
>
|
||||
<div
|
||||
|
@ -383,25 +372,18 @@ Array [
|
|||
|
||||
exports[`PageHeader shallow renders without the date picker: page_header_no_date_picker 1`] = `
|
||||
Array [
|
||||
.c0 {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
<div
|
||||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--wrap"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem"
|
||||
>
|
||||
<h1
|
||||
class="c0 euiTitle euiTitle--medium"
|
||||
class="euiTitle euiTitle--medium eui-textNoWrap"
|
||||
>
|
||||
TestingHeading
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
/>
|
||||
</div>,
|
||||
<div
|
||||
class="euiSpacer euiSpacer--s"
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { monitorStatusSelector } from '../state/selectors';
|
||||
|
@ -17,6 +17,9 @@ import { MonitorStatusDetails, PingList } from '../components/monitor';
|
|||
import { getDynamicSettings } from '../state/actions/dynamic_settings';
|
||||
import { Ping } from '../../common/runtime_types/ping';
|
||||
import { setSelectedMonitorId } from '../state/actions';
|
||||
import { EnableMonitorAlert } from '../components/overview/monitor_list/columns/enable_alert';
|
||||
import { getMonitorAlertsAction } from '../state/alerts/alerts';
|
||||
import { useInitApp } from '../hooks/use_init_app';
|
||||
|
||||
const isAutogeneratedId = (id: string) => {
|
||||
const autoGeneratedId = /^auto-(icmp|http|tcp)-OX[A-F0-9]{16}-[a-f0-9]{16}/;
|
||||
|
@ -38,6 +41,8 @@ const getPageTitle = (monId: string, selectedMonitor: Ping | null) => {
|
|||
export const MonitorPage: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useInitApp();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getDynamicSettings());
|
||||
}, [dispatch]);
|
||||
|
@ -46,6 +51,7 @@ export const MonitorPage: React.FC = () => {
|
|||
|
||||
useEffect(() => {
|
||||
dispatch(setSelectedMonitorId(monitorId));
|
||||
dispatch(getMonitorAlertsAction.get());
|
||||
}, [monitorId, dispatch]);
|
||||
|
||||
const selectedMonitor = useSelector(monitorStatusSelector);
|
||||
|
@ -57,7 +63,20 @@ export const MonitorPage: React.FC = () => {
|
|||
useBreadcrumbs([{ text: nameOrId }]);
|
||||
return (
|
||||
<>
|
||||
<PageHeader headingText={nameOrId} datePicker={true} />
|
||||
<PageHeader
|
||||
headingText={
|
||||
<EuiFlexGroup wrap={false}>
|
||||
<EuiFlexItem grow={false}>{nameOrId}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EnableMonitorAlert
|
||||
monitorId={monitorId}
|
||||
monitorName={selectedMonitor?.monitor?.name || selectedMonitor?.url?.full}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
datePicker={true}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<MonitorStatusDetails monitorId={monitorId} />
|
||||
<EuiSpacer size="s" />
|
||||
|
|
|
@ -8,6 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
|||
import React, { useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useGetUrlParams } from '../hooks';
|
||||
import { stringifyUrlParams } from '../lib/helper/stringify_url_params';
|
||||
import { PageHeader } from './page_header';
|
||||
|
@ -18,6 +19,8 @@ import { useTrackPageview } from '../../../observability/public';
|
|||
import { MonitorList } from '../components/overview/monitor_list/monitor_list_container';
|
||||
import { EmptyState, FilterGroup, KueryBar, ParsingErrorCallout } from '../components/overview';
|
||||
import { StatusPanel } from '../components/overview/status_panel';
|
||||
import { getConnectorsAction, getMonitorAlertsAction } from '../state/alerts/alerts';
|
||||
import { useInitApp } from '../hooks/use_init_app';
|
||||
|
||||
interface Props {
|
||||
loading: boolean;
|
||||
|
@ -45,12 +48,21 @@ export const OverviewPageComponent = React.memo(
|
|||
useTrackPageview({ app: 'uptime', path: 'overview' });
|
||||
useTrackPageview({ app: 'uptime', path: 'overview', delay: 15000 });
|
||||
|
||||
useInitApp();
|
||||
|
||||
const [esFilters, error] = useUpdateKueryString(indexPattern, search, urlFilters);
|
||||
|
||||
useEffect(() => {
|
||||
setEsKueryFilters(esFilters ?? '');
|
||||
}, [esFilters, setEsKueryFilters]);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getConnectorsAction.get());
|
||||
dispatch(getMonitorAlertsAction.get());
|
||||
}, [dispatch]);
|
||||
|
||||
const linkParameters = stringifyUrlParams(params, true);
|
||||
|
||||
const heading = i18n.translate('xpack.uptime.overviewPage.headerText', {
|
||||
|
|
|
@ -20,7 +20,7 @@ interface PageHeaderProps {
|
|||
datePicker?: boolean;
|
||||
}
|
||||
|
||||
const SETTINGS_LINK_TEXT = i18n.translate('xpack.uptime.page_header.settingsLink', {
|
||||
export const SETTINGS_LINK_TEXT = i18n.translate('xpack.uptime.page_header.settingsLink', {
|
||||
defaultMessage: 'Settings',
|
||||
});
|
||||
|
||||
|
@ -44,10 +44,6 @@ const StyledPicker = styled(EuiFlexItem)`
|
|||
}
|
||||
`;
|
||||
|
||||
const H1Text = styled.h1`
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const PageHeader = React.memo(
|
||||
({ headingText, extraLinks = false, datePicker = true }: PageHeaderProps) => {
|
||||
const DatePickerComponent = () =>
|
||||
|
@ -96,10 +92,10 @@ export const PageHeader = React.memo(
|
|||
>
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiTitle>
|
||||
<H1Text>{headingText}</H1Text>
|
||||
<h1 className="eui-textNoWrap">{headingText}</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{extraLinkComponents}</EuiFlexItem>
|
||||
{extraLinks && <EuiFlexItem grow={false}>{extraLinkComponents}</EuiFlexItem>}
|
||||
<DatePickerComponent />
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
|
|
|
@ -34,6 +34,7 @@ import {
|
|||
VALUE_MUST_BE_AN_INTEGER,
|
||||
} from '../../common/translations';
|
||||
import { ReactRouterEuiButtonEmpty } from '../components/common/react_router_helpers';
|
||||
import { AlertDefaultsForm } from '../components/settings/alert_defaults_form';
|
||||
|
||||
interface SettingsPageFieldErrors {
|
||||
heartbeatIndices: string | '';
|
||||
|
@ -83,7 +84,8 @@ const isDirtyForm = (formFields: DynamicSettings | null, settings?: DynamicSetti
|
|||
return (
|
||||
settings?.certAgeThreshold !== formFields?.certAgeThreshold ||
|
||||
settings?.certExpirationThreshold !== formFields?.certExpirationThreshold ||
|
||||
settings?.heartbeatIndices !== formFields?.heartbeatIndices
|
||||
settings?.heartbeatIndices !== formFields?.heartbeatIndices ||
|
||||
JSON.stringify(settings?.defaultConnectors) !== JSON.stringify(formFields?.defaultConnectors)
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -161,7 +163,7 @@ export const SettingsPage: React.FC = () => {
|
|||
</EuiFlexGroup>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<form onSubmit={onApply}>
|
||||
<div id="settings-form">
|
||||
<EuiForm>
|
||||
<IndicesForm
|
||||
loading={dss.loading}
|
||||
|
@ -170,6 +172,13 @@ export const SettingsPage: React.FC = () => {
|
|||
fieldErrors={fieldErrors}
|
||||
isDisabled={isFormDisabled}
|
||||
/>
|
||||
<AlertDefaultsForm
|
||||
loading={dss.loading}
|
||||
formFields={formFields}
|
||||
onChange={onChangeFormField}
|
||||
fieldErrors={fieldErrors}
|
||||
isDisabled={isFormDisabled}
|
||||
/>
|
||||
<CertificateExpirationForm
|
||||
loading={dss.loading}
|
||||
onChange={onChangeFormField}
|
||||
|
@ -197,7 +206,7 @@ export const SettingsPage: React.FC = () => {
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="apply-settings-button"
|
||||
type="submit"
|
||||
onClick={onApply}
|
||||
color="primary"
|
||||
isDisabled={!isFormDirty || !isFormValid || isFormDisabled}
|
||||
fill
|
||||
|
@ -210,7 +219,7 @@ export const SettingsPage: React.FC = () => {
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiForm>
|
||||
</form>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
|
|
|
@ -9,6 +9,7 @@ import { MonitorDetailsActionPayload } from './types';
|
|||
import { MonitorError } from '../../../common/runtime_types';
|
||||
import { MonitorLocations } from '../../../common/runtime_types';
|
||||
import { QueryParams } from './types';
|
||||
import { createAsyncAction } from './utils';
|
||||
|
||||
export interface MonitorLocationsPayload extends QueryParams {
|
||||
monitorId: string;
|
||||
|
@ -19,13 +20,10 @@ export interface MonitorDetailsState {
|
|||
error: MonitorError;
|
||||
}
|
||||
|
||||
export const getMonitorDetailsAction = createAction<MonitorDetailsActionPayload>(
|
||||
'GET_MONITOR_DETAILS'
|
||||
);
|
||||
export const getMonitorDetailsActionSuccess = createAction<MonitorDetailsState>(
|
||||
'GET_MONITOR_DETAILS_SUCCESS'
|
||||
);
|
||||
export const getMonitorDetailsActionFail = createAction<Error>('GET_MONITOR_DETAILS_FAIL');
|
||||
export const getMonitorDetailsAction = createAsyncAction<
|
||||
MonitorDetailsActionPayload,
|
||||
MonitorDetailsState
|
||||
>('GET_MONITOR_DETAILS');
|
||||
|
||||
export const getMonitorLocationsAction = createAction<MonitorLocationsPayload>(
|
||||
'GET_MONITOR_LOCATIONS'
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { Action } from 'redux-actions';
|
||||
import { IHttpFetchError } from 'src/core/public';
|
||||
import { Alert } from '../../../../triggers_actions_ui/public';
|
||||
|
||||
export interface AsyncAction<Payload, SuccessPayload> {
|
||||
get: (payload: Payload) => Action<Payload>;
|
||||
|
@ -53,3 +54,10 @@ export interface DeleteJobResults {
|
|||
error?: any;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AlertsResult {
|
||||
page: number;
|
||||
perPage: number;
|
||||
total: number;
|
||||
data: Alert[];
|
||||
}
|
||||
|
|
165
x-pack/plugins/uptime/public/state/alerts/alerts.ts
Normal file
165
x-pack/plugins/uptime/public/state/alerts/alerts.ts
Normal file
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { handleActions, Action } from 'redux-actions';
|
||||
import { call, put, select, takeLatest } from 'redux-saga/effects';
|
||||
import { createAsyncAction } from '../actions/utils';
|
||||
import { asyncInitState, handleAsyncAction } from '../reducers/utils';
|
||||
import { AppState } from '../index';
|
||||
import { AsyncInitState } from '../reducers/types';
|
||||
import { fetchEffectFactory } from '../effects/fetch_effect';
|
||||
import {
|
||||
createAlert,
|
||||
disableAlertById,
|
||||
fetchAlertRecords,
|
||||
fetchConnectors,
|
||||
fetchMonitorAlertRecords,
|
||||
NewAlertParams,
|
||||
} from '../api/alerts';
|
||||
import {
|
||||
ActionConnector as RawActionConnector,
|
||||
Alert,
|
||||
} from '../../../../triggers_actions_ui/public';
|
||||
import { kibanaService } from '../kibana_service';
|
||||
import { monitorIdSelector } from '../selectors';
|
||||
import { AlertsResult, MonitorIdParam } from '../actions/types';
|
||||
import { simpleAlertEnabled } from '../../lib/alert_types/alert_messages';
|
||||
|
||||
export type ActionConnector = Omit<RawActionConnector, 'secrets'>;
|
||||
|
||||
export const createAlertAction = createAsyncAction<NewAlertParams, Alert | null>('CREATE ALERT');
|
||||
export const getConnectorsAction = createAsyncAction<{}, ActionConnector[]>('GET CONNECTORS');
|
||||
export const getMonitorAlertsAction = createAsyncAction<{}, AlertsResult | null>('GET ALERTS');
|
||||
|
||||
export const getAnomalyAlertAction = createAsyncAction<MonitorIdParam, Alert>(
|
||||
'GET EXISTING ALERTS'
|
||||
);
|
||||
export const deleteAlertAction = createAsyncAction<{ alertId: string }, string | null>(
|
||||
'DELETE ALERTS'
|
||||
);
|
||||
export const deleteAnomalyAlertAction = createAsyncAction<{ alertId: string }, any>(
|
||||
'DELETE ANOMALY ALERT'
|
||||
);
|
||||
|
||||
interface AlertState {
|
||||
connectors: AsyncInitState<ActionConnector[]>;
|
||||
newAlert: AsyncInitState<Alert>;
|
||||
alerts: AsyncInitState<AlertsResult>;
|
||||
anomalyAlert: AsyncInitState<Alert>;
|
||||
alertDeletion: AsyncInitState<string>;
|
||||
anomalyAlertDeletion: AsyncInitState<boolean>;
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
connectors: asyncInitState(),
|
||||
newAlert: asyncInitState(),
|
||||
alerts: asyncInitState(),
|
||||
anomalyAlert: asyncInitState(),
|
||||
alertDeletion: asyncInitState(),
|
||||
anomalyAlertDeletion: asyncInitState(),
|
||||
};
|
||||
|
||||
export const alertsReducer = handleActions<AlertState>(
|
||||
{
|
||||
...handleAsyncAction<AlertState>('connectors', getConnectorsAction),
|
||||
...handleAsyncAction<AlertState>('newAlert', createAlertAction),
|
||||
...handleAsyncAction<AlertState>('alerts', getMonitorAlertsAction),
|
||||
...handleAsyncAction<AlertState>('anomalyAlert', getAnomalyAlertAction),
|
||||
...handleAsyncAction<AlertState>('alertDeletion', deleteAlertAction),
|
||||
...handleAsyncAction<AlertState>('anomalyAlertDeletion', deleteAnomalyAlertAction),
|
||||
},
|
||||
initialState
|
||||
);
|
||||
|
||||
const showAlertDisabledSuccess = () => {
|
||||
kibanaService.core.notifications.toasts.addSuccess(
|
||||
i18n.translate('xpack.uptime.overview.alerts.disabled.success', {
|
||||
defaultMessage: 'Alert successfully disabled!',
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const showAlertDisabledFailed = (err: Error) => {
|
||||
kibanaService.core.notifications.toasts.addError(err, {
|
||||
title: i18n.translate('xpack.uptime.overview.alerts.disabled.failed', {
|
||||
defaultMessage: 'Alert cannot be disabled!',
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export function* fetchAlertsEffect() {
|
||||
yield takeLatest(
|
||||
getAnomalyAlertAction.get,
|
||||
fetchEffectFactory(fetchAlertRecords, getAnomalyAlertAction.success, getAnomalyAlertAction.fail)
|
||||
);
|
||||
|
||||
yield takeLatest(deleteAnomalyAlertAction.get, function* (action: Action<{ alertId: string }>) {
|
||||
try {
|
||||
yield call(disableAlertById, action.payload);
|
||||
yield put(deleteAnomalyAlertAction.success(action.payload.alertId));
|
||||
showAlertDisabledSuccess();
|
||||
const monitorId = yield select(monitorIdSelector);
|
||||
yield put(getAnomalyAlertAction.get({ monitorId }));
|
||||
} catch (err) {
|
||||
showAlertDisabledFailed(err);
|
||||
yield put(deleteAnomalyAlertAction.fail(err));
|
||||
}
|
||||
});
|
||||
|
||||
yield takeLatest(deleteAlertAction.get, function* (action: Action<{ alertId: string }>) {
|
||||
try {
|
||||
yield call(disableAlertById, action.payload);
|
||||
// clear previous state
|
||||
yield put(createAlertAction.success(null));
|
||||
yield put(deleteAlertAction.success(action.payload.alertId));
|
||||
|
||||
showAlertDisabledSuccess();
|
||||
yield put(getMonitorAlertsAction.get());
|
||||
} catch (err) {
|
||||
showAlertDisabledFailed(err);
|
||||
yield put(deleteAlertAction.fail(err));
|
||||
}
|
||||
});
|
||||
|
||||
yield takeLatest(
|
||||
getConnectorsAction.get,
|
||||
fetchEffectFactory(fetchConnectors, getConnectorsAction.success, getConnectorsAction.fail)
|
||||
);
|
||||
yield takeLatest(
|
||||
getMonitorAlertsAction.get,
|
||||
fetchEffectFactory(
|
||||
fetchMonitorAlertRecords,
|
||||
getMonitorAlertsAction.success,
|
||||
getMonitorAlertsAction.fail
|
||||
)
|
||||
);
|
||||
yield takeLatest(createAlertAction.get, function* (action: Action<NewAlertParams>) {
|
||||
try {
|
||||
const response = yield call(createAlert, action.payload);
|
||||
yield put(createAlertAction.success(response));
|
||||
|
||||
kibanaService.core.notifications.toasts.addSuccess(
|
||||
simpleAlertEnabled(action.payload.defaultActions)
|
||||
);
|
||||
yield put(getMonitorAlertsAction.get());
|
||||
} catch (err) {
|
||||
kibanaService.core.notifications.toasts.addError(err, {
|
||||
title: i18n.translate('xpack.uptime.overview.alerts.enabled.failed', {
|
||||
defaultMessage: 'Alert cannot be enabled!',
|
||||
}),
|
||||
});
|
||||
yield put(createAlertAction.fail(err));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const connectorsSelector = ({ alerts }: AppState) => alerts.connectors;
|
||||
export const newAlertSelector = ({ alerts }: AppState) => alerts.newAlert;
|
||||
export const alertsSelector = ({ alerts }: AppState) => alerts.alerts;
|
||||
export const isAlertDeletedSelector = ({ alerts }: AppState) => alerts.alertDeletion;
|
||||
|
||||
export const anomalyAlertSelector = ({ alerts }: AppState) => alerts.anomalyAlert;
|
|
@ -4,10 +4,81 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ACTION_GROUP_DEFINITIONS, CLIENT_ALERT_TYPES } from '../../../common/constants/alerts';
|
||||
import { apiService } from './utils';
|
||||
import { ActionConnector } from '../alerts/alerts';
|
||||
|
||||
import { AlertsResult, MonitorIdParam } from '../actions/types';
|
||||
import { Alert, AlertAction } from '../../../../triggers_actions_ui/public';
|
||||
import { API_URLS } from '../../../common/constants';
|
||||
import { MonitorIdParam } from '../actions/types';
|
||||
import { Alert } from '../../../../triggers_actions_ui/public';
|
||||
import { MonitorStatusTranslations } from '../../../common/translations';
|
||||
|
||||
const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS;
|
||||
|
||||
const UPTIME_AUTO_ALERT = 'UPTIME_AUTO';
|
||||
|
||||
export const fetchConnectors = async () => {
|
||||
return await apiService.get(API_URLS.ALERT_ACTIONS);
|
||||
};
|
||||
|
||||
export interface NewAlertParams {
|
||||
monitorId: string;
|
||||
monitorName?: string;
|
||||
defaultActions: ActionConnector[];
|
||||
}
|
||||
|
||||
export const createAlert = async ({
|
||||
defaultActions,
|
||||
monitorId,
|
||||
monitorName,
|
||||
}: NewAlertParams): Promise<Alert> => {
|
||||
const actions: AlertAction[] = [];
|
||||
defaultActions.forEach((aId) => {
|
||||
actions.push({
|
||||
id: aId.id,
|
||||
actionTypeId: aId.actionTypeId,
|
||||
group: MONITOR_STATUS.id,
|
||||
params: {
|
||||
message: MonitorStatusTranslations.defaultActionMessage,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const data = {
|
||||
actions,
|
||||
params: {
|
||||
numTimes: 1,
|
||||
timerangeUnit: 'm',
|
||||
timerangeCount: 1,
|
||||
shouldCheckStatus: true,
|
||||
shouldCheckAvailability: false,
|
||||
isAutoGenerated: true,
|
||||
search: `monitor.id : ${monitorId} `,
|
||||
filters: { 'url.port': [], 'observer.geo.name': [], 'monitor.type': [], tags: [] },
|
||||
},
|
||||
consumer: 'uptime',
|
||||
alertTypeId: CLIENT_ALERT_TYPES.MONITOR_STATUS,
|
||||
schedule: { interval: '1m' },
|
||||
tags: [UPTIME_AUTO_ALERT],
|
||||
name: `${monitorName} (Simple status alert)`,
|
||||
};
|
||||
|
||||
return await apiService.post(API_URLS.CREATE_ALERT, data);
|
||||
};
|
||||
|
||||
export const fetchMonitorAlertRecords = async (): Promise<AlertsResult> => {
|
||||
const data = {
|
||||
page: 1,
|
||||
per_page: 500,
|
||||
filter: 'alert.attributes.alertTypeId:(xpack.uptime.alerts.monitorStatus)',
|
||||
default_search_operator: 'AND',
|
||||
sort_field: 'name.keyword',
|
||||
sort_order: 'asc',
|
||||
search_fields: ['name', 'tags'],
|
||||
search: 'UPTIME_AUTO',
|
||||
};
|
||||
return await apiService.get(API_URLS.ALERTS_FIND, data);
|
||||
};
|
||||
|
||||
export const fetchAlertRecords = async ({ monitorId }: MonitorIdParam): Promise<Alert> => {
|
||||
const data = {
|
||||
|
@ -22,6 +93,6 @@ export const fetchAlertRecords = async ({ monitorId }: MonitorIdParam): Promise<
|
|||
return alerts.data.find((alert: Alert) => alert.params.monitorId === monitorId);
|
||||
};
|
||||
|
||||
export const disableAnomalyAlert = async ({ alertId }: { alertId: string }) => {
|
||||
export const disableAlertById = async ({ alertId }: { alertId: string }) => {
|
||||
return await apiService.delete(API_URLS.ALERT + alertId);
|
||||
};
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
import { handleActions } from 'redux-actions';
|
||||
import { takeLatest } from 'redux-saga/effects';
|
||||
import { createAsyncAction } from '../actions/utils';
|
||||
import { getAsyncInitialState, handleAsyncAction } from '../reducers/utils';
|
||||
import { asyncInitState, handleAsyncAction } from '../reducers/utils';
|
||||
import { CertResult, GetCertsParams } from '../../../common/runtime_types';
|
||||
import { AppState } from '../index';
|
||||
import { AsyncInitialState } from '../reducers/types';
|
||||
import { AsyncInitState } from '../reducers/types';
|
||||
import { fetchEffectFactory } from '../effects/fetch_effect';
|
||||
import { fetchCertificates } from '../api/certificates';
|
||||
|
||||
|
@ -19,11 +19,11 @@ export const getCertificatesAction = createAsyncAction<GetCertsParams, CertResul
|
|||
);
|
||||
|
||||
interface CertificatesState {
|
||||
certs: AsyncInitialState<CertResult>;
|
||||
certs: AsyncInitState<CertResult>;
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
certs: getAsyncInitialState(),
|
||||
certs: asyncInitState(),
|
||||
};
|
||||
|
||||
export const certificatesReducer = handleActions<CertificatesState>(
|
||||
|
|
|
@ -8,7 +8,7 @@ import { Action } from 'redux-actions';
|
|||
import { call, put, takeLatest, select } from 'redux-saga/effects';
|
||||
import { fetchEffectFactory } from './fetch_effect';
|
||||
import { deleteAlertAction, getExistingAlertAction } from '../actions/alerts';
|
||||
import { disableAnomalyAlert, fetchAlertRecords } from '../api/alerts';
|
||||
import { disableAlertById, fetchAlertRecords } from '../api/alerts';
|
||||
import { kibanaService } from '../kibana_service';
|
||||
import { monitorIdSelector } from '../selectors';
|
||||
|
||||
|
@ -24,7 +24,7 @@ export function* fetchAlertsEffect() {
|
|||
|
||||
yield takeLatest(String(deleteAlertAction.get), function* (action: Action<{ alertId: string }>) {
|
||||
try {
|
||||
const response = yield call(disableAnomalyAlert, action.payload);
|
||||
const response = yield call(disableAlertById, action.payload);
|
||||
yield put(deleteAlertAction.success(response));
|
||||
kibanaService.core.notifications.toasts.addSuccess('Alert successfully deleted!');
|
||||
const monitorId = yield select(monitorIdSelector);
|
||||
|
|
|
@ -17,7 +17,7 @@ import { fetchMonitorDurationEffect } from './monitor_duration';
|
|||
import { fetchMLJobEffect } from './ml_anomaly';
|
||||
import { fetchIndexStatusEffect } from './index_status';
|
||||
import { fetchCertificatesEffect } from '../certificates/certificates';
|
||||
import { fetchAlertsEffect } from './alerts';
|
||||
import { fetchAlertsEffect } from '../alerts/alerts';
|
||||
|
||||
export function* rootEffect() {
|
||||
yield fork(fetchMonitorDetailsEffect);
|
||||
|
|
|
@ -21,9 +21,8 @@ import {
|
|||
deleteMLJob,
|
||||
getMLCapabilities,
|
||||
} from '../api/ml_anomaly';
|
||||
import { deleteAlertAction } from '../actions/alerts';
|
||||
import { alertSelector } from '../selectors';
|
||||
import { MonitorIdParam } from '../actions/types';
|
||||
import { anomalyAlertSelector, deleteAlertAction } from '../alerts/alerts';
|
||||
|
||||
export function* fetchMLJobEffect() {
|
||||
yield takeLatest(
|
||||
|
@ -49,7 +48,7 @@ export function* fetchMLJobEffect() {
|
|||
yield put(deleteMLJobAction.success(response));
|
||||
|
||||
// let's delete alert as well if it's there
|
||||
const { data: anomalyAlert } = yield select(alertSelector);
|
||||
const { data: anomalyAlert } = yield select(anomalyAlertSelector);
|
||||
if (anomalyAlert) {
|
||||
yield put(deleteAlertAction.get({ alertId: anomalyAlert.id as string }));
|
||||
}
|
||||
|
|
|
@ -7,8 +7,6 @@
|
|||
import { takeLatest } from 'redux-saga/effects';
|
||||
import {
|
||||
getMonitorDetailsAction,
|
||||
getMonitorDetailsActionSuccess,
|
||||
getMonitorDetailsActionFail,
|
||||
getMonitorLocationsAction,
|
||||
getMonitorLocationsActionSuccess,
|
||||
getMonitorLocationsActionFail,
|
||||
|
@ -18,11 +16,11 @@ import { fetchEffectFactory } from './fetch_effect';
|
|||
|
||||
export function* fetchMonitorDetailsEffect() {
|
||||
yield takeLatest(
|
||||
getMonitorDetailsAction,
|
||||
getMonitorDetailsAction.get,
|
||||
fetchEffectFactory(
|
||||
fetchMonitorDetails,
|
||||
getMonitorDetailsActionSuccess,
|
||||
getMonitorDetailsActionFail
|
||||
getMonitorDetailsAction.success,
|
||||
getMonitorDetailsAction.fail
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
@ -1,29 +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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { handleActions } from 'redux-actions';
|
||||
import { getAsyncInitialState, handleAsyncAction } from './utils';
|
||||
import { AsyncInitialState } from './types';
|
||||
import { deleteAlertAction, getExistingAlertAction } from '../actions/alerts';
|
||||
import { Alert } from '../../../../triggers_actions_ui/public';
|
||||
|
||||
export interface AlertsState {
|
||||
alert: AsyncInitialState<Alert>;
|
||||
alertDeletion: AsyncInitialState<boolean>;
|
||||
}
|
||||
|
||||
const initialState: AlertsState = {
|
||||
alert: getAsyncInitialState(),
|
||||
alertDeletion: getAsyncInitialState(),
|
||||
};
|
||||
|
||||
export const alertsReducer = handleActions<AlertsState>(
|
||||
{
|
||||
...handleAsyncAction<AlertsState>('alert', getExistingAlertAction),
|
||||
...handleAsyncAction<AlertsState>('alertDeletion', deleteAlertAction),
|
||||
},
|
||||
initialState
|
||||
);
|
|
@ -20,7 +20,7 @@ import { indexStatusReducer } from './index_status';
|
|||
import { mlJobsReducer } from './ml_anomaly';
|
||||
import { certificatesReducer } from '../certificates/certificates';
|
||||
import { selectedFiltersReducer } from './selected_filters';
|
||||
import { alertsReducer } from './alerts';
|
||||
import { alertsReducer } from '../alerts/alerts';
|
||||
|
||||
export const rootReducer = combineReducers({
|
||||
monitor: monitorReducer,
|
||||
|
|
|
@ -6,16 +6,16 @@
|
|||
|
||||
import { handleActions } from 'redux-actions';
|
||||
import { indexStatusAction } from '../actions';
|
||||
import { getAsyncInitialState, handleAsyncAction } from './utils';
|
||||
import { AsyncInitialState } from './types';
|
||||
import { asyncInitState, handleAsyncAction } from './utils';
|
||||
import { AsyncInitState } from './types';
|
||||
import { StatesIndexStatus } from '../../../common/runtime_types';
|
||||
|
||||
export interface IndexStatusState {
|
||||
indexStatus: AsyncInitialState<StatesIndexStatus | null>;
|
||||
indexStatus: AsyncInitState<StatesIndexStatus | null>;
|
||||
}
|
||||
|
||||
const initialState: IndexStatusState = {
|
||||
indexStatus: getAsyncInitialState(),
|
||||
indexStatus: asyncInitState(),
|
||||
};
|
||||
|
||||
type PayLoad = StatesIndexStatus & Error;
|
||||
|
|
|
@ -14,25 +14,25 @@ import {
|
|||
AnomalyRecords,
|
||||
getMLCapabilitiesAction,
|
||||
} from '../actions';
|
||||
import { getAsyncInitialState, handleAsyncAction } from './utils';
|
||||
import { AsyncInitialState } from './types';
|
||||
import { asyncInitState, handleAsyncAction } from './utils';
|
||||
import { AsyncInitState } from './types';
|
||||
import { MlCapabilitiesResponse, JobExistResult } from '../../../../../plugins/ml/public';
|
||||
import { CreateMLJobSuccess, DeleteJobResults } from '../actions/types';
|
||||
|
||||
export interface MLJobState {
|
||||
mlJob: AsyncInitialState<JobExistResult>;
|
||||
createJob: AsyncInitialState<CreateMLJobSuccess>;
|
||||
deleteJob: AsyncInitialState<DeleteJobResults>;
|
||||
anomalies: AsyncInitialState<AnomalyRecords>;
|
||||
mlCapabilities: AsyncInitialState<MlCapabilitiesResponse>;
|
||||
mlJob: AsyncInitState<JobExistResult>;
|
||||
createJob: AsyncInitState<CreateMLJobSuccess>;
|
||||
deleteJob: AsyncInitState<DeleteJobResults>;
|
||||
anomalies: AsyncInitState<AnomalyRecords>;
|
||||
mlCapabilities: AsyncInitState<MlCapabilitiesResponse>;
|
||||
}
|
||||
|
||||
const initialState: MLJobState = {
|
||||
mlJob: getAsyncInitialState(),
|
||||
createJob: getAsyncInitialState(),
|
||||
deleteJob: getAsyncInitialState(),
|
||||
anomalies: getAsyncInitialState(),
|
||||
mlCapabilities: getAsyncInitialState(),
|
||||
mlJob: asyncInitState(),
|
||||
createJob: asyncInitState(),
|
||||
deleteJob: asyncInitState(),
|
||||
anomalies: asyncInitState(),
|
||||
mlCapabilities: asyncInitState(),
|
||||
};
|
||||
|
||||
export const mlJobsReducer = handleActions<MLJobState>(
|
||||
|
|
|
@ -9,8 +9,6 @@ import {
|
|||
MonitorDetailsState,
|
||||
getMonitorDetailsAction,
|
||||
getMonitorLocationsAction,
|
||||
getMonitorDetailsActionSuccess,
|
||||
getMonitorDetailsActionFail,
|
||||
getMonitorLocationsActionSuccess,
|
||||
getMonitorLocationsActionFail,
|
||||
} from '../actions/monitor';
|
||||
|
@ -34,12 +32,12 @@ const initialState: MonitorState = {
|
|||
|
||||
export function monitorReducer(state = initialState, action: Action<any>): MonitorState {
|
||||
switch (action.type) {
|
||||
case String(getMonitorDetailsAction):
|
||||
case String(getMonitorDetailsAction.get):
|
||||
return {
|
||||
...state,
|
||||
loading: true,
|
||||
};
|
||||
case String(getMonitorDetailsActionSuccess):
|
||||
case String(getMonitorDetailsAction.success):
|
||||
const { monitorId } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
|
@ -49,10 +47,11 @@ export function monitorReducer(state = initialState, action: Action<any>): Monit
|
|||
},
|
||||
loading: false,
|
||||
};
|
||||
case String(getMonitorDetailsActionFail):
|
||||
case String(getMonitorDetailsAction.fail):
|
||||
return {
|
||||
...state,
|
||||
errors: [...state.errors, action.payload],
|
||||
loading: false,
|
||||
};
|
||||
case String(getMonitorLocationsAction):
|
||||
return {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { IHttpFetchError } from 'src/core/public';
|
||||
|
||||
export interface AsyncInitialState<ReduceStateType> {
|
||||
export interface AsyncInitState<ReduceStateType> {
|
||||
data: ReduceStateType | null;
|
||||
loading: boolean;
|
||||
error?: IHttpFetchError | null;
|
||||
|
|
|
@ -8,7 +8,7 @@ import { Action } from 'redux-actions';
|
|||
import { AsyncAction } from '../actions/types';
|
||||
|
||||
export function handleAsyncAction<ReducerState>(
|
||||
storeKey: string,
|
||||
storeKey: keyof ReducerState,
|
||||
asyncAction: AsyncAction<any, any>
|
||||
) {
|
||||
return {
|
||||
|
@ -20,14 +20,16 @@ export function handleAsyncAction<ReducerState>(
|
|||
},
|
||||
}),
|
||||
|
||||
[String(asyncAction.success)]: (state: ReducerState, action: Action<any>) => ({
|
||||
...state,
|
||||
[storeKey]: {
|
||||
...(state as any)[storeKey],
|
||||
data: action.payload,
|
||||
loading: false,
|
||||
},
|
||||
}),
|
||||
[String(asyncAction.success)]: (state: ReducerState, action: Action<any>) => {
|
||||
return {
|
||||
...state,
|
||||
[storeKey]: {
|
||||
...(state as any)[storeKey],
|
||||
data: action.payload,
|
||||
loading: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
[String(asyncAction.fail)]: (state: ReducerState, action: Action<any>) => ({
|
||||
...state,
|
||||
|
@ -41,7 +43,7 @@ export function handleAsyncAction<ReducerState>(
|
|||
};
|
||||
}
|
||||
|
||||
export function getAsyncInitialState(initialData = null) {
|
||||
export function asyncInitState(initialData = null) {
|
||||
return {
|
||||
data: initialData,
|
||||
loading: false,
|
||||
|
|
|
@ -111,7 +111,11 @@ describe('state selectors', () => {
|
|||
selectedFilters: null,
|
||||
alerts: {
|
||||
alertDeletion: { data: null, loading: false },
|
||||
alert: { data: null, loading: false },
|
||||
anomalyAlert: { data: null, loading: false },
|
||||
alerts: { data: null, loading: false },
|
||||
connectors: { data: null, loading: false },
|
||||
newAlert: { data: null, loading: false },
|
||||
anomalyAlertDeletion: { data: null, loading: false },
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ export const monitorDetailsSelector = (state: AppState, summary: any) => {
|
|||
return state.monitor.monitorDetailsList[summary.monitor_id];
|
||||
};
|
||||
|
||||
export const monitorDetailsLoadingSelector = (state: AppState) => state.monitor.loading;
|
||||
|
||||
export const monitorLocationsSelector = (state: AppState, monitorId: string) => {
|
||||
return state.monitor.monitorLocationsList?.get(monitorId);
|
||||
};
|
||||
|
@ -92,5 +94,3 @@ export const searchTextSelector = ({ ui: { searchText } }: AppState) => searchTe
|
|||
export const selectedFiltersSelector = ({ selectedFilters }: AppState) => selectedFilters;
|
||||
|
||||
export const monitorIdSelector = ({ ui: { monitorId } }: AppState) => monitorId;
|
||||
|
||||
export const alertSelector = ({ alerts }: AppState) => alerts.alert;
|
||||
|
|
|
@ -43,7 +43,7 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor
|
|||
all: {
|
||||
app: ['uptime', 'kibana'],
|
||||
catalogue: ['uptime'],
|
||||
api: ['uptime-read', 'uptime-write'],
|
||||
api: ['uptime-read', 'uptime-write', 'lists-all'],
|
||||
savedObject: {
|
||||
all: [umDynamicSettings.name, 'alert'],
|
||||
read: [],
|
||||
|
@ -54,12 +54,12 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor
|
|||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
ui: ['save', 'configureSettings', 'show'],
|
||||
ui: ['save', 'configureSettings', 'show', 'alerting:save'],
|
||||
},
|
||||
read: {
|
||||
app: ['uptime', 'kibana'],
|
||||
catalogue: ['uptime'],
|
||||
api: ['uptime-read'],
|
||||
api: ['uptime-read', 'lists-read'],
|
||||
savedObject: {
|
||||
all: ['alert'],
|
||||
read: [umDynamicSettings.name],
|
||||
|
@ -70,7 +70,7 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor
|
|||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
ui: ['show'],
|
||||
ui: ['show', 'alerting:save'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -7,23 +7,19 @@
|
|||
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
|
||||
import {
|
||||
IRouter,
|
||||
LegacyCallAPIOptions,
|
||||
SavedObjectsClientContract,
|
||||
ISavedObjectsRepository,
|
||||
ILegacyScopedClusterClient,
|
||||
} from 'src/core/server';
|
||||
import { UMKibanaRoute } from '../../../rest_api';
|
||||
import { PluginSetupContract } from '../../../../../features/server';
|
||||
import { DynamicSettings } from '../../../../common/runtime_types';
|
||||
import { MlPluginSetup as MlSetup } from '../../../../../ml/server';
|
||||
|
||||
export type APICaller = (
|
||||
endpoint: string,
|
||||
clientParams: Record<string, any>,
|
||||
options?: LegacyCallAPIOptions
|
||||
) => Promise<any>;
|
||||
export type ESAPICaller = ILegacyScopedClusterClient['callAsCurrentUser'];
|
||||
|
||||
export type UMElasticsearchQueryFn<P, R = any> = (
|
||||
params: { callES: APICaller; dynamicSettings: DynamicSettings } & P
|
||||
params: { callES: ESAPICaller; dynamicSettings: DynamicSettings } & P
|
||||
) => Promise<R>;
|
||||
|
||||
export type UMSavedObjectsQueryFn<T = any, P = undefined> = (
|
||||
|
|
|
@ -8,7 +8,7 @@ import moment from 'moment';
|
|||
import { ISavedObjectsRepository, SavedObjectsClientContract } from 'kibana/server';
|
||||
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
|
||||
import { PageViewParams, UptimeTelemetry, Usage } from './types';
|
||||
import { APICaller } from '../framework';
|
||||
import { ESAPICaller } from '../framework';
|
||||
import { savedObjectsAdapter } from '../../saved_objects';
|
||||
|
||||
interface UptimeTelemetryCollector {
|
||||
|
@ -69,7 +69,7 @@ export class KibanaTelemetryAdapter {
|
|||
},
|
||||
},
|
||||
},
|
||||
fetch: async (callCluster: APICaller) => {
|
||||
fetch: async (callCluster: ESAPICaller) => {
|
||||
const savedObjectsClient = getSavedObjectsClient()!;
|
||||
if (savedObjectsClient) {
|
||||
await this.countNoOfUniqueMonitorAndLocations(callCluster, savedObjectsClient);
|
||||
|
@ -125,7 +125,7 @@ export class KibanaTelemetryAdapter {
|
|||
}
|
||||
|
||||
public static async countNoOfUniqueMonitorAndLocations(
|
||||
callCluster: APICaller,
|
||||
callCluster: ESAPICaller,
|
||||
savedObjectsClient: ISavedObjectsRepository | SavedObjectsClientContract
|
||||
) {
|
||||
const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient);
|
||||
|
|
|
@ -98,6 +98,7 @@ describe('status check alert', () => {
|
|||
"dynamicSettings": Object {
|
||||
"certAgeThreshold": 730,
|
||||
"certExpirationThreshold": 30,
|
||||
"defaultConnectors": Array [],
|
||||
"heartbeatIndices": "heartbeat-8*",
|
||||
},
|
||||
"filters": undefined,
|
||||
|
@ -153,6 +154,7 @@ describe('status check alert', () => {
|
|||
"dynamicSettings": Object {
|
||||
"certAgeThreshold": 730,
|
||||
"certExpirationThreshold": 30,
|
||||
"defaultConnectors": Array [],
|
||||
"heartbeatIndices": "heartbeat-8*",
|
||||
},
|
||||
"filters": undefined,
|
||||
|
@ -331,6 +333,7 @@ describe('status check alert', () => {
|
|||
"dynamicSettings": Object {
|
||||
"certAgeThreshold": 730,
|
||||
"certExpirationThreshold": 30,
|
||||
"defaultConnectors": Array [],
|
||||
"heartbeatIndices": "heartbeat-8*",
|
||||
},
|
||||
"filters": Object {
|
||||
|
@ -568,6 +571,7 @@ describe('status check alert', () => {
|
|||
"dynamicSettings": Object {
|
||||
"certAgeThreshold": 730,
|
||||
"certExpirationThreshold": 30,
|
||||
"defaultConnectors": Array [],
|
||||
"heartbeatIndices": "heartbeat-8*",
|
||||
},
|
||||
"filters": Object {
|
||||
|
@ -754,6 +758,7 @@ describe('status check alert', () => {
|
|||
"dynamicSettings": Object {
|
||||
"certAgeThreshold": 730,
|
||||
"certExpirationThreshold": 30,
|
||||
"defaultConnectors": Array [],
|
||||
"heartbeatIndices": "heartbeat-8*",
|
||||
},
|
||||
"filters": "{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":12349}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":5601}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":443}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"observer.geo.name\\":\\"harrisburg\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"monitor.type\\":\\"http\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"tags\\":\\"unsecured\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"tags\\":\\"containers\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"tags\\":\\"org:google\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}]}}]}}]}}",
|
||||
|
@ -808,6 +813,7 @@ describe('status check alert', () => {
|
|||
"dynamicSettings": Object {
|
||||
"certAgeThreshold": 730,
|
||||
"certExpirationThreshold": 30,
|
||||
"defaultConnectors": Array [],
|
||||
"heartbeatIndices": "heartbeat-8*",
|
||||
},
|
||||
"filters": "{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\":\\"ur.port\\"}}],\\"minimum_should_match\\":1}}",
|
||||
|
@ -851,6 +857,7 @@ describe('status check alert', () => {
|
|||
"dynamicSettings": Object {
|
||||
"certAgeThreshold": 730,
|
||||
"certExpirationThreshold": 30,
|
||||
"defaultConnectors": Array [],
|
||||
"heartbeatIndices": "heartbeat-8*",
|
||||
},
|
||||
"filters": undefined,
|
||||
|
@ -886,6 +893,7 @@ describe('status check alert', () => {
|
|||
"timerangeUnit",
|
||||
"timerange",
|
||||
"version",
|
||||
"isAutoGenerated",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ILegacyScopedClusterClient } from 'kibana/server';
|
||||
import Mustache from 'mustache';
|
||||
import { UptimeAlertTypeFactory } from './types';
|
||||
import { esKuery, IIndexPattern } from '../../../../../../src/plugins/data/server';
|
||||
|
@ -22,11 +21,13 @@ import { updateState } from './common';
|
|||
import { commonMonitorStateI18, commonStateTranslations, DOWN_LABEL } from './translations';
|
||||
import { stringifyKueries, combineFiltersAndUserSearch } from '../../../common/lib';
|
||||
import { GetMonitorAvailabilityResult } from '../requests/get_monitor_availability';
|
||||
import { UMServerLibs } from '../lib';
|
||||
import { GetMonitorStatusResult } from '../requests/get_monitor_status';
|
||||
import { UNNAMED_LOCATION } from '../../../common/constants';
|
||||
import { uptimeAlertWrapper } from './uptime_alert_wrapper';
|
||||
import { MonitorStatusTranslations } from '../../../common/translations';
|
||||
import { ESAPICaller } from '../adapters/framework';
|
||||
import { getUptimeIndexPattern } from '../requests/get_index_pattern';
|
||||
import { UMServerLibs } from '../lib';
|
||||
|
||||
const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS;
|
||||
|
||||
|
@ -77,19 +78,21 @@ export const generateFilterDSL = async (
|
|||
);
|
||||
};
|
||||
|
||||
const formatFilterString = async (
|
||||
libs: UMServerLibs,
|
||||
export const formatFilterString = async (
|
||||
dynamicSettings: DynamicSettings,
|
||||
callES: ILegacyScopedClusterClient['callAsCurrentUser'],
|
||||
callES: ESAPICaller,
|
||||
filters: StatusCheckFilters,
|
||||
search: string
|
||||
search: string,
|
||||
libs?: UMServerLibs
|
||||
) =>
|
||||
await generateFilterDSL(
|
||||
() =>
|
||||
libs.requests.getIndexPattern({
|
||||
callES,
|
||||
dynamicSettings,
|
||||
}),
|
||||
libs?.requests?.getIndexPattern
|
||||
? libs?.requests?.getIndexPattern({ callES, dynamicSettings })
|
||||
: getUptimeIndexPattern({
|
||||
callES,
|
||||
dynamicSettings,
|
||||
}),
|
||||
filters,
|
||||
search
|
||||
);
|
||||
|
@ -197,6 +200,7 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) =
|
|||
})
|
||||
),
|
||||
version: schema.maybe(schema.number()),
|
||||
isAutoGenerated: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
},
|
||||
defaultActionGroupId: MONITOR_STATUS.id,
|
||||
|
@ -244,26 +248,19 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) =
|
|||
availability,
|
||||
shouldCheckAvailability,
|
||||
shouldCheckStatus,
|
||||
isAutoGenerated,
|
||||
timerange: oldVersionTimeRange,
|
||||
} = rawParams;
|
||||
|
||||
const filterString = await formatFilterString(dynamicSettings, callES, filters, search, libs);
|
||||
|
||||
const timerange = oldVersionTimeRange || {
|
||||
from: `now-${String(timerangeCount) + timerangeUnit}`,
|
||||
from: isAutoGenerated
|
||||
? state.lastCheckedAt
|
||||
: `now-${String(timerangeCount) + timerangeUnit}`,
|
||||
to: 'now',
|
||||
};
|
||||
|
||||
const filterString = await formatFilterString(libs, dynamicSettings, callES, filters, search);
|
||||
|
||||
let availabilityResults: GetMonitorAvailabilityResult[] = [];
|
||||
if (shouldCheckAvailability) {
|
||||
availabilityResults = await libs.requests.getMonitorAvailability({
|
||||
callES,
|
||||
dynamicSettings,
|
||||
...availability,
|
||||
filters: JSON.stringify(filterString) || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
let downMonitorsByLocation: GetMonitorStatusResult[] = [];
|
||||
|
||||
// if oldVersionTimeRange present means it's 7.7 format and
|
||||
|
@ -279,6 +276,37 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) =
|
|||
});
|
||||
}
|
||||
|
||||
if (isAutoGenerated) {
|
||||
for (const monitorLoc of downMonitorsByLocation) {
|
||||
const monitorInfo = monitorLoc.monitorInfo;
|
||||
|
||||
const alertInstance = alertInstanceFactory(MONITOR_STATUS.id + monitorLoc.location);
|
||||
|
||||
const monitorSummary = getMonitorSummary(monitorInfo);
|
||||
const statusMessage = getStatusMessage(monitorInfo);
|
||||
|
||||
alertInstance.replaceState({
|
||||
...state,
|
||||
...monitorSummary,
|
||||
statusMessage,
|
||||
...updateState(state, true),
|
||||
});
|
||||
|
||||
alertInstance.scheduleActions(MONITOR_STATUS.id);
|
||||
}
|
||||
return updateState(state, downMonitorsByLocation.length > 0);
|
||||
}
|
||||
|
||||
let availabilityResults: GetMonitorAvailabilityResult[] = [];
|
||||
if (shouldCheckAvailability) {
|
||||
availabilityResults = await libs.requests.getMonitorAvailability({
|
||||
callES,
|
||||
dynamicSettings,
|
||||
...availability,
|
||||
filters: JSON.stringify(filterString) || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
const mergedIdsByLoc = getUniqueIdsByLoc(downMonitorsByLocation, availabilityResults);
|
||||
|
||||
mergedIdsByLoc.forEach((monIdByLoc) => {
|
||||
|
|
|
@ -94,6 +94,7 @@ describe('getCerts', () => {
|
|||
heartbeatIndices: 'heartbeat*',
|
||||
certAgeThreshold: DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold,
|
||||
certExpirationThreshold: DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold,
|
||||
defaultConnectors: [],
|
||||
},
|
||||
index: 1,
|
||||
from: 'now-2d',
|
||||
|
|
|
@ -32,7 +32,7 @@ describe('getLatestMonitor', () => {
|
|||
},
|
||||
},
|
||||
size: 1,
|
||||
_source: ['url', 'monitor', 'observer', '@timestamp', 'tls.*', 'http'],
|
||||
_source: ['url', 'monitor', 'observer', '@timestamp', 'tls.*', 'http', 'error'],
|
||||
sort: {
|
||||
'@timestamp': { order: 'desc' },
|
||||
},
|
||||
|
|
|
@ -16,6 +16,10 @@ export interface GetLatestMonitorParams {
|
|||
|
||||
/** @member monitorId optional limit to monitorId */
|
||||
monitorId?: string | null;
|
||||
|
||||
observerLocation?: string;
|
||||
|
||||
status?: string;
|
||||
}
|
||||
|
||||
// Get The monitor latest state sorted by timestamp with date range
|
||||
|
@ -25,6 +29,8 @@ export const getLatestMonitor: UMElasticsearchQueryFn<GetLatestMonitorParams, Pi
|
|||
dateStart,
|
||||
dateEnd,
|
||||
monitorId,
|
||||
observerLocation,
|
||||
status,
|
||||
}) => {
|
||||
const params = {
|
||||
index: dynamicSettings.heartbeatIndices,
|
||||
|
@ -40,12 +46,14 @@ export const getLatestMonitor: UMElasticsearchQueryFn<GetLatestMonitorParams, Pi
|
|||
},
|
||||
},
|
||||
},
|
||||
...(status ? [{ term: { 'monitor.status': status } }] : []),
|
||||
...(monitorId ? [{ term: { 'monitor.id': monitorId } }] : []),
|
||||
...(observerLocation ? [{ term: { 'observer.geo.name': observerLocation } }] : []),
|
||||
],
|
||||
},
|
||||
},
|
||||
size: 1,
|
||||
_source: ['url', 'monitor', 'observer', '@timestamp', 'tls.*', 'http'],
|
||||
_source: ['url', 'monitor', 'observer', '@timestamp', 'tls.*', 'http', 'error'],
|
||||
sort: {
|
||||
'@timestamp': { order: 'desc' },
|
||||
},
|
||||
|
|
|
@ -4,19 +4,87 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { UMElasticsearchQueryFn } from '../adapters';
|
||||
import { ESAPICaller, UMElasticsearchQueryFn } from '../adapters';
|
||||
import { MonitorDetails, MonitorError } from '../../../common/runtime_types';
|
||||
import { formatFilterString } from '../alerts/status_check';
|
||||
|
||||
export interface GetMonitorDetailsParams {
|
||||
monitorId: string;
|
||||
dateStart: string;
|
||||
dateEnd: string;
|
||||
alertsClient: any;
|
||||
}
|
||||
|
||||
const getMonitorAlerts = async (
|
||||
callES: ESAPICaller,
|
||||
dynamicSettings: any,
|
||||
alertsClient: any,
|
||||
monitorId: string
|
||||
) => {
|
||||
const options: any = {
|
||||
page: 1,
|
||||
perPage: 500,
|
||||
filter: 'alert.attributes.alertTypeId:(xpack.uptime.alerts.monitorStatus)',
|
||||
defaultSearchOperator: 'AND',
|
||||
sortField: 'name.keyword',
|
||||
};
|
||||
|
||||
const { data } = await alertsClient.find({ options });
|
||||
const monitorAlerts = [];
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const currAlert = data[i];
|
||||
|
||||
if (currAlert.params.search?.includes(monitorId)) {
|
||||
monitorAlerts.push(currAlert);
|
||||
continue;
|
||||
}
|
||||
const esParams: any = {
|
||||
index: dynamicSettings.heartbeatIndices,
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
term: {
|
||||
'monitor.id': monitorId,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
size: 0,
|
||||
aggs: {
|
||||
monitors: {
|
||||
terms: {
|
||||
field: 'monitor.id',
|
||||
size: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const parsedFilters = await formatFilterString(
|
||||
dynamicSettings,
|
||||
callES,
|
||||
currAlert.params.filters,
|
||||
currAlert.params.search
|
||||
);
|
||||
esParams.body.query.bool = Object.assign({}, esParams.body.query.bool, parsedFilters?.bool);
|
||||
|
||||
const result = await callES('search', esParams);
|
||||
|
||||
if (result.hits.total.value > 0) {
|
||||
monitorAlerts.push(currAlert);
|
||||
}
|
||||
}
|
||||
return monitorAlerts;
|
||||
};
|
||||
|
||||
export const getMonitorDetails: UMElasticsearchQueryFn<
|
||||
GetMonitorDetailsParams,
|
||||
MonitorDetails
|
||||
> = async ({ callES, dynamicSettings, monitorId, dateStart, dateEnd }) => {
|
||||
> = async ({ callES, dynamicSettings, monitorId, dateStart, dateEnd, alertsClient }) => {
|
||||
const queryFilters: any = [
|
||||
{
|
||||
range: {
|
||||
|
@ -66,10 +134,11 @@ export const getMonitorDetails: UMElasticsearchQueryFn<
|
|||
|
||||
const monitorError: MonitorError | undefined = data?.error;
|
||||
const errorTimestamp: string | undefined = data?.['@timestamp'];
|
||||
|
||||
const monAlerts = await getMonitorAlerts(callES, dynamicSettings, alertsClient, monitorId);
|
||||
return {
|
||||
monitorId,
|
||||
error: monitorError,
|
||||
timestamp: errorTimestamp,
|
||||
alerts: monAlerts,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -142,7 +142,7 @@ export const getMonitorStatus: UMElasticsearchQueryFn<
|
|||
} while (afterKey !== undefined);
|
||||
|
||||
return monitors
|
||||
.filter((monitor: any) => monitor?.doc_count > numTimes)
|
||||
.filter((monitor: any) => monitor?.doc_count >= numTimes)
|
||||
.map(({ key, doc_count: count, fields }: any) => ({
|
||||
...key,
|
||||
count,
|
||||
|
|
|
@ -36,6 +36,9 @@ export const umDynamicSettings: SavedObjectsType = {
|
|||
certExpirationThreshold: {
|
||||
type: 'long',
|
||||
},
|
||||
defaultConnectors: {
|
||||
type: 'keyword',
|
||||
},
|
||||
*/
|
||||
},
|
||||
},
|
||||
|
|
|
@ -14,6 +14,7 @@ describe('dynamic settings', () => {
|
|||
certAgeThreshold: -1,
|
||||
certExpirationThreshold: 2,
|
||||
heartbeatIndices: 'foo',
|
||||
defaultConnectors: [],
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -28,6 +29,7 @@ describe('dynamic settings', () => {
|
|||
certAgeThreshold: 10.2,
|
||||
certExpirationThreshold: 2,
|
||||
heartbeatIndices: 'foo',
|
||||
defaultConnectors: [],
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -42,6 +44,7 @@ describe('dynamic settings', () => {
|
|||
certAgeThreshold: 2,
|
||||
certExpirationThreshold: -1,
|
||||
heartbeatIndices: 'foo',
|
||||
defaultConnectors: [],
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -56,6 +59,7 @@ describe('dynamic settings', () => {
|
|||
certAgeThreshold: 2,
|
||||
certExpirationThreshold: 1.23,
|
||||
heartbeatIndices: 'foo',
|
||||
defaultConnectors: [],
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -70,6 +74,7 @@ describe('dynamic settings', () => {
|
|||
certAgeThreshold: 2,
|
||||
certExpirationThreshold: 13,
|
||||
heartbeatIndices: 'foo',
|
||||
defaultConnectors: [],
|
||||
})
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
|
|
@ -54,6 +54,7 @@ export const createPostDynamicSettingsRoute: UMRestApiRouteFactory = (libs: UMSe
|
|||
heartbeatIndices: schema.string(),
|
||||
certAgeThreshold: schema.number(),
|
||||
certExpirationThreshold: schema.number(),
|
||||
defaultConnectors: schema.arrayOf(schema.string()),
|
||||
}),
|
||||
},
|
||||
writeAccess: true,
|
||||
|
|
|
@ -19,8 +19,11 @@ export const createGetMonitorDetailsRoute: UMRestApiRouteFactory = (libs: UMServ
|
|||
dateEnd: schema.maybe(schema.string()),
|
||||
}),
|
||||
},
|
||||
handler: async ({ callES, dynamicSettings }, _context, request, response): Promise<any> => {
|
||||
handler: async ({ callES, dynamicSettings }, context, request, response): Promise<any> => {
|
||||
const { monitorId, dateStart, dateEnd } = request.query;
|
||||
|
||||
const alertsClient = context.alerting?.getAlertsClient();
|
||||
|
||||
return response.ok({
|
||||
body: {
|
||||
...(await libs.requests.getMonitorDetails({
|
||||
|
@ -29,6 +32,7 @@ export const createGetMonitorDetailsRoute: UMRestApiRouteFactory = (libs: UMServ
|
|||
monitorId,
|
||||
dateStart,
|
||||
dateEnd,
|
||||
alertsClient,
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -22,6 +22,7 @@ export const createGetMonitorDurationRoute: UMRestApiRouteFactory = (libs: UMSer
|
|||
},
|
||||
handler: async ({ callES, dynamicSettings }, _context, request, response): Promise<any> => {
|
||||
const { monitorId, dateStart, dateEnd } = request.query;
|
||||
|
||||
return response.ok({
|
||||
body: {
|
||||
...(await libs.requests.getMonitorDurationChart({
|
||||
|
|
|
@ -27,6 +27,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
heartbeatIndices: 'myIndex1*',
|
||||
certAgeThreshold: 15,
|
||||
certExpirationThreshold: 5,
|
||||
defaultConnectors: [],
|
||||
};
|
||||
const postResponse = await supertest
|
||||
.post(`/api/uptime/dynamic_settings`)
|
||||
|
|
|
@ -12,6 +12,28 @@ import {
|
|||
|
||||
const ARCHIVE = 'uptime/full_heartbeat';
|
||||
|
||||
export const deleteUptimeSettingsObject = async (server: any) => {
|
||||
// delete the saved object
|
||||
try {
|
||||
await server.savedObjects.delete({
|
||||
type: settingsObjectType,
|
||||
id: settingsObjectId,
|
||||
});
|
||||
} catch (e) {
|
||||
// a 404 just means the doc is already missing
|
||||
if (e.response.status !== 404) {
|
||||
const { status, statusText, data, headers, config } = e.response;
|
||||
throw new Error(
|
||||
`error attempting to delete settings:\n${JSON.stringify(
|
||||
{ status, statusText, data, headers, config },
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default ({ loadTestFile, getService }: FtrProviderContext) => {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
@ -22,25 +44,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => {
|
|||
this.tags('ciGroup6');
|
||||
|
||||
beforeEach('delete settings', async () => {
|
||||
// delete the saved object
|
||||
try {
|
||||
await server.savedObjects.delete({
|
||||
type: settingsObjectType,
|
||||
id: settingsObjectId,
|
||||
});
|
||||
} catch (e) {
|
||||
// a 404 just means the doc is already missing
|
||||
if (e.response.status !== 404) {
|
||||
const { status, statusText, data, headers, config } = e.response;
|
||||
throw new Error(
|
||||
`error attempting to delete settings:\n${JSON.stringify(
|
||||
{ status, statusText, data, headers, config },
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
await deleteUptimeSettingsObject(server);
|
||||
});
|
||||
|
||||
describe('with generated data', () => {
|
||||
|
|
|
@ -63,6 +63,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
heartbeatIndices: 'new*',
|
||||
certAgeThreshold: 365,
|
||||
certExpirationThreshold: 30,
|
||||
defaultConnectors: [],
|
||||
};
|
||||
await settings.changeHeartbeatIndicesInput(newFieldValues.heartbeatIndices);
|
||||
await settings.apply();
|
||||
|
|
|
@ -39,6 +39,7 @@ export function UptimePageProvider({ getPageObjects, getService }: FtrProviderCo
|
|||
if (monitorIdToCheck) {
|
||||
await commonService.monitorIdExists(monitorIdToCheck);
|
||||
}
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
}
|
||||
|
||||
public async loadDataAndGoToMonitorPage(dateStart: string, dateEnd: string, monitorId: string) {
|
||||
|
|
|
@ -6,12 +6,14 @@
|
|||
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export function UptimeCommonProvider({ getService }: FtrProviderContext) {
|
||||
export function UptimeCommonProvider({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const testSubjects = getService('testSubjects');
|
||||
const browser = getService('browser');
|
||||
const retry = getService('retry');
|
||||
const find = getService('find');
|
||||
|
||||
const { header } = getPageObjects(['header']);
|
||||
|
||||
return {
|
||||
async assertExists(key: string) {
|
||||
if (!(await testSubjects.exists(key))) {
|
||||
|
@ -92,9 +94,11 @@ export function UptimeCommonProvider({ getService }: FtrProviderContext) {
|
|||
);
|
||||
},
|
||||
async waitUntilDataIsLoaded() {
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
return retry.tryForTime(60 * 1000, async () => {
|
||||
if (await testSubjects.exists('data-missing')) {
|
||||
await testSubjects.click('superDatePickerApplyTimeButton');
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
}
|
||||
await testSubjects.missingOrFail('data-missing');
|
||||
});
|
||||
|
|
|
@ -16,6 +16,7 @@ export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProv
|
|||
await retry.tryForTime(60 * 1000, async () => {
|
||||
if (await testSubjects.exists('uptimeSettingsToOverviewLink', { timeout: 0 })) {
|
||||
await testSubjects.click('uptimeSettingsToOverviewLink');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await testSubjects.existOrFail('uptimeOverviewPage', { timeout: 2000 });
|
||||
} else {
|
||||
await PageObjects.common.navigateToApp('uptime');
|
||||
|
|
|
@ -30,5 +30,9 @@ export function UptimeOverviewProvider({ getService }: FtrProviderContext) {
|
|||
await testSubjects.click('xpack.uptime.alertsPopover.toggleButton');
|
||||
return testSubjects.click('xpack.uptime.openAlertContextPanel');
|
||||
},
|
||||
|
||||
async displaysDefineConnector() {
|
||||
return await testSubjects.existOrFail('uptimeSettingsDefineConnector');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ export function UptimeSettingsProvider({ getService }: FtrProviderContext) {
|
|||
heartbeatIndices,
|
||||
certAgeThreshold: parseInt(age, 10),
|
||||
certExpirationThreshold: parseInt(expiration, 10),
|
||||
defaultConnectors: [],
|
||||
};
|
||||
},
|
||||
applyButtonIsDisabled: async () => {
|
||||
|
|
|
@ -23,6 +23,7 @@ export default ({ getService, loadTestFile }: FtrProviderContext) => {
|
|||
|
||||
loadTestFile(require.resolve('./alert_flyout'));
|
||||
loadTestFile(require.resolve('./anomaly_alert'));
|
||||
loadTestFile(require.resolve('./simple_down_alert'));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { deleteUptimeSettingsObject } from '../../../functional/apps/uptime';
|
||||
|
||||
export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||
describe('uptime simple status alert', () => {
|
||||
const pageObjects = getPageObjects(['common', 'header', 'uptime']);
|
||||
const server = getService('kibanaServer');
|
||||
const uptimeService = getService('uptime');
|
||||
const retry = getService('retry');
|
||||
const supertest = getService('supertest');
|
||||
|
||||
const testSubjects = getService('testSubjects');
|
||||
|
||||
const monitorId = '0000-intermittent';
|
||||
|
||||
const uptime = getService('uptime');
|
||||
|
||||
const DEFAULT_DATE_START = 'Sep 10, 2019 @ 12:40:08.078';
|
||||
const DEFAULT_DATE_END = 'Sep 11, 2019 @ 19:40:08.078';
|
||||
|
||||
before(async () => {
|
||||
// delete the saved object
|
||||
await deleteUptimeSettingsObject(server);
|
||||
|
||||
await uptime.navigation.goToUptime();
|
||||
|
||||
await pageObjects.uptime.goToUptimeOverviewAndLoadData(DEFAULT_DATE_START, DEFAULT_DATE_END);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
});
|
||||
|
||||
it('displays to define default connector', async () => {
|
||||
await testSubjects.click('uptimeDisplayDefineConnector');
|
||||
await testSubjects.existOrFail('uptimeSettingsDefineConnector');
|
||||
});
|
||||
|
||||
it('go to settings to define connector', async () => {
|
||||
await testSubjects.click('uptimeSettingsLink');
|
||||
await uptimeService.common.waitUntilDataIsLoaded();
|
||||
await testSubjects.existOrFail('comboBoxInput');
|
||||
});
|
||||
|
||||
it('define default connector', async () => {
|
||||
await testSubjects.click('comboBoxInput');
|
||||
await testSubjects.click('Slack#xyztest');
|
||||
await testSubjects.click('apply-settings-button');
|
||||
await uptimeService.navigation.goToUptime();
|
||||
});
|
||||
|
||||
it('enable simple status alert', async () => {
|
||||
await pageObjects.uptime.goToUptimeOverviewAndLoadData(DEFAULT_DATE_START, DEFAULT_DATE_END);
|
||||
await testSubjects.click('uptimeEnableSimpleDownAlert' + monitorId);
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
});
|
||||
|
||||
it('displays relevant alert in list drawer', async () => {
|
||||
await testSubjects.click(`xpack.uptime.monitorList.${monitorId}.expandMonitorDetail`);
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
await testSubjects.existOrFail('uptimeMonitorListDrawerAlert0');
|
||||
});
|
||||
|
||||
it('has created a valid simple alert with expected parameters', async () => {
|
||||
let alert: any;
|
||||
await retry.tryForTime(15000, async () => {
|
||||
const apiResponse = await supertest.get(`/api/alerts/_find?search=Simple status alert`);
|
||||
const alertsFromThisTest = apiResponse.body.data.filter(({ params }: { params: any }) =>
|
||||
params.search.includes(monitorId)
|
||||
);
|
||||
expect(alertsFromThisTest).to.have.length(1);
|
||||
alert = alertsFromThisTest[0];
|
||||
});
|
||||
|
||||
const { actions, alertTypeId, consumer, id, tags } = alert ?? {};
|
||||
try {
|
||||
expect(actions).to.eql([
|
||||
{
|
||||
actionTypeId: '.slack',
|
||||
group: 'xpack.uptime.alerts.actionGroups.monitorStatus',
|
||||
id: 'my-slack1',
|
||||
params: {
|
||||
message:
|
||||
'Monitor {{state.monitorName}} with url {{{state.monitorUrl}}} is {{state.statusMessage}} from {{state.observerLocation}}. The latest error message is {{{state.latestErrorMessage}}}',
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(alertTypeId).to.eql('xpack.uptime.alerts.monitorStatus');
|
||||
expect(consumer).to.eql('uptime');
|
||||
expect(tags).to.eql(['UPTIME_AUTO']);
|
||||
} catch (e) {
|
||||
await supertest.delete(`/api/alerts/alert/${id}`).set('kbn-xsrf', 'true').expect(204);
|
||||
}
|
||||
});
|
||||
|
||||
it('disable simple status alert', async () => {
|
||||
await testSubjects.click('uptimeDisableSimpleDownAlert' + monitorId);
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
await testSubjects.existOrFail('uptimeEnableSimpleDownAlert' + monitorId);
|
||||
});
|
||||
});
|
||||
};
|
Loading…
Reference in a new issue