[Uptime] allow an administrator to enable and disable monitor management (#128223)

* uptime - allow an administrator to enable monitor management

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: shahzad31 <shahzad.muhammad@elastic.co>
This commit is contained in:
Dominique Clarke 2022-03-29 14:47:23 -04:00 committed by GitHub
parent 490cd51abc
commit 038f091311
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 1633 additions and 250 deletions

View file

@ -40,6 +40,7 @@ export enum API_URLS {
INDEX_TEMPLATES = '/internal/uptime/service/index_templates',
SERVICE_LOCATIONS = '/internal/uptime/service/locations',
SYNTHETICS_MONITORS = '/internal/uptime/service/monitors',
SYNTHETICS_ENABLEMENT = '/internal/uptime/service/enablement',
RUN_ONCE_MONITOR = '/internal/uptime/service/monitors/run_once',
TRIGGER_MONITOR = '/internal/uptime/service/monitors/trigger',
SERVICE_ALLOWED = '/internal/uptime/service/allowed',

View file

@ -7,7 +7,7 @@
import * as t from 'io-ts';
export const FetchMonitorManagementListQueryArgsType = t.partial({
export const FetchMonitorManagementListQueryArgsCodec = t.partial({
page: t.number,
perPage: t.number,
sortField: t.string,
@ -17,5 +17,15 @@ export const FetchMonitorManagementListQueryArgsType = t.partial({
});
export type FetchMonitorManagementListQueryArgs = t.TypeOf<
typeof FetchMonitorManagementListQueryArgsType
typeof FetchMonitorManagementListQueryArgsCodec
>;
export const MonitorManagementEnablementResultCodec = t.type({
isEnabled: t.boolean,
canEnable: t.boolean,
areApiKeysEnabled: t.boolean,
});
export type MonitorManagementEnablementResult = t.TypeOf<
typeof MonitorManagementEnablementResultCodec
>;

View file

@ -13,4 +13,5 @@ export * from './read_only_user';
export * from './monitor_details.journey';
export * from './monitor_name.journey';
export * from './monitor_management.journey';
export * from './monitor_management_enablement.journey';
export * from './monitor_details';

View file

@ -96,18 +96,14 @@ const createMonitorJourney = ({
async ({ page, params }: { page: Page; params: any }) => {
const uptime = monitorManagementPageProvider({ page, kibanaUrl: params.kibanaUrl });
const isRemote = process.env.SYNTHETICS_REMOTE_ENABLED;
const deleteMonitor = async () => {
await uptime.navigateToMonitorManagement();
const isSuccessful = await uptime.deleteMonitor();
expect(isSuccessful).toBeTruthy();
};
before(async () => {
await uptime.waitForLoadingToFinish();
});
after(async () => {
await deleteMonitor();
await uptime.navigateToMonitorManagement();
await uptime.enableMonitorManagement(false);
});
step('Go to monitor-management', async () => {
@ -123,13 +119,14 @@ const createMonitorJourney = ({
});
step(`create ${monitorType} monitor`, async () => {
await uptime.enableMonitorManagement();
await uptime.clickAddMonitor();
await uptime.createMonitor({ monitorConfig, monitorType });
const isSuccessful = await uptime.confirmAndSave();
expect(isSuccessful).toBeTruthy();
});
step(`view ${monitorType} details in monitor management UI`, async () => {
step(`view ${monitorType} details in Monitor Management UI`, async () => {
await uptime.navigateToMonitorManagement();
const hasFailure = await uptime.findMonitorConfiguration(monitorDetails);
expect(hasFailure).toBeFalsy();
@ -141,6 +138,12 @@ const createMonitorJourney = ({
await page.waitForSelector(`text=${monitorName}`, { timeout: 160 * 1000 });
});
}
step('delete monitor', async () => {
await uptime.navigateToMonitorManagement();
const isSuccessful = await uptime.deleteMonitors();
expect(isSuccessful).toBeTruthy();
});
}
);
};
@ -167,6 +170,10 @@ journey('Monitor Management breadcrumbs', async ({ page, params }: { page: Page;
await uptime.waitForLoadingToFinish();
});
after(async () => {
await uptime.enableMonitorManagement(false);
});
step('Go to monitor-management', async () => {
await uptime.navigateToMonitorManagement();
});
@ -177,13 +184,14 @@ journey('Monitor Management breadcrumbs', async ({ page, params }: { page: Page;
step('Check breadcrumb', async () => {
const lastBreadcrumb = await (await uptime.findByTestSubj('"breadcrumb last"')).textContent();
expect(lastBreadcrumb).toEqual('Monitor management');
expect(lastBreadcrumb).toEqual('Monitor Management');
});
step('check breadcrumbs', async () => {
await uptime.enableMonitorManagement();
await uptime.clickAddMonitor();
const breadcrumbs = await page.$$('[data-test-subj="breadcrumb"]');
expect(await breadcrumbs[1].textContent()).toEqual('Monitor management');
expect(await breadcrumbs[1].textContent()).toEqual('Monitor Management');
const lastBreadcrumb = await (await uptime.findByTestSubj('"breadcrumb last"')).textContent();
expect(lastBreadcrumb).toEqual('Add monitor');
});
@ -204,14 +212,14 @@ journey('Monitor Management breadcrumbs', async ({ page, params }: { page: Page;
// breadcrumb is available before edit page is loaded, make sure its edit view
await page.waitForSelector(byTestId('monitorManagementMonitorName'), { timeout: 60 * 1000 });
const breadcrumbs = await page.$$('[data-test-subj=breadcrumb]');
expect(await breadcrumbs[1].textContent()).toEqual('Monitor management');
expect(await breadcrumbs[1].textContent()).toEqual('Monitor Management');
const lastBreadcrumb = await (await uptime.findByTestSubj('"breadcrumb last"')).textContent();
expect(lastBreadcrumb).toEqual('Edit monitor');
});
step('delete monitor', async () => {
await uptime.navigateToMonitorManagement();
const isSuccessful = await uptime.deleteMonitor();
const isSuccessful = await uptime.deleteMonitors();
expect(isSuccessful).toBeTruthy();
});
});

View file

@ -0,0 +1,76 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { journey, step, expect, before, after, Page } from '@elastic/synthetics';
import { monitorManagementPageProvider } from '../page_objects/monitor_management';
journey(
'Monitor Management-enablement-superuser',
async ({ page, params }: { page: Page; params: any }) => {
const uptime = monitorManagementPageProvider({ page, kibanaUrl: params.kibanaUrl });
before(async () => {
await uptime.waitForLoadingToFinish();
});
after(async () => {
await uptime.enableMonitorManagement(false);
});
step('Go to monitor-management', async () => {
await uptime.navigateToMonitorManagement();
});
step('login to Kibana', async () => {
await uptime.loginToKibana();
const invalid = await page.locator(
`text=Username or password is incorrect. Please try again.`
);
expect(await invalid.isVisible()).toBeFalsy();
});
step('check add monitor button', async () => {
expect(await uptime.checkIsEnabled()).toBe(false);
});
step('enable Monitor Management', async () => {
await uptime.enableMonitorManagement();
expect(await uptime.checkIsEnabled()).toBe(true);
});
}
);
journey(
'MonitorManagement-enablement-obs-admin',
async ({ page, params }: { page: Page; params: any }) => {
const uptime = monitorManagementPageProvider({ page, kibanaUrl: params.kibanaUrl });
before(async () => {
await uptime.waitForLoadingToFinish();
});
step('Go to monitor-management', async () => {
await uptime.navigateToMonitorManagement();
});
step('login to Kibana', async () => {
await uptime.loginToKibana('obs_admin_user', 'changeme');
const invalid = await page.locator(
`text=Username or password is incorrect. Please try again.`
);
expect(await invalid.isVisible()).toBeFalsy();
});
step('check add monitor button', async () => {
expect(await uptime.checkIsEnabled()).toBe(false);
});
step('check that enabled toggle does not appear', async () => {
expect(await page.$(`[data-test-subj=syntheticsEnableSwitch]`)).toBeFalsy();
});
}
);

View file

@ -12,7 +12,7 @@
* 2.0.
*/
import { journey, step, expect, after, before, Page } from '@elastic/synthetics';
import { journey, step, expect, before, Page } from '@elastic/synthetics';
import { monitorManagementPageProvider } from '../page_objects/monitor_management';
import { byTestId } from './utils';
@ -23,11 +23,6 @@ journey(`MonitorName`, async ({ page, params }: { page: Page; params: any }) =>
await uptime.waitForLoadingToFinish();
});
after(async () => {
await uptime.navigateToMonitorManagement();
await uptime.deleteMonitor();
});
step('Go to monitor-management', async () => {
await uptime.navigateToMonitorManagement();
});
@ -39,6 +34,7 @@ journey(`MonitorName`, async ({ page, params }: { page: Page; params: any }) =>
});
step(`shows error if name already exists`, async () => {
await uptime.enableMonitorManagement();
await uptime.clickAddMonitor();
await uptime.createBasicMonitorDetails({
name: 'Test monitor',
@ -61,4 +57,10 @@ journey(`MonitorName`, async ({ page, params }: { page: Page; params: any }) =>
expect(await page.isEnabled(byTestId('monitorTestNowRunBtn'))).toBeTruthy();
});
step('delete monitor', async () => {
await uptime.navigateToMonitorManagement();
await uptime.deleteMonitors();
await uptime.enableMonitorManagement(false);
});
});

View file

@ -27,7 +27,7 @@ journey(
});
step('Adding monitor is disabled', async () => {
expect(await page.isEnabled(byTestId('addMonitorBtn'))).toBeFalsy();
expect(await page.isEnabled(byTestId('syntheticsAddMonitorBtn'))).toBeFalsy();
});
}
);

View file

@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { Page } from '@elastic/synthetics';
import { DataStream } from '../../common/runtime_types/monitor_management';
import { getQuerystring } from '../journeys/utils';
@ -39,6 +38,60 @@ export function monitorManagementPageProvider({
await page.goto(monitorManagement, {
waitUntil: 'networkidle',
});
await this.waitForMonitorManagementLoadingToFinish();
},
async waitForMonitorManagementLoadingToFinish() {
while (true) {
if ((await page.$(this.byTestId('uptimeLoader'))) === null) break;
await page.waitForTimeout(5 * 1000);
}
},
async enableMonitorManagement(shouldEnable: boolean = true) {
const isEnabled = await this.checkIsEnabled();
if (isEnabled === shouldEnable) {
return;
}
const [toggle, button] = await Promise.all([
page.$(this.byTestId('syntheticsEnableSwitch')),
page.$(this.byTestId('syntheticsEnableButton')),
]);
if (toggle === null && button === null) {
return null;
}
if (toggle) {
if (isEnabled !== shouldEnable) {
await toggle.click();
}
} else {
await button?.click();
}
if (shouldEnable) {
await this.findByText('Monitor Management enabled successfully.');
} else {
await this.findByText('Monitor Management disabled successfully.');
}
},
async getEnableToggle() {
return await this.findByTestSubj('syntheticsEnableSwitch');
},
async getEnableButton() {
return await this.findByTestSubj('syntheticsEnableSwitch');
},
async getAddMonitorButton() {
return await this.findByTestSubj('syntheticsAddMonitorBtn');
},
async checkIsEnabled() {
await page.waitForTimeout(5 * 1000);
const addMonitorBtn = await this.getAddMonitorButton();
const isDisabled = await addMonitorBtn.isDisabled();
return !isDisabled;
},
async navigateToAddMonitor() {
@ -57,10 +110,18 @@ export function monitorManagementPageProvider({
await page.click('text=Add monitor');
},
async deleteMonitor() {
await this.clickByTestSubj('monitorManagementDeleteMonitor');
await this.clickByTestSubj('confirmModalConfirmButton');
return await this.findByTestSubj('uptimeDeleteMonitorSuccess');
async deleteMonitors() {
let isSuccessful: boolean = false;
await page.waitForSelector('[data-test-subj="monitorManagementDeleteMonitor"]');
while (true) {
if ((await page.$(this.byTestId('monitorManagementDeleteMonitor'))) === null) break;
await page.click(this.byTestId('monitorManagementDeleteMonitor'), { delay: 800 });
await page.waitForSelector('[data-test-subj="confirmModalTitleText"]');
await this.clickByTestSubj('confirmModalConfirmButton');
isSuccessful = Boolean(await this.findByTestSubj('uptimeDeleteMonitorSuccess'));
await page.waitForTimeout(5 * 1000);
}
return isSuccessful;
},
async editMonitor() {

View file

@ -1,3 +1,2 @@
{"attributes":{"__ui":{"is_tls_enabled":false,"is_zip_url_tls_enabled":false},"check.request.method":"GET","check.response.status":[],"enabled":true,"locations":[{"geo":{"lat":41.25,"lon":-95.86},"id":"us_central","label":"US Central","url":"https://us-central.synthetics.elastic.dev"}],"max_redirects":"0","name":"Test Monitor","proxy_url":"","response.include_body":"on_error","response.include_headers":true,"schedule":{"number":"3","unit":"m"},"service.name":"","tags":[],"timeout":"16","type":"http","urls":"https://www.google.com", "secrets": "{}"},"coreMigrationVersion":"8.1.0","id":"832b9980-7fba-11ec-b360-25a79ce3f496","references":[],"sort":[1643319958480,20371],"type":"synthetics-monitor","updated_at":"2022-01-27T21:45:58.480Z","version":"WzExOTg3ODYsMl0="}
{"attributes":{"__ui":{"is_tls_enabled":false,"is_zip_url_tls_enabled":false},"check.request.method":"GET","check.response.status":[],"enabled":true,"locations":[{"geo":{"lat":41.25,"lon":-95.86},"id":"us_central","label":"US Central","url":"https://us-central.synthetics.elastic.dev"}],"max_redirects":"0","name":"Test Monito","proxy_url":"","response.include_body":"on_error","response.include_headers":true,"schedule":{"number":"3","unit":"m"},"service.name":"","tags":[],"timeout":"16","type":"http","urls":"https://www.google.com", "secrets": "{}"},"coreMigrationVersion":"8.1.0","id":"b28380d0-7fba-11ec-b360-25a79ce3f496","references":[],"sort":[1643320037906,20374],"type":"synthetics-monitor","updated_at":"2022-01-27T21:47:17.906Z","version":"WzExOTg4MDAsMl0="}
{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":2,"missingRefCount":0,"missingReferences":[]}
{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":2,"missingRefCount":0,"missingReferences":[]}

View file

@ -78,7 +78,7 @@ export function ActionMenuContent(): React.ReactElement {
<EuiHeaderLinks gutterSize="xs">
<EuiHeaderLink
aria-label={i18n.translate('xpack.uptime.page_header.manageLink.label', {
defaultMessage: 'Navigate to the Uptime monitor management page',
defaultMessage: 'Navigate to the Uptime Monitor Management page',
})}
color="text"
data-test-subj="management-page-link"
@ -88,7 +88,7 @@ export function ActionMenuContent(): React.ReactElement {
>
<FormattedMessage
id="xpack.uptime.page_header.manageLink"
defaultMessage="Monitor management"
defaultMessage="Monitor Management"
/>
</EuiHeaderLink>

View file

@ -5,39 +5,203 @@
* 2.0.
*/
import React from 'react';
import React, { useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButton, EuiFlexItem } from '@elastic/eui';
import { EuiButton, EuiFlexItem, EuiFlexGroup, EuiToolTip, EuiSwitch } from '@elastic/eui';
import { useHistory } from 'react-router-dom';
import { kibanaService } from '../../state/kibana_service';
import { MONITOR_ADD_ROUTE } from '../../../common/constants';
import { useEnablement } from './hooks/use_enablement';
import { useSyntheticsServiceAllowed } from './hooks/use_service_allowed';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
export const AddMonitorBtn = () => {
const history = useHistory();
const [isEnabling, setIsEnabling] = useState(false);
const [isDisabling, setIsDisabling] = useState(false);
const {
error,
loading: enablementLoading,
enablement,
disableSynthetics,
enableSynthetics,
totalMonitors,
} = useEnablement();
const { isEnabled, canEnable, areApiKeysEnabled } = enablement || {};
const { isAllowed, loading } = useSyntheticsServiceAllowed();
useEffect(() => {
if (isEnabling && isEnabled) {
setIsEnabling(false);
kibanaService.toasts.addSuccess({
title: SYNTHETICS_ENABLE_SUCCESS,
toastLifeTimeMs: 3000,
});
} else if (isDisabling && !isEnabled) {
setIsDisabling(false);
kibanaService.toasts.addSuccess({
title: SYNTHETICS_DISABLE_SUCCESS,
toastLifeTimeMs: 3000,
});
} else if (isEnabling && error) {
setIsEnabling(false);
kibanaService.toasts.addDanger({
title: SYNTHETICS_ENABLE_FAILURE,
toastLifeTimeMs: 3000,
});
} else if (isDisabling && error) {
kibanaService.toasts.addDanger({
title: SYNTHETICS_DISABLE_FAILURE,
toastLifeTimeMs: 3000,
});
}
}, [isEnabled, isEnabling, isDisabling, error]);
const handleSwitch = () => {
if (isEnabled) {
setIsDisabling(true);
disableSynthetics();
} else {
setIsEnabling(true);
enableSynthetics();
}
};
const getShowSwitch = () => {
if (isEnabled) {
return canEnable;
} else if (!isEnabled) {
return canEnable && (totalMonitors || 0) > 0;
}
};
const getSwitchToolTipContent = () => {
if (!isEnabled) {
return SYNTHETICS_ENABLE_TOOL_TIP_MESSAGE;
} else if (isEnabled) {
return SYNTHETICS_DISABLE_TOOL_TIP_MESSAGE;
} else if (!areApiKeysEnabled) {
return API_KEYS_DISABLED_TOOL_TIP_MESSAGE;
} else {
return '';
}
};
const { isAllowed, loading: allowedLoading } = useSyntheticsServiceAllowed();
const loading = allowedLoading || enablementLoading;
const canSave: boolean = !!useKibana().services?.application?.capabilities.uptime.save;
return (
<EuiFlexItem style={{ alignItems: 'flex-end' }} grow={false} data-test-subj="addMonitorButton">
<EuiButton
fill
isLoading={loading}
isDisabled={!canSave || !isAllowed}
iconType="plus"
data-test-subj="addMonitorBtn"
href={history.createHref({
pathname: MONITOR_ADD_ROUTE,
})}
>
{ADD_MONITOR_LABEL}
</EuiButton>
</EuiFlexItem>
<EuiFlexGroup alignItems="center">
<EuiFlexItem style={{ alignItems: 'flex-end' }} grow={false}>
{getShowSwitch() && !loading && (
<EuiToolTip content={getSwitchToolTipContent()}>
<EuiSwitch
checked={Boolean(isEnabled)}
label={SYNTHETICS_ENABLE_LABEL}
disabled={loading || !areApiKeysEnabled}
onChange={() => handleSwitch()}
data-test-subj="syntheticsEnableSwitch"
/>
</EuiToolTip>
)}
{getShowSwitch() && loading && (
<EuiSwitch
checked={Boolean(isEnabled)}
label={SYNTHETICS_ENABLE_LABEL}
disabled={true}
onChange={() => {}}
/>
)}
</EuiFlexItem>
<EuiFlexItem style={{ alignItems: 'flex-end' }} grow={false}>
<EuiToolTip content={!isEnabled && !canEnable ? SYNTHETICS_DISABLED_MESSAGE : ''}>
<EuiButton
isLoading={loading}
fill
isDisabled={!canSave || !isEnabled || !isAllowed}
iconType="plus"
data-test-subj="syntheticsAddMonitorBtn"
href={history.createHref({
pathname: MONITOR_ADD_ROUTE,
})}
>
{ADD_MONITOR_LABEL}
</EuiButton>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
);
};
const ADD_MONITOR_LABEL = i18n.translate('xpack.uptime.monitorManagement.addMonitorLabel', {
defaultMessage: 'Add monitor',
});
const SYNTHETICS_ENABLE_LABEL = i18n.translate(
'xpack.uptime.monitorManagement.syntheticsEnableLabel',
{
defaultMessage: 'Enable',
}
);
const SYNTHETICS_ENABLE_FAILURE = i18n.translate(
'xpack.uptime.monitorManagement.syntheticsEnabledFailure',
{
defaultMessage: 'Monitor Management was not able to be enabled. Please contact support.',
}
);
const SYNTHETICS_DISABLE_FAILURE = i18n.translate(
'xpack.uptime.monitorManagement.syntheticsDisabledFailure',
{
defaultMessage: 'Monitor Management was not able to be disabled. Please contact support.',
}
);
const SYNTHETICS_ENABLE_SUCCESS = i18n.translate(
'xpack.uptime.monitorManagement.syntheticsEnableSuccess',
{
defaultMessage: 'Monitor Management enabled successfully.',
}
);
const SYNTHETICS_DISABLE_SUCCESS = i18n.translate(
'xpack.uptime.monitorManagement.syntheticsDisabledSuccess',
{
defaultMessage: 'Monitor Management disabled successfully.',
}
);
const SYNTHETICS_DISABLED_MESSAGE = i18n.translate(
'xpack.uptime.monitorManagement.syntheticsDisabled',
{
defaultMessage:
'Monitor Management is currently disabled. Please contact an administrator to enable Monitor Management.',
}
);
const SYNTHETICS_ENABLE_TOOL_TIP_MESSAGE = i18n.translate(
'xpack.uptime.monitorManagement.syntheticsEnableToolTip',
{
defaultMessage:
'Enable Monitor Management to create lightweight and real-browser monitors from locations around the world.',
}
);
const SYNTHETICS_DISABLE_TOOL_TIP_MESSAGE = i18n.translate(
'xpack.uptime.monitorManagement.syntheticsDisableToolTip',
{
defaultMessage:
'Disabling Monitor Management with immediately stop the execution of monitors in all test locations and prevent the creation of new monitors.',
}
);
const API_KEYS_DISABLED_TOOL_TIP_MESSAGE = i18n.translate(
'xpack.uptime.monitorManagement.apiKeysDisabledToolTip',
{
defaultMessage:
'API Keys are disabled for this cluster. Monitor Management requires the use of API keys to write back to your Elasticsearch cluster. To enable API keys, please contact an administrator.',
}
);

View file

@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
export const SYNTHETICS_ENABLE_FAILURE = i18n.translate(
'xpack.uptime.monitorManagement.syntheticsEnabledFailure',
{
defaultMessage: 'Monitor Management was not able to be enabled. Please contact support.',
}
);
export const SYNTHETICS_DISABLE_FAILURE = i18n.translate(
'xpack.uptime.monitorManagement.syntheticsDisabledFailure',
{
defaultMessage: 'Monitor Management was not able to be disabled. Please contact support.',
}
);
export const SYNTHETICS_ENABLE_SUCCESS = i18n.translate(
'xpack.uptime.monitorManagement.syntheticsEnableSuccess',
{
defaultMessage: 'Monitor Management enabled successfully.',
}
);
export const SYNTHETICS_DISABLE_SUCCESS = i18n.translate(
'xpack.uptime.monitorManagement.syntheticsDisabledSuccess',
{
defaultMessage: 'Monitor Management disabled successfully.',
}
);

View file

@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useEffect, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { monitorManagementListSelector } from '../../../state/selectors';
import {
getSyntheticsEnablement,
disableSynthetics,
enableSynthetics,
} from '../../../state/actions';
export function useEnablement() {
const dispatch = useDispatch();
const {
loading: { enablement: loading },
error: { enablement: error },
enablement,
list: { total },
} = useSelector(monitorManagementListSelector);
useEffect(() => {
if (!enablement) {
dispatch(getSyntheticsEnablement());
}
}, [dispatch, enablement]);
return {
enablement: {
areApiKeysEnabled: enablement?.areApiKeysEnabled,
canEnable: enablement?.canEnable,
isEnabled: enablement?.isEnabled,
},
error,
loading,
totalMonitors: total,
enableSynthetics: useCallback(() => dispatch(enableSynthetics()), [dispatch]),
disableSynthetics: useCallback(() => dispatch(disableSynthetics()), [dispatch]),
};
}

View file

@ -68,9 +68,10 @@ describe('useInlineErrors', function () {
[
'heartbeat-8*,heartbeat-7*,synthetics-*',
{
error: { monitorList: null, serviceLocations: null },
error: { monitorList: null, serviceLocations: null, enablement: null },
enablement: null,
list: { monitors: [], page: 1, perPage: 10, total: null },
loading: { monitorList: false, serviceLocations: false },
loading: { monitorList: false, serviceLocations: false, enablement: false },
locations: [],
syntheticsService: {
loading: false,

View file

@ -67,9 +67,10 @@ describe('useInlineErrorsCount', function () {
[
'heartbeat-8*,heartbeat-7*,synthetics-*',
{
error: { monitorList: null, serviceLocations: null },
error: { monitorList: null, serviceLocations: null, enablement: null },
list: { monitors: [], page: 1, perPage: 10, total: null },
loading: { monitorList: false, serviceLocations: false },
enablement: null,
loading: { monitorList: false, serviceLocations: false, enablement: false },
locations: [],
syntheticsService: {
loading: false,

View file

@ -42,13 +42,16 @@ describe('useExpViewTimeRange', function () {
monitors: [],
},
locations: [],
enablement: null,
error: {
serviceLocations: error,
monitorList: null,
enablement: null,
},
loading: {
monitorList: false,
serviceLocations: loading,
enablement: false,
},
syntheticsService: {
loading: false,

View file

@ -44,6 +44,7 @@ export const Loader = ({
color="subdued"
icon={<EuiLoadingLogo logo="logoKibana" size="xl" />}
title={<h2>{loadingTitle}</h2>}
data-test-subj="uptimeLoader"
/>
) : null}
</>

View file

@ -0,0 +1,133 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useState, useEffect, useRef } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiEmptyPrompt, EuiButton, EuiTitle, EuiLink } from '@elastic/eui';
import { useEnablement } from '..//hooks/use_enablement';
import { kibanaService } from '../../../state/kibana_service';
import { SYNTHETICS_ENABLE_SUCCESS, SYNTHETICS_DISABLE_SUCCESS } from '../content';
export const EnablementEmptyState = ({ focusButton }: { focusButton: boolean }) => {
const { error, enablement, enableSynthetics, loading } = useEnablement();
const [isEnabling, setIsEnabling] = useState(false);
const { isEnabled, canEnable } = enablement;
const buttonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (isEnabling && isEnabled) {
setIsEnabling(false);
kibanaService.toasts.addSuccess({
title: SYNTHETICS_ENABLE_SUCCESS,
toastLifeTimeMs: 3000,
});
} else if (isEnabling && error) {
setIsEnabling(false);
kibanaService.toasts.addSuccess({
title: SYNTHETICS_DISABLE_SUCCESS,
toastLifeTimeMs: 3000,
});
}
}, [isEnabled, isEnabling, error]);
const handleEnableSynthetics = () => {
enableSynthetics();
setIsEnabling(true);
};
useEffect(() => {
if (focusButton) {
buttonRef.current?.focus();
}
}, [focusButton]);
return !isEnabled && !loading ? (
<EuiEmptyPrompt
title={
<h2>
{canEnable ? MONITOR_MANAGEMENT_ENABLEMENT_LABEL : MONITOR_MANAGEMENT_DISABLED_LABEL}
</h2>
}
body={
<p>
{canEnable ? MONITOR_MANAGEMENT_ENABLEMENT_MESSAGE : MONITOR_MANAGEMENT_DISABLED_MESSAGE}
</p>
}
actions={
canEnable ? (
<EuiButton
color="primary"
fill
onClick={handleEnableSynthetics}
data-test-subj="syntheticsEnableButton"
buttonRef={buttonRef}
>
{MONITOR_MANAGEMENT_ENABLEMENT_BTN_LABEL}
</EuiButton>
) : null
}
footer={
<>
<EuiTitle size="xxs">
<h3>{LEARN_MORE_LABEL}</h3>
</EuiTitle>
<EuiLink href="#" target="_blank">
{DOCS_LABEL}
</EuiLink>
</>
}
/>
) : null;
};
const MONITOR_MANAGEMENT_ENABLEMENT_LABEL = i18n.translate(
'xpack.uptime.monitorManagement.emptyState.enablement.enabled.title',
{
defaultMessage: 'Enable Monitor Management',
}
);
const MONITOR_MANAGEMENT_DISABLED_LABEL = i18n.translate(
'xpack.uptime.monitorManagement.emptyState.enablement.disabled.title',
{
defaultMessage: 'Monitor Management is disabled',
}
);
const MONITOR_MANAGEMENT_ENABLEMENT_MESSAGE = i18n.translate(
'xpack.uptime.monitorManagement.emptyState.enablement',
{
defaultMessage:
'Enable Monitor Management to run lightweight checks and real-browser monitors from hosted testing locations around the world. Enabling Monitor Management will generate an API key to allow the Synthetics Service to write back to your Elasticsearch cluster.',
}
);
const MONITOR_MANAGEMENT_DISABLED_MESSAGE = i18n.translate(
'xpack.uptime.monitorManagement.emptyState.enablement.disabledDescription',
{
defaultMessage:
'Monitor Management is currently disabled. Monitor Management allows you to run lightweight checks and real-browser monitors from hosted testing locations around the world. To enable Monitor Management, please contact an administrator.',
}
);
const MONITOR_MANAGEMENT_ENABLEMENT_BTN_LABEL = i18n.translate(
'xpack.uptime.monitorManagement.emptyState.enablement.title',
{
defaultMessage: 'Enable',
}
);
const DOCS_LABEL = i18n.translate('xpack.uptime.monitorManagement.emptyState.enablement.doc', {
defaultMessage: 'Read the docs',
});
const LEARN_MORE_LABEL = i18n.translate(
'xpack.uptime.monitorManagement.emptyState.enablement.learnMore',
{
defaultMessage: 'Want to learn more?',
}
);

View file

@ -49,8 +49,9 @@ export const InvalidMonitors = ({
perPage: pageState.pageSize,
total: invalidTotal ?? 0,
},
error: { monitorList: null, serviceLocations: null },
loading: { monitorList: summariesLoading, serviceLocations: false },
enablement: null,
error: { monitorList: null, serviceLocations: null, enablement: null },
loading: { monitorList: summariesLoading, serviceLocations: false, enablement: false },
locations: monitorList.locations,
syntheticsService: monitorList.syntheticsService,
throttling: DEFAULT_THROTTLING,

View file

@ -50,13 +50,16 @@ describe('<MonitorManagementList />', () => {
monitors,
},
locations: [],
enablement: null,
error: {
serviceLocations: null,
monitorList: null,
enablement: null,
},
loading: {
monitorList: true,
serviceLocations: false,
enablement: false,
},
syntheticsService: {
loading: false,

View file

@ -207,7 +207,7 @@ export const MonitorManagementList = ({
<EuiSpacer size="m" />
<EuiBasicTable
aria-label={i18n.translate('xpack.uptime.monitorManagement.monitorList.title', {
defaultMessage: 'Monitor management list',
defaultMessage: 'Monitor Management list',
})}
error={error?.message}
loading={loading}

View file

@ -0,0 +1,119 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useEffect, useReducer, useCallback, Reducer } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { useTrackPageview } from '../../../../../observability/public';
import { ConfigKey } from '../../../../common/runtime_types';
import { getMonitors } from '../../../state/actions';
import { monitorManagementListSelector } from '../../../state/selectors';
import { MonitorManagementListPageState } from './monitor_list';
import { useInlineErrors } from '../hooks/use_inline_errors';
import { MonitorListTabs } from './list_tabs';
import { AllMonitors } from './all_monitors';
import { InvalidMonitors } from './invalid_monitors';
import { useInvalidMonitors } from '../hooks/use_invalid_monitors';
export const MonitorListContainer: React.FC = () => {
const [pageState, dispatchPageAction] = useReducer<typeof monitorManagementPageReducer>(
monitorManagementPageReducer,
{
pageIndex: 1, // saved objects page index is base 1
pageSize: 10,
sortOrder: 'asc',
sortField: ConfigKey.NAME,
}
);
const onPageStateChange = useCallback(
(state) => {
dispatchPageAction({ type: 'update', payload: state });
},
[dispatchPageAction]
);
const onUpdate = useCallback(() => {
dispatchPageAction({ type: 'refresh' });
}, [dispatchPageAction]);
useTrackPageview({ app: 'uptime', path: 'manage-monitors' });
useTrackPageview({ app: 'uptime', path: 'manage-monitors', delay: 15000 });
const dispatch = useDispatch();
const monitorList = useSelector(monitorManagementListSelector);
const { pageIndex, pageSize, sortField, sortOrder } = pageState as MonitorManagementListPageState;
const { type: viewType } = useParams<{ type: 'all' | 'invalid' }>();
const { errorSummaries, loading, count } = useInlineErrors({
onlyInvalidMonitors: viewType === 'invalid',
sortField: pageState.sortField,
sortOrder: pageState.sortOrder,
});
useEffect(() => {
if (viewType === 'all') {
dispatch(getMonitors({ page: pageIndex, perPage: pageSize, sortField, sortOrder }));
}
}, [dispatch, pageState, pageIndex, pageSize, sortField, sortOrder, viewType]);
const { data: monitorSavedObjects, loading: objectsLoading } = useInvalidMonitors(errorSummaries);
return (
<>
<MonitorListTabs
invalidTotal={monitorSavedObjects?.length ?? 0}
onUpdate={onUpdate}
onPageStateChange={onPageStateChange}
/>
{viewType === 'all' ? (
<AllMonitors
pageState={pageState}
monitorList={monitorList}
onPageStateChange={onPageStateChange}
onUpdate={onUpdate}
errorSummaries={errorSummaries}
/>
) : (
<InvalidMonitors
pageState={pageState}
monitorSavedObjects={monitorSavedObjects}
onPageStateChange={onPageStateChange}
onUpdate={onUpdate}
errorSummaries={errorSummaries}
invalidTotal={count ?? 0}
loading={Boolean(loading) || Boolean(objectsLoading)}
/>
)}
</>
);
};
type MonitorManagementPageAction =
| {
type: 'update';
payload: MonitorManagementListPageState;
}
| { type: 'refresh' };
const monitorManagementPageReducer: Reducer<
MonitorManagementListPageState,
MonitorManagementPageAction
> = (state: MonitorManagementListPageState, action: MonitorManagementPageAction) => {
switch (action.type) {
case 'update':
return {
...state,
...action.payload,
};
case 'refresh':
return { ...state };
default:
throw new Error(`Action "${(action as MonitorManagementPageAction)?.type}" not recognizable`);
}
};

View file

@ -58,7 +58,7 @@ export const TEST_NOW_ARIA_LABEL = i18n.translate('xpack.uptime.monitorList.test
export const TEST_NOW_AVAILABLE_LABEL = i18n.translate(
'xpack.uptime.monitorList.testNow.available',
{
defaultMessage: 'Test now is only available for monitors added via Monitor management.',
defaultMessage: 'Test now is only available for monitors added via Monitor Management.',
}
);

View file

@ -74,11 +74,14 @@ export const mockState: AppState = {
loading: {
monitorList: false,
serviceLocations: false,
enablement: false,
},
error: {
monitorList: null,
serviceLocations: null,
enablement: null,
},
enablement: null,
syntheticsService: {
loading: false,
},

View file

@ -52,7 +52,7 @@ const LOADING_LABEL = i18n.translate('xpack.uptime.monitorManagement.addMonitorL
const ERROR_HEADING_LABEL = i18n.translate(
'xpack.uptime.monitorManagement.addMonitorLoadingError',
{
defaultMessage: 'Error loading monitor management',
defaultMessage: 'Error loading Monitor Management',
}
);

View file

@ -49,8 +49,8 @@ const LOADING_LABEL = i18n.translate('xpack.uptime.monitorManagement.editMonitor
defaultMessage: 'Loading monitor',
});
const ERROR_HEADING_LABEL = i18n.translate('xpack.uptime.monitorManagement.editMonitorError', {
defaultMessage: 'Error loading monitor management',
const ERROR_HEADING_LABEL = i18n.translate('xpack.uptime.monitorManagement.manageMonitorError', {
defaultMessage: 'Error loading Monitor Management',
});
const SERVICE_LOCATIONS_ERROR_LABEL = i18n.translate(

View file

@ -5,116 +5,151 @@
* 2.0.
*/
import React, { useEffect, useReducer, useCallback, Reducer } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { EuiCallOut, EuiButton, EuiSpacer, EuiLink } from '@elastic/eui';
import { useTrackPageview } from '../../../../observability/public';
import { ConfigKey } from '../../../common/runtime_types';
import { getMonitors } from '../../state/actions';
import { monitorManagementListSelector } from '../../state/selectors';
import { MonitorManagementListPageState } from '../../components/monitor_management/monitor_list/monitor_list';
import { useMonitorManagementBreadcrumbs } from './use_monitor_management_breadcrumbs';
import { useInlineErrors } from '../../components/monitor_management/hooks/use_inline_errors';
import { MonitorListTabs } from '../../components/monitor_management/monitor_list/list_tabs';
import { AllMonitors } from '../../components/monitor_management/monitor_list/all_monitors';
import { InvalidMonitors } from '../../components/monitor_management/monitor_list/invalid_monitors';
import { useInvalidMonitors } from '../../components/monitor_management/hooks/use_invalid_monitors';
import { MonitorListContainer } from '../../components/monitor_management/monitor_list/monitor_list_container';
import { EnablementEmptyState } from '../../components/monitor_management/monitor_list/enablement_empty_state';
import { useEnablement } from '../../components/monitor_management/hooks/use_enablement';
import { Loader } from '../../components/monitor_management/loader/loader';
export const MonitorManagementPage: React.FC = () => {
const [pageState, dispatchPageAction] = useReducer<typeof monitorManagementPageReducer>(
monitorManagementPageReducer,
{
pageIndex: 1, // saved objects page index is base 1
pageSize: 10,
sortOrder: 'asc',
sortField: ConfigKey.NAME,
}
);
const onPageStateChange = useCallback(
(state) => {
dispatchPageAction({ type: 'update', payload: state });
},
[dispatchPageAction]
);
const onUpdate = useCallback(() => {
dispatchPageAction({ type: 'refresh' });
}, [dispatchPageAction]);
useTrackPageview({ app: 'uptime', path: 'manage-monitors' });
useTrackPageview({ app: 'uptime', path: 'manage-monitors', delay: 15000 });
useMonitorManagementBreadcrumbs();
const dispatch = useDispatch();
const monitorList = useSelector(monitorManagementListSelector);
const [shouldFocusEnablementButton, setShouldFocusEnablementButton] = useState(false);
const { pageIndex, pageSize, sortField, sortOrder } = pageState as MonitorManagementListPageState;
const {
error: enablementError,
enablement,
loading: enablementLoading,
enableSynthetics,
} = useEnablement();
const { list: monitorList } = useSelector(monitorManagementListSelector);
const { isEnabled } = enablement;
const { type: viewType } = useParams<{ type: 'all' | 'invalid' }>();
const { errorSummaries, loading, count } = useInlineErrors({
onlyInvalidMonitors: viewType === 'invalid',
sortField: pageState.sortField,
sortOrder: pageState.sortOrder,
});
const isEnabledRef = useRef(isEnabled);
useEffect(() => {
if (viewType === 'all') {
dispatch(getMonitors({ page: pageIndex, perPage: pageSize, sortField, sortOrder }));
if (monitorList.total === null) {
dispatch(
getMonitors({
page: 1, // saved objects page index is base 1
perPage: 10,
sortOrder: 'asc',
sortField: ConfigKey.NAME,
})
);
}
}, [dispatch, pageState, pageIndex, pageSize, sortField, sortOrder, viewType]);
}, [dispatch, monitorList.total]);
const { data: monitorSavedObjects, loading: objectsLoading } = useInvalidMonitors(errorSummaries);
useEffect(() => {
if (!isEnabled && isEnabledRef.current === true) {
/* shift focus to enable button when enable toggle disappears. Prevent
* focus loss on the page */
setShouldFocusEnablementButton(true);
}
isEnabledRef.current = Boolean(isEnabled);
}, [isEnabled]);
return (
<>
<MonitorListTabs
invalidTotal={monitorSavedObjects?.length ?? 0}
onUpdate={onUpdate}
onPageStateChange={onPageStateChange}
/>
{viewType === 'all' ? (
<AllMonitors
pageState={pageState}
monitorList={monitorList}
onPageStateChange={onPageStateChange}
onUpdate={onUpdate}
errorSummaries={errorSummaries}
/>
) : (
<InvalidMonitors
pageState={pageState}
monitorSavedObjects={monitorSavedObjects}
onPageStateChange={onPageStateChange}
onUpdate={onUpdate}
errorSummaries={errorSummaries}
invalidTotal={count ?? 0}
loading={Boolean(loading) || Boolean(objectsLoading)}
/>
<Loader
loading={enablementLoading || monitorList.total === null}
error={Boolean(enablementError)}
loadingTitle={LOADING_LABEL}
errorTitle={ERROR_HEADING_LABEL}
errorBody={ERROR_HEADING_BODY}
>
{!isEnabled && monitorList.total && monitorList.total > 0 ? (
<>
<EuiCallOut title={CALLOUT_MANAGEMENT_DISABLED} color="warning" iconType="help">
<p>{CALLOUT_MANAGEMENT_DESCRIPTION}</p>
{enablement.canEnable ? (
<EuiButton
fill
color="primary"
onClick={() => {
enableSynthetics();
}}
>
{SYNTHETICS_ENABLE_LABEL}
</EuiButton>
) : (
<p>
{CALLOUT_MANAGEMENT_CONTACT_ADMIN}{' '}
<EuiLink href="#" target="_blank">
{LEARN_MORE_LABEL}
</EuiLink>
</p>
)}
</EuiCallOut>
<EuiSpacer size="s" />
</>
) : null}
{isEnabled || (!isEnabled && monitorList.total) ? <MonitorListContainer /> : null}
</Loader>
{isEnabled !== undefined && monitorList.total === 0 && (
<EnablementEmptyState focusButton={shouldFocusEnablementButton} />
)}
</>
);
};
type MonitorManagementPageAction =
| {
type: 'update';
payload: MonitorManagementListPageState;
}
| { type: 'refresh' };
const LOADING_LABEL = i18n.translate('xpack.uptime.monitorManagement.manageMonitorLoadingLabel', {
defaultMessage: 'Loading Monitor Management',
});
const monitorManagementPageReducer: Reducer<
MonitorManagementListPageState,
MonitorManagementPageAction
> = (state: MonitorManagementListPageState, action: MonitorManagementPageAction) => {
switch (action.type) {
case 'update':
return {
...state,
...action.payload,
};
case 'refresh':
return { ...state };
default:
throw new Error(`Action "${(action as MonitorManagementPageAction)?.type}" not recognizable`);
const LEARN_MORE_LABEL = i18n.translate(
'xpack.uptime.monitorManagement.manageMonitorLoadingLabel.callout.learnMore',
{
defaultMessage: 'Learn more.',
}
};
);
const CALLOUT_MANAGEMENT_DISABLED = i18n.translate(
'xpack.uptime.monitorManagement.callout.disabled',
{
defaultMessage: 'Monitor Management is disabled',
}
);
const CALLOUT_MANAGEMENT_CONTACT_ADMIN = i18n.translate(
'xpack.uptime.monitorManagement.callout.disabled.adminContact',
{
defaultMessage: 'Please contact your administrator to enable Monitor Management.',
}
);
const CALLOUT_MANAGEMENT_DESCRIPTION = i18n.translate(
'xpack.uptime.monitorManagement.callout.description.disabled',
{
defaultMessage:
'Monitor Management is currently disabled. To run your monitors on Elastic managed Synthetics service, enable Monitor Management. Your existing monitors are paused.',
}
);
const ERROR_HEADING_LABEL = i18n.translate('xpack.uptime.monitorManagement.editMonitorError', {
defaultMessage: 'Error loading Monitor Management',
});
const ERROR_HEADING_BODY = i18n.translate(
'xpack.uptime.monitorManagement.editMonitorError.description',
{
defaultMessage: 'Monitor Management settings could not be loaded. Please contact Support.',
}
);
const SYNTHETICS_ENABLE_LABEL = i18n.translate(
'xpack.uptime.monitorManagement.syntheticsEnableLabel.management',
{
defaultMessage: 'Enable Monitor Management',
}
);

View file

@ -31,7 +31,7 @@ describe('ServiceAllowedWrapper', () => {
</ServiceAllowedWrapper>
);
expect(await findByText('Loading monitor management')).toBeInTheDocument();
expect(await findByText('Loading Monitor Management')).toBeInTheDocument();
});
it('renders when enabled state is false', async () => {
@ -45,7 +45,7 @@ describe('ServiceAllowedWrapper', () => {
</ServiceAllowedWrapper>
);
expect(await findByText('Monitor management')).toBeInTheDocument();
expect(await findByText('Monitor Management')).toBeInTheDocument();
});
it('renders when enabled state is true', async () => {

View file

@ -45,13 +45,13 @@ const REQUEST_ACCESS_LABEL = i18n.translate('xpack.uptime.monitorManagement.requ
});
const MONITOR_MANAGEMENT_LABEL = i18n.translate('xpack.uptime.monitorManagement.label', {
defaultMessage: 'Monitor management',
defaultMessage: 'Monitor Management',
});
const LOADING_MONITOR_MANAGEMENT_LABEL = i18n.translate(
'xpack.uptime.monitorManagement.loading.label',
{
defaultMessage: 'Loading monitor management',
defaultMessage: 'Loading Monitor Management',
}
);
@ -59,7 +59,7 @@ const PUBLIC_BETA_DESCRIPTION = i18n.translate(
'xpack.uptime.monitorManagement.publicBetaDescription',
{
defaultMessage:
'Monitor management is available only for selected public beta users. With public\n' +
'Monitor Management is available only for selected public beta users. With public\n' +
'beta access, you will be able to add HTTP, TCP, ICMP and Browser checks which will\n' +
"run on Elastic's managed synthetics service nodes.",
}

View file

@ -48,7 +48,7 @@ export const useMonitorManagementBreadcrumbs = ({
export const MONITOR_MANAGEMENT_CRUMB = i18n.translate(
'xpack.uptime.monitorManagement.monitorManagementCrumb',
{
defaultMessage: 'Monitor management',
defaultMessage: 'Monitor Management',
}
);

View file

@ -30,6 +30,22 @@ export const getServiceLocationsSuccess = createAction<{
}>('GET_SERVICE_LOCATIONS_LIST_SUCCESS');
export const getServiceLocationsFailure = createAction<Error>('GET_SERVICE_LOCATIONS_LIST_FAILURE');
export const getSyntheticsEnablement = createAction('GET_SYNTHETICS_ENABLEMENT');
export const getSyntheticsEnablementSuccess = createAction<any>(
'GET_SYNTHETICS_ENABLEMENT_SUCCESS'
);
export const getSyntheticsEnablementFailure = createAction<Error>(
'GET_SYNTHETICS_ENABLEMENT_FAILURE'
);
export const disableSynthetics = createAction('DISABLE_SYNTHETICS');
export const disableSyntheticsSuccess = createAction<any>('DISABLE_SYNTEHTICS_SUCCESS');
export const disableSyntheticsFailure = createAction<Error>('DISABLE_SYNTHETICS_FAILURE');
export const enableSynthetics = createAction('ENABLE_SYNTHETICS');
export const enableSyntheticsSuccess = createAction<any>('ENABLE_SYNTEHTICS_SUCCESS');
export const enableSyntheticsFailure = createAction<Error>('ENABLE_SYNTHETICS_FAILURE');
export const getSyntheticsServiceAllowed = createAsyncAction<void, SyntheticsServiceAllowed>(
'GET_SYNTHETICS_SERVICE_ALLOWED'
);

View file

@ -10,6 +10,8 @@ import {
FetchMonitorManagementListQueryArgs,
MonitorManagementListResultCodec,
MonitorManagementListResult,
MonitorManagementEnablementResultCodec,
MonitorManagementEnablementResult,
ServiceLocations,
SyntheticsMonitor,
EncryptedSyntheticsMonitor,
@ -91,6 +93,23 @@ export const testNowMonitor = async (configId: string): Promise<TestNowResponse
return await apiService.get(API_URLS.TRIGGER_MONITOR + `/${configId}`);
};
export const fetchGetSyntheticsEnablement =
async (): Promise<MonitorManagementEnablementResult> => {
return await apiService.get(
API_URLS.SYNTHETICS_ENABLEMENT,
undefined,
MonitorManagementEnablementResultCodec
);
};
export const fetchDisableSynthetics = async (): Promise<void> => {
return await apiService.delete(API_URLS.SYNTHETICS_ENABLEMENT);
};
export const fetchEnableSynthetics = async (): Promise<void> => {
return await apiService.post(API_URLS.SYNTHETICS_ENABLEMENT);
};
export const fetchServiceAllowed = async (): Promise<SyntheticsServiceAllowed> => {
return await apiService.get(API_URLS.SERVICE_ALLOWED);
};

View file

@ -13,9 +13,25 @@ import {
getServiceLocations,
getServiceLocationsSuccess,
getServiceLocationsFailure,
getSyntheticsEnablement,
getSyntheticsEnablementSuccess,
getSyntheticsEnablementFailure,
disableSynthetics,
disableSyntheticsSuccess,
disableSyntheticsFailure,
enableSynthetics,
enableSyntheticsSuccess,
enableSyntheticsFailure,
getSyntheticsServiceAllowed,
} from '../actions';
import { fetchMonitorManagementList, fetchServiceAllowed, fetchServiceLocations } from '../api';
import {
fetchMonitorManagementList,
fetchServiceLocations,
fetchServiceAllowed,
fetchGetSyntheticsEnablement,
fetchDisableSynthetics,
fetchEnableSynthetics,
} from '../api';
import { fetchEffectFactory } from './fetch_effect';
export function* fetchMonitorManagementEffect() {
@ -31,6 +47,22 @@ export function* fetchMonitorManagementEffect() {
getServiceLocationsFailure
)
);
yield takeLatest(
getSyntheticsEnablement,
fetchEffectFactory(
fetchGetSyntheticsEnablement,
getSyntheticsEnablementSuccess,
getSyntheticsEnablementFailure
)
);
yield takeLatest(
disableSynthetics,
fetchEffectFactory(fetchDisableSynthetics, disableSyntheticsSuccess, disableSyntheticsFailure)
);
yield takeLatest(
enableSynthetics,
fetchEffectFactory(fetchEnableSynthetics, enableSyntheticsSuccess, enableSyntheticsFailure)
);
}
export function* fetchSyntheticsServiceAllowedEffect() {

View file

@ -14,23 +14,32 @@ import {
getServiceLocations,
getServiceLocationsSuccess,
getServiceLocationsFailure,
getSyntheticsEnablement,
getSyntheticsEnablementSuccess,
getSyntheticsEnablementFailure,
disableSynthetics,
disableSyntheticsSuccess,
disableSyntheticsFailure,
enableSynthetics,
enableSyntheticsSuccess,
enableSyntheticsFailure,
getSyntheticsServiceAllowed,
} from '../actions';
import { SyntheticsServiceAllowed } from '../../../common/types';
import {
MonitorManagementEnablementResult,
MonitorManagementListResult,
ServiceLocations,
ThrottlingOptions,
DEFAULT_THROTTLING,
} from '../../../common/runtime_types';
import { SyntheticsServiceAllowed } from '../../../common/types';
export interface MonitorManagementList {
error: Record<'monitorList' | 'serviceLocations', Error | null>;
loading: Record<'monitorList' | 'serviceLocations', boolean>;
error: Record<'monitorList' | 'serviceLocations' | 'enablement', Error | null>;
loading: Record<'monitorList' | 'serviceLocations' | 'enablement', boolean>;
list: MonitorManagementListResult;
locations: ServiceLocations;
enablement: MonitorManagementEnablementResult | null;
syntheticsService: { isAllowed?: boolean; loading: boolean };
throttling: ThrottlingOptions;
}
@ -43,13 +52,16 @@ export const initialState: MonitorManagementList = {
monitors: [],
},
locations: [],
enablement: null,
loading: {
monitorList: false,
serviceLocations: false,
enablement: false,
},
error: {
monitorList: null,
serviceLocations: null,
enablement: null,
},
syntheticsService: {
loading: false,
@ -141,6 +153,116 @@ export const monitorManagementListReducer = createReducer(initialState, (builder
},
})
)
.addCase(getSyntheticsEnablement, (state: WritableDraft<MonitorManagementList>) => ({
...state,
loading: {
...state.loading,
enablement: true,
},
}))
.addCase(
getSyntheticsEnablementSuccess,
(state: WritableDraft<MonitorManagementList>, action: PayloadAction<any>) => ({
...state,
loading: {
...state.loading,
enablement: false,
},
error: {
...state.error,
enablement: null,
},
enablement: action.payload,
})
)
.addCase(
getSyntheticsEnablementFailure,
(state: WritableDraft<MonitorManagementList>, action: PayloadAction<Error>) => ({
...state,
loading: {
...state.loading,
enablement: false,
},
error: {
...state.error,
enablement: action.payload,
},
})
)
.addCase(disableSynthetics, (state: WritableDraft<MonitorManagementList>) => ({
...state,
loading: {
...state.loading,
enablement: true,
},
}))
.addCase(disableSyntheticsSuccess, (state: WritableDraft<MonitorManagementList>) => ({
...state,
loading: {
...state.loading,
enablement: false,
},
error: {
...state.error,
enablement: null,
},
enablement: {
canEnable: state.enablement?.canEnable || false,
areApiKeysEnabled: state.enablement?.areApiKeysEnabled || false,
isEnabled: false,
},
}))
.addCase(
disableSyntheticsFailure,
(state: WritableDraft<MonitorManagementList>, action: PayloadAction<Error>) => ({
...state,
loading: {
...state.loading,
enablement: false,
},
error: {
...state.error,
enablement: action.payload,
},
})
)
.addCase(enableSynthetics, (state: WritableDraft<MonitorManagementList>) => ({
...state,
loading: {
...state.loading,
enablement: true,
},
}))
.addCase(enableSyntheticsSuccess, (state: WritableDraft<MonitorManagementList>) => ({
...state,
loading: {
...state.loading,
enablement: false,
},
error: {
...state.error,
enablement: null,
},
enablement: {
canEnable: state.enablement?.canEnable || false,
areApiKeysEnabled: state.enablement?.areApiKeysEnabled || false,
isEnabled: true,
},
}))
.addCase(
enableSyntheticsFailure,
(state: WritableDraft<MonitorManagementList>, action: PayloadAction<Error>) => ({
...state,
loading: {
...state.loading,
enablement: false,
},
error: {
...state.error,
enablement: action.payload,
},
})
)
.addCase(
String(getSyntheticsServiceAllowed.get),
(state: WritableDraft<MonitorManagementList>) => ({

View file

@ -26,6 +26,12 @@ import { getJourneyFailedSteps } from './get_journey_failed_steps';
import { getLastSuccessfulCheck } from './get_last_successful_check';
import { getJourneyScreenshotBlocks } from './get_journey_screenshot_blocks';
import { getSyntheticsMonitor } from './get_monitor';
import {
getSyntheticsEnablement,
deleteServiceApiKey,
generateAndSaveServiceAPIKey,
getAPIKeyForSyntheticsService,
} from '../synthetics_service/get_api_key';
export const requests = {
getCerts,
@ -49,6 +55,10 @@ export const requests = {
getJourneyScreenshotBlocks,
getJourneyDetails,
getNetworkEvents,
getSyntheticsEnablement,
getAPIKeyForSyntheticsService,
deleteServiceApiKey,
generateAndSaveServiceAPIKey,
};
export type UptimeRequests = typeof requests;

View file

@ -63,6 +63,7 @@ export const getSyntheticsServiceAPIKey = async (client: EncryptedSavedObjectsCl
throw getErr;
}
};
export const setSyntheticsServiceApiKey = async (
client: SavedObjectsClientContract,
apiKey: SyntheticsServiceApiKey
@ -72,3 +73,11 @@ export const setSyntheticsServiceApiKey = async (
overwrite: true,
});
};
export const deleteSyntheticsServiceApiKey = async (client: SavedObjectsClientContract) => {
try {
return await client.delete(syntheticsServiceApiKey.name, syntheticsApiKeyID);
} catch (e) {
throw e;
}
};

View file

@ -12,6 +12,7 @@ import { coreMock } from '../../../../../../src/core/server/mocks';
import { syntheticsServiceApiKey } from '../saved_objects/service_api_key';
import { KibanaRequest } from 'kibana/server';
import { UptimeServerSetup } from '../adapters';
import { getUptimeESMockClient } from '../requests/helper';
describe('getAPIKeyTest', function () {
const core = coreMock.createStart();
@ -23,6 +24,7 @@ describe('getAPIKeyTest', function () {
security,
encryptedSavedObjects,
savedObjectsClient: core.savedObjects.getScopedClient(request),
uptimeEsClient: getUptimeESMockClient().uptimeEsClient,
} as unknown as UptimeServerSetup;
security.authc.apiKeys.areAPIKeysEnabled = jest.fn().mockReturnValue(true);
@ -33,38 +35,6 @@ describe('getAPIKeyTest', function () {
encoded: '@#$%^&',
});
it('should generate an api key and return it', async () => {
const apiKey = await getAPIKeyForSyntheticsService({
request,
server,
});
expect(security.authc.apiKeys.areAPIKeysEnabled).toHaveBeenCalledTimes(1);
expect(security.authc.apiKeys.create).toHaveBeenCalledTimes(1);
expect(security.authc.apiKeys.create).toHaveBeenCalledWith(
{},
{
name: 'synthetics-api-key',
role_descriptors: {
synthetics_writer: {
cluster: ['monitor', 'read_ilm', 'read_pipeline'],
index: [
{
names: ['synthetics-*'],
privileges: ['view_index_metadata', 'create_doc', 'auto_configure'],
},
],
},
},
metadata: {
description:
'Created for synthetics service to be passed to the heartbeat to communicate with ES',
},
}
);
expect(apiKey).toEqual({ apiKey: 'qwerty', id: 'test', name: 'service-api-key' });
});
it('should return existing api key', async () => {
const getObject = jest
.fn()
@ -74,7 +44,6 @@ describe('getAPIKeyTest', function () {
getDecryptedAsInternalUser: getObject,
});
const apiKey = await getAPIKeyForSyntheticsService({
request,
server,
});

View file

@ -4,25 +4,42 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type {
SecurityClusterPrivilege,
SecurityIndexPrivilege,
} from '@elastic/elasticsearch/lib/api/types';
import { KibanaRequest, SavedObjectsClientContract } from '../../../../../../src/core/server';
import { SecurityPluginStart } from '../../../../security/server';
import {
getSyntheticsServiceAPIKey,
deleteSyntheticsServiceApiKey,
setSyntheticsServiceApiKey,
syntheticsServiceApiKey,
} from '../saved_objects/service_api_key';
import { SyntheticsServiceApiKey } from '../../../common/runtime_types/synthetics_service_api_key';
import { UptimeServerSetup } from '../adapters';
export const serviceApiKeyPrivileges = {
cluster: ['monitor', 'read_ilm', 'read_pipeline'] as SecurityClusterPrivilege[],
index: [
{
names: ['synthetics-*'],
privileges: [
'view_index_metadata',
'create_doc',
'auto_configure',
] as SecurityIndexPrivilege[],
},
],
};
export const getAPIKeyForSyntheticsService = async ({
request,
server,
}: {
server: UptimeServerSetup;
request?: KibanaRequest;
}): Promise<SyntheticsServiceApiKey | undefined> => {
const { security, encryptedSavedObjects, authSavedObjectsClient } = server;
const { encryptedSavedObjects } = server;
const encryptedClient = encryptedSavedObjects.getClient({
includedHiddenTypes: [syntheticsServiceApiKey.name],
@ -36,19 +53,15 @@ export const getAPIKeyForSyntheticsService = async ({
} catch (err) {
// TODO: figure out how to handle decryption errors
}
return await generateAndSaveAPIKey({
request,
security,
authSavedObjectsClient,
});
};
export const generateAndSaveAPIKey = async ({
export const generateAndSaveServiceAPIKey = async ({
server,
security,
request,
authSavedObjectsClient,
}: {
server: UptimeServerSetup;
request?: KibanaRequest;
security: SecurityPluginStart;
// authSavedObject is needed for write operations
@ -64,18 +77,15 @@ export const generateAndSaveAPIKey = async ({
throw new Error('User authorization is needed for api key generation');
}
const { canEnable } = await getSyntheticsEnablement({ request, server });
if (!canEnable) {
throw new SyntheticsForbiddenError();
}
const apiKeyResult = await security.authc.apiKeys?.create(request, {
name: 'synthetics-api-key',
role_descriptors: {
synthetics_writer: {
cluster: ['monitor', 'read_ilm', 'read_pipeline'],
index: [
{
names: ['synthetics-*'],
privileges: ['view_index_metadata', 'create_doc', 'auto_configure'],
},
],
},
synthetics_writer: serviceApiKeyPrivileges,
},
metadata: {
description:
@ -93,3 +103,73 @@ export const generateAndSaveAPIKey = async ({
return apiKeyObject;
}
};
export const deleteServiceApiKey = async ({
request,
server,
savedObjectsClient,
}: {
server: UptimeServerSetup;
request?: KibanaRequest;
savedObjectsClient: SavedObjectsClientContract;
}) => {
await deleteSyntheticsServiceApiKey(savedObjectsClient);
};
export const getSyntheticsEnablement = async ({
request,
server: { uptimeEsClient, security, encryptedSavedObjects },
}: {
server: UptimeServerSetup;
request?: KibanaRequest;
}) => {
const encryptedClient = encryptedSavedObjects.getClient({
includedHiddenTypes: [syntheticsServiceApiKey.name],
});
const [apiKey, hasPrivileges, areApiKeysEnabled] = await Promise.all([
getSyntheticsServiceAPIKey(encryptedClient),
uptimeEsClient.baseESClient.security.hasPrivileges({
body: {
cluster: [
'manage_security',
'manage_api_key',
'manage_own_api_key',
...serviceApiKeyPrivileges.cluster,
],
index: serviceApiKeyPrivileges.index,
},
}),
security.authc.apiKeys.areAPIKeysEnabled(),
]);
const { cluster } = hasPrivileges;
const {
manage_security: manageSecurity,
manage_api_key: manageApiKey,
manage_own_api_key: manageOwnApiKey,
monitor,
read_ilm: readILM,
read_pipeline: readPipeline,
} = cluster || {};
const canManageApiKeys = manageSecurity || manageApiKey || manageOwnApiKey;
const hasClusterPermissions = readILM && readPipeline && monitor;
const hasIndexPermissions = !Object.values(hasPrivileges.index?.['synthetics-*'] || []).includes(
false
);
return {
canEnable: canManageApiKeys && hasClusterPermissions && hasIndexPermissions,
isEnabled: Boolean(apiKey),
areApiKeysEnabled,
};
};
export class SyntheticsForbiddenError extends Error {
constructor() {
super();
this.message = 'Forbidden';
this.name = 'SyntheticsForbiddenError';
}
}

View file

@ -191,32 +191,25 @@ export class SyntheticsService {
}
}
async getOutput(request?: KibanaRequest) {
if (!this.apiKey) {
try {
this.apiKey = await getAPIKeyForSyntheticsService({ server: this.server, request });
} catch (err) {
this.logger.error(err);
throw err;
}
async getApiKey() {
try {
this.apiKey = await getAPIKeyForSyntheticsService({ server: this.server });
} catch (err) {
this.logger.error(err);
throw err;
}
if (!this.apiKey) {
const error = new APIKeyMissingError();
this.logger.error(error);
throw error;
}
this.logger.debug('Found api key and esHosts for service.');
return this.apiKey;
}
async getOutput(apiKey: SyntheticsServiceApiKey) {
return {
hosts: this.esHosts,
api_key: `${this.apiKey.id}:${this.apiKey.apiKey}`,
api_key: `${apiKey?.id}:${apiKey?.apiKey}`,
};
}
async pushConfigs(
request?: KibanaRequest,
configs?: Array<
SyntheticsMonitorWithId & {
fields_under_root?: boolean;
@ -229,9 +222,16 @@ export class SyntheticsService {
this.logger.debug('No monitor found which can be pushed to service.');
return;
}
this.apiKey = await this.getApiKey();
if (!this.apiKey) {
return null;
}
const data = {
monitors,
output: await this.getOutput(request),
output: await this.getOutput(this.apiKey),
};
this.logger.debug(`${monitors.length} monitors will be pushed to synthetics service.`);
@ -245,7 +245,6 @@ export class SyntheticsService {
}
async runOnceConfigs(
request?: KibanaRequest,
configs?: Array<
SyntheticsMonitorWithId & {
fields_under_root?: boolean;
@ -257,9 +256,15 @@ export class SyntheticsService {
if (monitors.length === 0) {
return;
}
this.apiKey = await this.getApiKey();
if (!this.apiKey) {
return null;
}
const data = {
monitors,
output: await this.getOutput(request),
output: await this.getOutput(this.apiKey),
};
try {
@ -283,9 +288,16 @@ export class SyntheticsService {
if (monitors.length === 0) {
return;
}
this.apiKey = await this.getApiKey();
if (!this.apiKey) {
return null;
}
const data = {
monitors,
output: await this.getOutput(request),
output: await this.getOutput(this.apiKey),
};
try {
@ -296,14 +308,25 @@ export class SyntheticsService {
}
}
async deleteConfigs(request: KibanaRequest, configs: SyntheticsMonitorWithId[]) {
async deleteConfigs(configs: SyntheticsMonitorWithId[]) {
this.apiKey = await this.getApiKey();
if (!this.apiKey) {
return null;
}
const data = {
monitors: this.formatConfigs(configs),
output: await this.getOutput(request),
output: await this.getOutput(this.apiKey),
};
return await this.apiClient.delete(data);
}
async deleteAllConfigs() {
const configs = await this.getMonitorConfigs();
return await this.deleteConfigs(configs);
}
async getMonitorConfigs() {
const savedObjectsClient = this.server.savedObjectsClient;
const encryptedClient = this.server.encryptedSavedObjects.getClient();
@ -362,14 +385,6 @@ export class SyntheticsService {
}
}
class APIKeyMissingError extends Error {
constructor() {
super();
this.message = 'API key is needed for synthetics service.';
this.name = 'APIKeyMissingError';
}
}
class IndexTemplateInstallationError extends Error {
constructor() {
super();

View file

@ -38,6 +38,11 @@ import { editSyntheticsMonitorRoute } from './synthetics_service/edit_monitor';
import { deleteSyntheticsMonitorRoute } from './synthetics_service/delete_monitor';
import { runOnceSyntheticsMonitorRoute } from './synthetics_service/run_once_monitor';
import { testNowMonitorRoute } from './synthetics_service/test_now_monitor';
import {
getSyntheticsEnablementRoute,
disableSyntheticsRoute,
enableSyntheticsRoute,
} from './synthetics_service/enablement';
import { getServiceAllowedRoute } from './synthetics_service/get_service_allowed';
export * from './types';
@ -45,6 +50,8 @@ export { createRouteWithAuth } from './create_route_with_auth';
export { uptimeRouteWrapper } from './uptime_route_wrapper';
export const restApiRoutes: UMRestApiRouteFactory[] = [
addSyntheticsMonitorRoute,
getSyntheticsEnablementRoute,
createGetPingsRoute,
createGetIndexStatusRoute,
createGetDynamicSettingsRoute,
@ -63,13 +70,14 @@ export const restApiRoutes: UMRestApiRouteFactory[] = [
createJourneyFailedStepsRoute,
createLastSuccessfulCheckRoute,
createJourneyScreenshotBlocksRoute,
installIndexTemplatesRoute,
deleteSyntheticsMonitorRoute,
disableSyntheticsRoute,
editSyntheticsMonitorRoute,
enableSyntheticsRoute,
getServiceLocationsRoute,
getSyntheticsMonitorRoute,
getAllSyntheticsMonitorRoute,
addSyntheticsMonitorRoute,
editSyntheticsMonitorRoute,
deleteSyntheticsMonitorRoute,
installIndexTemplatesRoute,
runOnceSyntheticsMonitorRoute,
testNowMonitorRoute,
getServiceAllowedRoute,

View file

@ -45,7 +45,7 @@ export const addSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
const { syntheticsService } = server;
const errors = await syntheticsService.pushConfigs(request, [
const errors = await syntheticsService.pushConfigs([
{
...monitor,
id: newMonitor.id,

View file

@ -59,7 +59,7 @@ export const deleteSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
const normalizedMonitor = normalizeSecrets(monitor);
await savedObjectsClient.delete(syntheticsMonitorType, monitorId);
const errors = await syntheticsService.deleteConfigs(request, [
const errors = await syntheticsService.deleteConfigs([
{ ...normalizedMonitor.attributes, id: monitorId },
]);

View file

@ -91,7 +91,7 @@ export const editSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
monitor.type === 'browser' ? { ...monitorWithRevision, urls: '' } : monitorWithRevision
);
const errors = await syntheticsService.pushConfigs(request, [
const errors = await syntheticsService.pushConfigs([
{
...editedMonitor,
id: editMonitor.id,

View file

@ -0,0 +1,81 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { UMRestApiRouteFactory } from '../types';
import { API_URLS } from '../../../common/constants';
import { SyntheticsForbiddenError } from '../../lib/synthetics_service/get_api_key';
export const getSyntheticsEnablementRoute: UMRestApiRouteFactory = (libs) => ({
method: 'GET',
path: API_URLS.SYNTHETICS_ENABLEMENT,
validate: {},
handler: async ({ request, response, server }): Promise<any> => {
try {
return response.ok({
body: await libs.requests.getSyntheticsEnablement({
request,
server,
}),
});
} catch (e) {
server.logger.error(e);
throw e;
}
},
});
export const disableSyntheticsRoute: UMRestApiRouteFactory = (libs) => ({
method: 'DELETE',
path: API_URLS.SYNTHETICS_ENABLEMENT,
validate: {},
handler: async ({ response, request, server, savedObjectsClient }): Promise<any> => {
const { syntheticsService, security } = server;
try {
const { canEnable } = await libs.requests.getSyntheticsEnablement({ request, server });
if (!canEnable) {
return response.forbidden();
}
await syntheticsService.deleteAllConfigs();
const apiKey = await libs.requests.getAPIKeyForSyntheticsService({
server,
});
await libs.requests.deleteServiceApiKey({
request,
server,
savedObjectsClient,
});
await security.authc.apiKeys?.invalidate(request, { ids: [apiKey?.id || ''] });
return response.ok({});
} catch (e) {
server.logger.error(e);
throw e;
}
},
});
export const enableSyntheticsRoute: UMRestApiRouteFactory = (libs) => ({
method: 'POST',
path: API_URLS.SYNTHETICS_ENABLEMENT,
validate: {},
handler: async ({ request, response, server }): Promise<any> => {
const { authSavedObjectsClient, logger, security } = server;
try {
await libs.requests.generateAndSaveServiceAPIKey({
request,
authSavedObjectsClient,
security,
server,
});
return response.ok({});
} catch (e) {
logger.error(e);
if (e instanceof SyntheticsForbiddenError) {
return response.forbidden();
}
throw e;
}
},
});

View file

@ -32,7 +32,7 @@ export const runOnceSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
const { syntheticsService } = server;
const errors = await syntheticsService.runOnceConfigs(request, [
const errors = await syntheticsService.runOnceConfigs([
{
...monitor,
id: monitorId,

View file

@ -53,7 +53,7 @@ export function formatTelemetryEvent({
lastUpdatedAt?: string;
durationSinceLastUpdated?: number;
deletedAt?: string;
errors?: ServiceLocationErrors;
errors?: ServiceLocationErrors | null;
}) {
const { attributes } = monitor;
@ -91,7 +91,7 @@ export function formatTelemetryUpdateEvent(
currentMonitor: SavedObjectsUpdateResponse<EncryptedSyntheticsMonitor>,
previousMonitor: SavedObject<EncryptedSyntheticsMonitor>,
kibanaVersion: string,
errors?: ServiceLocationErrors
errors?: ServiceLocationErrors | null
) {
let durationSinceLastUpdated: number = 0;
if (currentMonitor.updated_at && previousMonitor.updated_at) {
@ -113,7 +113,7 @@ export function formatTelemetryDeleteEvent(
previousMonitor: SavedObject<EncryptedSyntheticsMonitor>,
kibanaVersion: string,
deletedAt: string,
errors?: ServiceLocationErrors
errors?: ServiceLocationErrors | null
) {
let durationSinceLastUpdated: number = 0;
if (deletedAt && previousMonitor.updated_at) {

View file

@ -30,7 +30,9 @@ export default function ({ getService }: FtrProviderContext) {
return res.body as SimpleSavedObject<MonitorFields>;
};
before(() => {
before(async () => {
await supertest.post(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200);
_monitors = [
getFixtureJson('icmp_monitor'),
getFixtureJson('tcp_monitor'),

View file

@ -77,6 +77,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./add_monitor'));
loadTestFile(require.resolve('./edit_monitor'));
loadTestFile(require.resolve('./delete_monitor'));
loadTestFile(require.resolve('./synthetics_enablement'));
});
});
}

View file

@ -0,0 +1,316 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { API_URLS } from '../../../../../plugins/uptime/common/constants';
import { serviceApiKeyPrivileges } from '../../../../../plugins/uptime/server/lib/synthetics_service/get_api_key';
export default function ({ getService }: FtrProviderContext) {
describe('/internal/uptime/service/enablement', () => {
const supertestWithAuth = getService('supertest');
const supertest = getService('supertestWithoutAuth');
const security = getService('security');
before(async () => {
await supertestWithAuth.delete(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true');
});
describe('[GET] - /internal/uptime/service/enablement', () => {
['manage_security', 'manage_api_key', 'manage_own_api_key'].forEach((privilege) => {
it(`returns response for an admin with priviledge ${privilege}`, async () => {
const username = 'admin';
const roleName = `synthetics_admin-${privilege}`;
const password = `${username}-password`;
try {
await security.role.create(roleName, {
kibana: [
{
feature: {
uptime: ['all'],
},
spaces: ['*'],
},
],
elasticsearch: {
cluster: [privilege, ...serviceApiKeyPrivileges.cluster],
indices: serviceApiKeyPrivileges.index,
},
});
await security.user.create(username, {
password,
roles: [roleName],
full_name: 'a kibana user',
});
const apiResponse = await supertest
.get(API_URLS.SYNTHETICS_ENABLEMENT)
.auth(username, password)
.set('kbn-xsrf', 'true')
.expect(200);
expect(apiResponse.body).eql({
areApiKeysEnabled: true,
canEnable: true,
isEnabled: false,
});
} finally {
await security.user.delete(username);
await security.role.delete(roleName);
}
});
});
it('returns response for an uptime all user without admin privileges', async () => {
const username = 'uptime';
const roleName = 'uptime_user';
const password = `${username}-password`;
try {
await security.role.create(roleName, {
kibana: [
{
feature: {
uptime: ['all'],
},
spaces: ['*'],
},
],
elasticsearch: {},
});
await security.user.create(username, {
password,
roles: [roleName],
full_name: 'a kibana user',
});
const apiResponse = await supertest
.get(API_URLS.SYNTHETICS_ENABLEMENT)
.auth(username, password)
.set('kbn-xsrf', 'true')
.expect(200);
expect(apiResponse.body).eql({
areApiKeysEnabled: true,
canEnable: false,
isEnabled: false,
});
} finally {
await security.role.delete(roleName);
await security.user.delete(username);
}
});
});
describe('[POST] - /internal/uptime/service/enablement', () => {
it('with an admin', async () => {
const username = 'admin';
const roleName = `synthetics_admin`;
const password = `${username}-password`;
try {
await security.role.create(roleName, {
kibana: [
{
feature: {
uptime: ['all'],
},
spaces: ['*'],
},
],
elasticsearch: {
cluster: ['manage_security', ...serviceApiKeyPrivileges.cluster],
indices: serviceApiKeyPrivileges.index,
},
});
await security.user.create(username, {
password,
roles: [roleName],
full_name: 'a kibana user',
});
await supertest
.post(API_URLS.SYNTHETICS_ENABLEMENT)
.auth(username, password)
.set('kbn-xsrf', 'true')
.expect(200);
const apiResponse = await supertest
.get(API_URLS.SYNTHETICS_ENABLEMENT)
.auth(username, password)
.set('kbn-xsrf', 'true')
.expect(200);
expect(apiResponse.body).eql({
areApiKeysEnabled: true,
canEnable: true,
isEnabled: true,
});
} finally {
await supertest
.delete(API_URLS.SYNTHETICS_ENABLEMENT)
.auth(username, password)
.set('kbn-xsrf', 'true')
.expect(200);
await security.user.delete(username);
await security.role.delete(roleName);
}
});
it('with an uptime user', async () => {
const username = 'uptime';
const roleName = `uptime_user`;
const password = `${username}-password`;
try {
await security.role.create(roleName, {
kibana: [
{
feature: {
uptime: ['all'],
},
spaces: ['*'],
},
],
elasticsearch: {},
});
await security.user.create(username, {
password,
roles: [roleName],
full_name: 'a kibana user',
});
await supertest
.post(API_URLS.SYNTHETICS_ENABLEMENT)
.auth(username, password)
.set('kbn-xsrf', 'true')
.expect(403);
const apiResponse = await supertest
.get(API_URLS.SYNTHETICS_ENABLEMENT)
.auth(username, password)
.set('kbn-xsrf', 'true')
.expect(200);
expect(apiResponse.body).eql({
areApiKeysEnabled: true,
canEnable: false,
isEnabled: false,
});
} finally {
await security.user.delete(username);
await security.role.delete(roleName);
}
});
});
describe('[DELETE] - /internal/uptime/service/enablement', () => {
it('with an admin', async () => {
const username = 'admin';
const roleName = `synthetics_admin`;
const password = `${username}-password`;
try {
await security.role.create(roleName, {
kibana: [
{
feature: {
uptime: ['all'],
},
spaces: ['*'],
},
],
elasticsearch: {
cluster: ['manage_security', ...serviceApiKeyPrivileges.cluster],
indices: serviceApiKeyPrivileges.index,
},
});
await security.user.create(username, {
password,
roles: [roleName],
full_name: 'a kibana user',
});
await supertest
.post(API_URLS.SYNTHETICS_ENABLEMENT)
.auth(username, password)
.set('kbn-xsrf', 'true')
.expect(200);
await supertest
.delete(API_URLS.SYNTHETICS_ENABLEMENT)
.auth(username, password)
.set('kbn-xsrf', 'true')
.expect(200);
const apiResponse = await supertest
.get(API_URLS.SYNTHETICS_ENABLEMENT)
.auth(username, password)
.set('kbn-xsrf', 'true')
.expect(200);
expect(apiResponse.body).eql({
areApiKeysEnabled: true,
canEnable: true,
isEnabled: false,
});
} finally {
await security.user.delete(username);
await security.role.delete(roleName);
}
});
it('with an uptime user', async () => {
const username = 'uptime';
const roleName = `uptime_user`;
const password = `${username}-password`;
try {
await security.role.create(roleName, {
kibana: [
{
feature: {
uptime: ['all'],
},
spaces: ['*'],
},
],
elasticsearch: {},
});
await security.user.create(username, {
password,
roles: [roleName],
full_name: 'a kibana user',
});
await supertestWithAuth
.post(API_URLS.SYNTHETICS_ENABLEMENT)
.set('kbn-xsrf', 'true')
.expect(200);
await supertest
.delete(API_URLS.SYNTHETICS_ENABLEMENT)
.auth(username, password)
.set('kbn-xsrf', 'true')
.expect(403);
const apiResponse = await supertest
.get(API_URLS.SYNTHETICS_ENABLEMENT)
.auth(username, password)
.set('kbn-xsrf', 'true')
.expect(200);
expect(apiResponse.body).eql({
areApiKeysEnabled: true,
canEnable: false,
isEnabled: true,
});
} finally {
await supertestWithAuth
.delete(API_URLS.SYNTHETICS_ENABLEMENT)
.set('kbn-xsrf', 'true')
.expect(200);
await security.user.delete(username);
await security.role.delete(roleName);
}
});
});
});
}