[Monitor management] Enable check for public beta (#128240)

Co-authored-by: Dominique Clarke <dominique.clarke@elastic.co>
This commit is contained in:
Shahzad 2022-03-24 18:50:56 +01:00 committed by GitHub
parent 0079b3672b
commit fd1c76691f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 564 additions and 286 deletions

View file

@ -21,28 +21,12 @@ const serviceConfig = schema.object({
const uptimeConfig = schema.object({
index: schema.maybe(schema.string()),
ui: schema.maybe(
schema.object({
monitorManagement: schema.maybe(
schema.object({
enabled: schema.boolean(),
})
),
})
),
service: schema.maybe(serviceConfig),
});
export const config: PluginConfigDescriptor = {
exposeToBrowser: {
ui: true,
},
schema: uptimeConfig,
};
export type UptimeConfig = TypeOf<typeof uptimeConfig>;
export type ServiceConfig = TypeOf<typeof serviceConfig>;
export interface UptimeUiConfig {
ui?: TypeOf<typeof config.schema>['ui'];
}

View file

@ -42,4 +42,5 @@ export enum API_URLS {
SYNTHETICS_MONITORS = '/internal/uptime/service/monitors',
RUN_ONCE_MONITOR = '/internal/uptime/service/monitors/run_once',
TRIGGER_MONITOR = '/internal/uptime/service/monitors/trigger',
SERVICE_ALLOWED = '/internal/uptime/service/allowed',
}

View file

@ -15,3 +15,7 @@ export interface MonitorIdParam {
export type SyntheticsMonitorSavedObject = SimpleSavedObject<SyntheticsMonitor> & {
updated_at: string;
};
export interface SyntheticsServiceAllowed {
serviceAllowed: boolean;
}

View file

@ -63,7 +63,6 @@ async function config({ readConfigFile }: FtrConfigProviderContext) {
: 'localKibanaIntegrationTestsUser'
}`,
`--xpack.uptime.service.password=${servicPassword}`,
'--xpack.uptime.ui.monitorManagement.enabled=true',
],
},
};

View file

@ -48,7 +48,6 @@ import {
} from '../components/fleet_package';
import { LazySyntheticsCustomAssetsExtension } from '../components/fleet_package/lazy_synthetics_custom_assets_extension';
import { Start as InspectorPluginStart } from '../../../../../src/plugins/inspector/public';
import { UptimeUiConfig } from '../../common/config';
import { CasesUiStart } from '../../../cases/public';
export interface ClientPluginsSetup {
@ -87,7 +86,6 @@ export class UptimePlugin
constructor(private readonly initContext: PluginInitializerContext) {}
public setup(core: CoreSetup<ClientPluginsStart, unknown>, plugins: ClientPluginsSetup): void {
const config = this.initContext.config.get<UptimeUiConfig>();
if (plugins.home) {
plugins.home.featureCatalogue.register({
id: PLUGIN.ID,
@ -215,14 +213,7 @@ export class UptimePlugin
const [coreStart, corePlugins] = await core.getStartServices();
const { renderApp } = await import('./render_app');
return renderApp(
coreStart,
plugins,
corePlugins,
params,
config,
this.initContext.env.mode.dev
);
return renderApp(coreStart, plugins, corePlugins, params, this.initContext.env.mode.dev);
},
});
}

View file

@ -17,7 +17,6 @@ import {
} from '../../common/constants';
import { UptimeApp, UptimeAppProps } from './uptime_app';
import { ClientPluginsSetup, ClientPluginsStart } from './plugin';
import { UptimeUiConfig } from '../../common/config';
import { uptimeOverviewNavigatorParams } from './locators/overview';
export function renderApp(
@ -25,7 +24,6 @@ export function renderApp(
plugins: ClientPluginsSetup,
startPlugins: ClientPluginsStart,
appMountParameters: AppMountParameters,
config: UptimeUiConfig,
isDev: boolean
) {
const {
@ -77,7 +75,6 @@ export function renderApp(
setBadge,
appMountParameters,
setBreadcrumbs: core.chrome.setBreadcrumbs,
config,
};
ReactDOM.render(<UptimeApp {...props} />, appMountParameters.element);

View file

@ -35,7 +35,6 @@ import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
import { UptimeIndexPatternContextProvider } from '../contexts/uptime_index_pattern_context';
import { InspectorContextProvider } from '../../../observability/public';
import { UptimeUiConfig } from '../../common/config';
export interface UptimeAppColors {
danger: string;
@ -64,7 +63,6 @@ export interface UptimeAppProps {
commonlyUsedRanges: CommonlyUsedRange[];
setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void;
appMountParameters: AppMountParameters;
config: UptimeUiConfig;
isDev: boolean;
}
@ -80,7 +78,6 @@ const Application = (props: UptimeAppProps) => {
setBadge,
startPlugins,
appMountParameters,
config,
} = props;
useEffect(() => {
@ -138,11 +135,8 @@ const Application = (props: UptimeAppProps) => {
>
<InspectorContextProvider>
<UptimeAlertsFlyoutWrapper />
<PageRouter config={config} />
<ActionMenu
appMountParameters={appMountParameters}
config={config}
/>
<PageRouter />
<ActionMenu appMountParameters={appMountParameters} />
</InspectorContextProvider>
</RedirectAppLinks>
</div>

View file

@ -9,19 +9,12 @@ import React from 'react';
import { HeaderMenuPortal } from '../../../../../observability/public';
import { AppMountParameters } from '../../../../../../../src/core/public';
import { ActionMenuContent } from './action_menu_content';
import { UptimeConfig } from '../../../../common/config';
export const ActionMenu = ({
appMountParameters,
config,
}: {
appMountParameters: AppMountParameters;
config: UptimeConfig;
}) => (
export const ActionMenu = ({ appMountParameters }: { appMountParameters: AppMountParameters }) => (
<HeaderMenuPortal
setHeaderActionMenu={appMountParameters.setHeaderActionMenu}
theme$={appMountParameters.theme$}
>
<ActionMenuContent config={config} />
<ActionMenuContent />
</HeaderMenuPortal>
);

View file

@ -12,7 +12,7 @@ import { ActionMenuContent } from './action_menu_content';
describe('ActionMenuContent', () => {
it('renders alerts dropdown', async () => {
const { getByLabelText, getByText } = render(<ActionMenuContent config={{}} />);
const { getByLabelText, getByText } = render(<ActionMenuContent />);
const alertsDropdown = getByLabelText('Open alerts and rules context menu');
fireEvent.click(alertsDropdown);
@ -24,7 +24,7 @@ describe('ActionMenuContent', () => {
});
it('renders settings link', () => {
const { getByRole, getByText } = render(<ActionMenuContent config={{}} />);
const { getByRole, getByText } = render(<ActionMenuContent />);
const settingsAnchor = getByRole('link', { name: 'Navigate to the Uptime settings page' });
expect(settingsAnchor.getAttribute('href')).toBe('/settings');
@ -32,7 +32,7 @@ describe('ActionMenuContent', () => {
});
it('renders exploratory view link', () => {
const { getByLabelText, getByText } = render(<ActionMenuContent config={{}} />);
const { getByLabelText, getByText } = render(<ActionMenuContent />);
const analyzeAnchor = getByLabelText(
'Navigate to the "Explore Data" view to visualize Synthetics/User data'
@ -43,7 +43,7 @@ describe('ActionMenuContent', () => {
});
it('renders Add Data link', () => {
const { getByLabelText, getByText } = render(<ActionMenuContent config={{}} />);
const { getByLabelText, getByText } = render(<ActionMenuContent />);
const addDataAnchor = getByLabelText('Navigate to a tutorial about adding Uptime data');

View file

@ -24,7 +24,6 @@ import {
import { stringifyUrlParams } from '../../../lib/helper/stringify_url_params';
import { InspectorHeaderLink } from './inspector_header_link';
import { monitorStatusSelector } from '../../../state/selectors';
import { UptimeConfig } from '../../../../common/config';
const ADD_DATA_LABEL = i18n.translate('xpack.uptime.addDataButtonLabel', {
defaultMessage: 'Add data',
@ -39,7 +38,7 @@ const ANALYZE_MESSAGE = i18n.translate('xpack.uptime.analyzeDataButtonLabel.mess
'Explore Data allows you to select and filter result data in any dimension and look for the cause or impact of performance problems.',
});
export function ActionMenuContent({ config }: { config: UptimeConfig }): React.ReactElement {
export function ActionMenuContent(): React.ReactElement {
const kibana = useKibana();
const { basePath } = useUptimeSettingsContext();
const params = useGetUrlParams();
@ -77,23 +76,21 @@ export function ActionMenuContent({ config }: { config: UptimeConfig }): React.R
return (
<EuiHeaderLinks gutterSize="xs">
{config.ui?.monitorManagement?.enabled && (
<EuiHeaderLink
aria-label={i18n.translate('xpack.uptime.page_header.manageLink.label', {
defaultMessage: 'Navigate to the Uptime monitor management page',
})}
color="text"
data-test-subj="management-page-link"
href={history.createHref({
pathname: MONITOR_MANAGEMENT_ROUTE + '/all',
})}
>
<FormattedMessage
id="xpack.uptime.page_header.manageLink"
defaultMessage="Monitor management"
/>
</EuiHeaderLink>
)}
<EuiHeaderLink
aria-label={i18n.translate('xpack.uptime.page_header.manageLink.label', {
defaultMessage: 'Navigate to the Uptime monitor management page',
})}
color="text"
data-test-subj="management-page-link"
href={history.createHref({
pathname: MONITOR_MANAGEMENT_ROUTE + '/all',
})}
>
<FormattedMessage
id="xpack.uptime.page_header.manageLink"
defaultMessage="Monitor management"
/>
</EuiHeaderLink>
<EuiHeaderLink
aria-label={i18n.translate('xpack.uptime.page_header.settingsLink.label', {

View file

@ -10,15 +10,22 @@ import { i18n } from '@kbn/i18n';
import { EuiButton, EuiFlexItem } from '@elastic/eui';
import { useHistory } from 'react-router-dom';
import { MONITOR_ADD_ROUTE } from '../../../common/constants';
import { useSyntheticsServiceAllowed } from './hooks/use_service_allowed';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
export const AddMonitorBtn = ({ isDisabled }: { isDisabled: boolean }) => {
export const AddMonitorBtn = () => {
const history = useHistory();
const { isAllowed, loading } = useSyntheticsServiceAllowed();
const canSave: boolean = !!useKibana().services?.application?.capabilities.uptime.save;
return (
<EuiFlexItem style={{ alignItems: 'flex-end' }} grow={false} data-test-subj="addMonitorButton">
<EuiButton
fill
isDisabled={isDisabled}
isLoading={loading}
isDisabled={!canSave || !isAllowed}
iconType="plus"
data-test-subj="addMonitorBtn"
href={history.createHref({

View file

@ -71,6 +71,9 @@ describe('useInlineErrors', function () {
list: { monitors: [], page: 1, perPage: 10, total: null },
loading: { monitorList: false, serviceLocations: false },
locations: [],
syntheticsService: {
loading: false,
},
},
1641081600000,
true,

View file

@ -70,6 +70,9 @@ describe('useInlineErrorsCount', function () {
list: { monitors: [], page: 1, perPage: 10, total: null },
loading: { monitorList: false, serviceLocations: false },
locations: [],
syntheticsService: {
loading: false,
},
},
1641081600000,
],

View file

@ -45,6 +45,9 @@ describe('useExpViewTimeRange', function () {
monitorList: false,
serviceLocations: loading,
},
syntheticsService: {
loading: false,
},
},
};

View file

@ -0,0 +1,21 @@
/*
* 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 { useDispatch, useSelector } from 'react-redux';
import { useEffect } from 'react';
import { syntheticsServiceAllowedSelector } from '../../../state/selectors';
import { getSyntheticsServiceAllowed } from '../../../state/actions';
export const useSyntheticsServiceAllowed = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getSyntheticsServiceAllowed.get());
}, [dispatch]);
return useSelector(syntheticsServiceAllowedSelector);
};

View file

@ -6,8 +6,10 @@
*/
import React from 'react';
import { useSelector } from 'react-redux';
import { MonitorManagementList, MonitorManagementListPageState } from './monitor_list';
import { MonitorManagementListResult, Ping } from '../../../../common/runtime_types';
import { monitorManagementListSelector } from '../../../state/selectors';
interface Props {
loading: boolean;
@ -31,6 +33,8 @@ export const InvalidMonitors = ({
const startIndex = (pageIndex - 1) * pageSize;
const monitorList = useSelector(monitorManagementListSelector);
return (
<MonitorManagementList
pageState={pageState}
@ -43,7 +47,8 @@ export const InvalidMonitors = ({
},
error: { monitorList: null, serviceLocations: null },
loading: { monitorList: summariesLoading, serviceLocations: false },
locations: [],
locations: monitorList.locations,
syntheticsService: monitorList.syntheticsService,
}}
onPageStateChange={onPageStateChange}
onUpdate={onUpdate}

View file

@ -51,6 +51,9 @@ describe('<MonitorManagementList />', () => {
monitorList: true,
serviceLocations: false,
},
syntheticsService: {
loading: false,
},
} as MonitorManagementListState,
};

View file

@ -24,7 +24,11 @@ export const TestNowColumn = ({
const testNowRun = useSelector(testNowRunSelector(configId));
if (!configId) {
return <>--</>;
return (
<EuiToolTip content={TEST_NOW_AVAILABLE_LABEL}>
<>--</>
</EuiToolTip>
);
}
const testNowClick = () => {
@ -51,6 +55,13 @@ export const TEST_NOW_ARIA_LABEL = i18n.translate('xpack.uptime.monitorList.test
defaultMessage: 'CLick to run test now',
});
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.',
}
);
export const TEST_NOW_LABEL = i18n.translate('xpack.uptime.monitorList.testNow.label', {
defaultMessage: 'Test now',
});

View file

@ -38,7 +38,6 @@ import { MonitorTags } from '../../common/monitor_tags';
import { useMonitorHistogram } from './use_monitor_histogram';
import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
import { TestNowColumn } from './columns/test_now_col';
import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context';
interface Props extends MonitorListProps {
pageSize: number;
@ -105,8 +104,6 @@ export const MonitorListComponent: ({
}, {});
};
const { config } = useUptimeSettingsContext();
const columns = [
...[
{
@ -209,19 +206,15 @@ export const MonitorListComponent: ({
/>
),
},
...(config.ui?.monitorManagement?.enabled
? [
{
align: 'center' as const,
field: '',
name: TEST_NOW_COLUMN,
width: '100px',
render: (item: MonitorSummary) => (
<TestNowColumn monitorId={item.monitor_id} configId={item.configId} />
),
},
]
: []),
{
align: 'center' as const,
field: '',
name: TEST_NOW_COLUMN,
width: '100px',
render: (item: MonitorSummary) => (
<TestNowColumn monitorId={item.monitor_id} configId={item.configId} />
),
},
...(!hideExtraColumns
? [
{

View file

@ -10,7 +10,6 @@ import { UptimeAppProps } from '../apps/uptime_app';
import { CLIENT_DEFAULTS, CONTEXT_DEFAULTS } from '../../common/constants';
import { CommonlyUsedRange } from '../components/common/uptime_date_picker';
import { useGetUrlParams } from '../hooks';
import { UptimeUiConfig } from '../../common/config';
export interface UptimeSettingsContextValues {
basePath: string;
@ -19,7 +18,6 @@ export interface UptimeSettingsContextValues {
isApmAvailable: boolean;
isInfraAvailable: boolean;
isLogsAvailable: boolean;
config: UptimeUiConfig;
commonlyUsedRanges?: CommonlyUsedRange[];
isDev?: boolean;
}
@ -39,21 +37,13 @@ const defaultContext: UptimeSettingsContextValues = {
isApmAvailable: true,
isInfraAvailable: true,
isLogsAvailable: true,
config: {},
isDev: false,
};
export const UptimeSettingsContext = createContext(defaultContext);
export const UptimeSettingsContextProvider: React.FC<UptimeAppProps> = ({ children, ...props }) => {
const {
basePath,
isApmAvailable,
isInfraAvailable,
isLogsAvailable,
commonlyUsedRanges,
config,
isDev,
} = props;
const { basePath, isApmAvailable, isInfraAvailable, isLogsAvailable, commonlyUsedRanges, isDev } =
props;
const { dateRangeStart, dateRangeEnd } = useGetUrlParams();
@ -65,7 +55,6 @@ export const UptimeSettingsContextProvider: React.FC<UptimeAppProps> = ({ childr
isInfraAvailable,
isLogsAvailable,
commonlyUsedRanges,
config,
dateRangeStart: dateRangeStart ?? DATE_RANGE_START,
dateRangeEnd: dateRangeEnd ?? DATE_RANGE_END,
};
@ -78,7 +67,6 @@ export const UptimeSettingsContextProvider: React.FC<UptimeAppProps> = ({ childr
dateRangeStart,
dateRangeEnd,
commonlyUsedRanges,
config,
]);
return <UptimeSettingsContext.Provider value={value} children={children} />;

View file

@ -77,6 +77,9 @@ export const mockState: AppState = {
monitorList: null,
serviceLocations: null,
},
syntheticsService: {
loading: false,
},
},
ml: {
mlJob: {

View file

@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { render } from '../../lib/helper/rtl_helpers';
import * as allowedHook from '../../components/monitor_management/hooks/use_service_allowed';
import { ServiceAllowedWrapper } from './service_allowed_wrapper';
describe('ServiceAllowedWrapper', () => {
it('renders expected elements for valid props', async () => {
const { findByText } = render(
<ServiceAllowedWrapper>
<div>Test text</div>
</ServiceAllowedWrapper>
);
expect(await findByText('Test text')).toBeInTheDocument();
});
it('renders when enabled state is loading', async () => {
jest.spyOn(allowedHook, 'useSyntheticsServiceAllowed').mockReturnValue({ loading: true });
const { findByText } = render(
<ServiceAllowedWrapper>
<div>Test text</div>
</ServiceAllowedWrapper>
);
expect(await findByText('Loading monitor management')).toBeInTheDocument();
});
it('renders when enabled state is false', async () => {
jest
.spyOn(allowedHook, 'useSyntheticsServiceAllowed')
.mockReturnValue({ loading: false, isAllowed: false });
const { findByText } = render(
<ServiceAllowedWrapper>
<div>Test text</div>
</ServiceAllowedWrapper>
);
expect(await findByText('Monitor management')).toBeInTheDocument();
});
it('renders when enabled state is true', async () => {
jest
.spyOn(allowedHook, 'useSyntheticsServiceAllowed')
.mockReturnValue({ loading: false, isAllowed: true });
const { findByText } = render(
<ServiceAllowedWrapper>
<div>Test text</div>
</ServiceAllowedWrapper>
);
expect(await findByText('Test text')).toBeInTheDocument();
});
});

View file

@ -0,0 +1,66 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButton, EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui';
import { useSyntheticsServiceAllowed } from '../../components/monitor_management/hooks/use_service_allowed';
export const ServiceAllowedWrapper: React.FC = ({ children }) => {
const { isAllowed, loading } = useSyntheticsServiceAllowed();
if (loading) {
return (
<EuiEmptyPrompt
icon={<EuiLoadingLogo logo="logoKibana" size="xl" />}
title={<h2>{LOADING_MONITOR_MANAGEMENT_LABEL}</h2>}
/>
);
}
// checking for explicit false
if (isAllowed === false) {
return (
<EuiEmptyPrompt
title={<h2>{MONITOR_MANAGEMENT_LABEL}</h2>}
body={<p>{PUBLIC_BETA_DESCRIPTION}</p>}
actions={[
<EuiButton color="primary" fill isDisabled={true}>
{REQUEST_ACCESS_LABEL}
</EuiButton>,
]}
/>
);
}
return <>{children}</>;
};
const REQUEST_ACCESS_LABEL = i18n.translate('xpack.uptime.monitorManagement.requestAccess', {
defaultMessage: 'Request access',
});
const MONITOR_MANAGEMENT_LABEL = i18n.translate('xpack.uptime.monitorManagement.label', {
defaultMessage: 'Monitor management',
});
const LOADING_MONITOR_MANAGEMENT_LABEL = i18n.translate(
'xpack.uptime.monitorManagement.loading.label',
{
defaultMessage: 'Loading monitor management',
}
);
const PUBLIC_BETA_DESCRIPTION = i18n.translate(
'xpack.uptime.monitorManagement.publicBetaDescription',
{
defaultMessage:
'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

@ -1,47 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
// app.test.js
import { screen } from '@testing-library/react';
import { render } from './lib/helper/rtl_helpers';
import { createMemoryHistory } from 'history';
import React from 'react';
import * as telemetry from './hooks/use_telemetry';
import { MONITOR_ADD_ROUTE, MONITOR_EDIT_ROUTE } from '../common/constants';
import '@testing-library/jest-dom';
import { PageRouter } from './routes';
describe('PageRouter', () => {
beforeEach(() => {
jest.spyOn(telemetry, 'useUptimeTelemetry').mockImplementation(() => {});
});
it.each([MONITOR_ADD_ROUTE, MONITOR_EDIT_ROUTE])(
'hides ui monitor management pages when feature flag is not enabled',
(page) => {
const history = createMemoryHistory();
history.push(page);
render(<PageRouter config={{}} />, { history });
expect(screen.getByText(/Page not found/i)).toBeInTheDocument();
}
);
it.each([
[MONITOR_ADD_ROUTE, 'Add Monitor'],
[MONITOR_EDIT_ROUTE, 'Edit Monitor'],
])('hides ui monitor management pages when feature flag is not enabled', (page, heading) => {
const history = createMemoryHistory();
history.push(page);
render(<PageRouter config={{ ui: { monitorManagement: { enabled: true } } }} />, {
history,
});
expect(screen.getByText(heading)).toBeInTheDocument();
});
});

View file

@ -55,14 +55,9 @@ import {
import { UptimePageTemplateComponent } from './apps/uptime_page_template';
import { apiService } from './state/api/utils';
import { useInspectorContext } from '../../observability/public';
import { UptimeConfig } from '../common/config';
import { AddMonitorBtn } from './components/monitor_management/add_monitor_btn';
import { useKibana } from '../../../../src/plugins/kibana_react/public';
import { SettingsBottomBar } from './components/settings/settings_bottom_bar';
interface PageRouterProps {
config: UptimeConfig;
}
import { ServiceAllowedWrapper } from './pages/monitor_management/service_allowed_wrapper';
type RouteProps = {
path: string;
@ -85,7 +80,7 @@ export const MONITORING_OVERVIEW_LABEL = i18n.translate('xpack.uptime.overview.h
defaultMessage: 'Monitors',
});
const getRoutes = (config: UptimeConfig, canSave: boolean): RouteProps[] => {
const getRoutes = (): RouteProps[] => {
return [
{
title: i18n.translate('xpack.uptime.monitorRoute.title', {
@ -190,69 +185,77 @@ const getRoutes = (config: UptimeConfig, canSave: boolean): RouteProps[] => {
rightSideItems: [],
},
},
...(config.ui?.monitorManagement?.enabled
? [
{
title: i18n.translate('xpack.uptime.addMonitorRoute.title', {
defaultMessage: 'Add Monitor | {baseTitle}',
values: { baseTitle },
}),
path: MONITOR_ADD_ROUTE,
component: AddMonitorPage,
dataTestSubj: 'uptimeMonitorAddPage',
telemetryId: UptimePage.MonitorAdd,
pageHeader: {
pageTitle: (
<FormattedMessage
id="xpack.uptime.addMonitor.pageHeader.title"
defaultMessage="Add Monitor"
/>
),
},
bottomBar: <MonitorManagementBottomBar />,
bottomBarProps: { paddingSize: 'm' as const },
},
{
title: i18n.translate('xpack.uptime.editMonitorRoute.title', {
defaultMessage: 'Edit Monitor | {baseTitle}',
values: { baseTitle },
}),
path: MONITOR_EDIT_ROUTE,
component: EditMonitorPage,
dataTestSubj: 'uptimeMonitorEditPage',
telemetryId: UptimePage.MonitorEdit,
pageHeader: {
pageTitle: (
<FormattedMessage
id="xpack.uptime.editMonitor.pageHeader.title"
defaultMessage="Edit Monitor"
/>
),
},
bottomBar: <MonitorManagementBottomBar />,
bottomBarProps: { paddingSize: 'm' as const },
},
{
title: i18n.translate('xpack.uptime.monitorManagementRoute.title', {
defaultMessage: 'Manage Monitors | {baseTitle}',
values: { baseTitle },
}),
path: MONITOR_MANAGEMENT_ROUTE + '/:type',
component: MonitorManagementPage,
dataTestSubj: 'uptimeMonitorManagementListPage',
telemetryId: UptimePage.MonitorManagement,
pageHeader: {
pageTitle: (
<FormattedMessage
id="xpack.uptime.monitorManagement.pageHeader.title"
defaultMessage="Manage monitors"
/>
),
rightSideItems: [<AddMonitorBtn isDisabled={!canSave} />],
},
},
]
: []),
{
title: i18n.translate('xpack.uptime.addMonitorRoute.title', {
defaultMessage: 'Add Monitor | {baseTitle}',
values: { baseTitle },
}),
path: MONITOR_ADD_ROUTE,
component: () => (
<ServiceAllowedWrapper>
<AddMonitorPage />
</ServiceAllowedWrapper>
),
dataTestSubj: 'uptimeMonitorAddPage',
telemetryId: UptimePage.MonitorAdd,
pageHeader: {
pageTitle: (
<FormattedMessage
id="xpack.uptime.addMonitor.pageHeader.title"
defaultMessage="Add Monitor"
/>
),
},
bottomBar: <MonitorManagementBottomBar />,
bottomBarProps: { paddingSize: 'm' as const },
},
{
title: i18n.translate('xpack.uptime.editMonitorRoute.title', {
defaultMessage: 'Edit Monitor | {baseTitle}',
values: { baseTitle },
}),
path: MONITOR_EDIT_ROUTE,
component: () => (
<ServiceAllowedWrapper>
<EditMonitorPage />
</ServiceAllowedWrapper>
),
dataTestSubj: 'uptimeMonitorEditPage',
telemetryId: UptimePage.MonitorEdit,
pageHeader: {
pageTitle: (
<FormattedMessage
id="xpack.uptime.editMonitor.pageHeader.title"
defaultMessage="Edit Monitor"
/>
),
},
bottomBar: <MonitorManagementBottomBar />,
bottomBarProps: { paddingSize: 'm' as const },
},
{
title: i18n.translate('xpack.uptime.monitorManagementRoute.title', {
defaultMessage: 'Manage Monitors | {baseTitle}',
values: { baseTitle },
}),
path: MONITOR_MANAGEMENT_ROUTE + '/:type',
component: () => (
<ServiceAllowedWrapper>
<MonitorManagementPage />
</ServiceAllowedWrapper>
),
dataTestSubj: 'uptimeMonitorManagementListPage',
telemetryId: UptimePage.MonitorManagement,
pageHeader: {
pageTitle: (
<FormattedMessage
id="xpack.uptime.monitorManagement.pageHeader.title"
defaultMessage="Manage monitors"
/>
),
rightSideItems: [<AddMonitorBtn />],
},
},
];
};
@ -268,10 +271,8 @@ const RouteInit: React.FC<Pick<RouteProps, 'path' | 'title' | 'telemetryId'>> =
return null;
};
export const PageRouter: FC<PageRouterProps> = ({ config = {} }) => {
const canSave: boolean = !!useKibana().services?.application?.capabilities.uptime.save;
const routes = getRoutes(config, canSave);
export const PageRouter: FC = () => {
const routes = getRoutes();
const { addInspectorRequest } = useInspectorContext();
apiService.addInspectorRequest = addInspectorRequest;

View file

@ -11,6 +11,8 @@ import {
ServiceLocations,
FetchMonitorManagementListQueryArgs,
} from '../../../common/runtime_types';
import { createAsyncAction } from './utils';
import { SyntheticsServiceAllowed } from '../../../common/types';
export const getMonitors = createAction<FetchMonitorManagementListQueryArgs>(
'GET_MONITOR_MANAGEMENT_LIST'
@ -25,3 +27,7 @@ export const getServiceLocationsSuccess = createAction<ServiceLocations>(
'GET_SERVICE_LOCATIONS_LIST_SUCCESS'
);
export const getServiceLocationsFailure = createAction<Error>('GET_SERVICE_LOCATIONS_LIST_FAILURE');
export const getSyntheticsServiceAllowed = createAsyncAction<void, SyntheticsServiceAllowed>(
'GET_SYNTHETICS_SERVICE_ALLOWED'
);

View file

@ -15,7 +15,7 @@ import {
ServiceLocationsApiResponseCodec,
ServiceLocationErrors,
} from '../../../common/runtime_types';
import { SyntheticsMonitorSavedObject } from '../../../common/types';
import { SyntheticsMonitorSavedObject, SyntheticsServiceAllowed } from '../../../common/types';
import { apiService } from './utils';
export const setMonitor = async ({
@ -78,3 +78,7 @@ export interface TestNowResponse {
export const testNowMonitor = async (configId: string): Promise<TestNowResponse | undefined> => {
return await apiService.get(API_URLS.TRIGGER_MONITOR + `/${configId}`);
};
export const fetchServiceAllowed = async (): Promise<SyntheticsServiceAllowed> => {
return await apiService.get(API_URLS.SERVICE_ALLOWED);
};

View file

@ -12,7 +12,10 @@ import {
fetchRunNowMonitorEffect,
fetchUpdatedMonitorEffect,
} from './monitor_list';
import { fetchMonitorManagementEffect } from './monitor_management';
import {
fetchMonitorManagementEffect,
fetchSyntheticsServiceAllowedEffect,
} from './monitor_management';
import { fetchMonitorStatusEffect } from './monitor_status';
import { fetchDynamicSettingsEffect, setDynamicSettingsEffect } from './dynamic_settings';
import { fetchPingsEffect, fetchPingHistogramEffect } from './ping';
@ -48,4 +51,5 @@ export function* rootEffect() {
yield fork(generateBlockStatsOnPut);
yield fork(pruneBlockCache);
yield fork(fetchRunNowMonitorEffect);
yield fork(fetchSyntheticsServiceAllowedEffect);
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { takeLatest } from 'redux-saga/effects';
import { takeLatest, takeLeading } from 'redux-saga/effects';
import {
getMonitors,
getMonitorsSuccess,
@ -13,8 +13,9 @@ import {
getServiceLocations,
getServiceLocationsSuccess,
getServiceLocationsFailure,
getSyntheticsServiceAllowed,
} from '../actions';
import { fetchMonitorManagementList, fetchServiceLocations } from '../api';
import { fetchMonitorManagementList, fetchServiceAllowed, fetchServiceLocations } from '../api';
import { fetchEffectFactory } from './fetch_effect';
export function* fetchMonitorManagementEffect() {
@ -31,3 +32,14 @@ export function* fetchMonitorManagementEffect() {
)
);
}
export function* fetchSyntheticsServiceAllowedEffect() {
yield takeLeading(
getSyntheticsServiceAllowed.get,
fetchEffectFactory(
fetchServiceAllowed,
getSyntheticsServiceAllowed.success,
getSyntheticsServiceAllowed.fail
)
);
}

View file

@ -14,14 +14,17 @@ import {
getServiceLocations,
getServiceLocationsSuccess,
getServiceLocationsFailure,
getSyntheticsServiceAllowed,
} from '../actions';
import { MonitorManagementListResult, ServiceLocations } from '../../../common/runtime_types';
import { SyntheticsServiceAllowed } from '../../../common/types';
export interface MonitorManagementList {
error: Record<'monitorList' | 'serviceLocations', Error | null>;
loading: Record<'monitorList' | 'serviceLocations', boolean>;
list: MonitorManagementListResult;
locations: ServiceLocations;
syntheticsService: { isAllowed?: boolean; loading: boolean };
}
export const initialState: MonitorManagementList = {
@ -40,6 +43,9 @@ export const initialState: MonitorManagementList = {
monitorList: null,
serviceLocations: null,
},
syntheticsService: {
loading: false,
},
};
export const monitorManagementListReducer = createReducer(initialState, (builder) => {
@ -118,5 +124,38 @@ export const monitorManagementListReducer = createReducer(initialState, (builder
serviceLocations: action.payload,
},
})
)
.addCase(
String(getSyntheticsServiceAllowed.get),
(state: WritableDraft<MonitorManagementList>) => ({
...state,
syntheticsService: {
isAllowed: state.syntheticsService?.isAllowed,
loading: true,
},
})
)
.addCase(
String(getSyntheticsServiceAllowed.success),
(
state: WritableDraft<MonitorManagementList>,
action: PayloadAction<SyntheticsServiceAllowed>
) => ({
...state,
syntheticsService: {
isAllowed: action.payload.serviceAllowed,
loading: false,
},
})
)
.addCase(
String(getSyntheticsServiceAllowed.fail),
(state: WritableDraft<MonitorManagementList>, action: PayloadAction<Error>) => ({
...state,
syntheticsService: {
isAllowed: false,
loading: false,
},
})
);
});

View file

@ -94,3 +94,6 @@ export const networkEventsSelector = ({ networkEvents }: AppState) => networkEve
export const syntheticsSelector = ({ synthetics }: AppState) => synthetics;
export const uptimeWriteSelector = (state: AppState) => state;
export const syntheticsServiceAllowedSelector = (state: AppState) =>
state.monitorManagementList.syntheticsService;

View file

@ -60,7 +60,8 @@ export function createUptimeESClient({
baseESClient: esClient,
async search<DocumentSource extends unknown, TParams extends estypes.SearchRequest>(
params: TParams,
operationName?: string
operationName?: string,
index?: string
): Promise<{ body: ESSearchResponse<DocumentSource, TParams> }> {
let res: any;
let esError: any;
@ -68,7 +69,7 @@ export function createUptimeESClient({
savedObjectsClient!
);
const esParams = { index: dynamicSettings!.heartbeatIndices, ...params };
const esParams = { index: index ?? dynamicSettings!.heartbeatIndices, ...params };
const startTime = process.hrtime();
const startTimeNow = Date.now();
@ -84,6 +85,7 @@ export function createUptimeESClient({
}
const inspectableEsQueries = inspectableEsQueriesMap.get(request!);
if (inspectableEsQueries) {
inspectableEsQueries.push(
getInspectResponse({

View file

@ -85,44 +85,48 @@ export const hydrateSavedObjects = async ({
};
const fetchSampleMonitorDocuments = async (esClient: UptimeESClient, configIds: string[]) => {
const data = await esClient.search({
body: {
query: {
bool: {
filter: [
{
range: {
'@timestamp': {
gte: 'now-15m',
lt: 'now',
const data = await esClient.search(
{
body: {
query: {
bool: {
filter: [
{
range: {
'@timestamp': {
gte: 'now-15m',
lt: 'now',
},
},
},
},
{
terms: {
config_id: configIds,
{
terms: {
config_id: configIds,
},
},
},
{
exists: {
field: 'summary',
{
exists: {
field: 'summary',
},
},
},
{
bool: {
minimum_should_match: 1,
should: [{ exists: { field: 'url.full' } }, { exists: { field: 'url.port' } }],
{
bool: {
minimum_should_match: 1,
should: [{ exists: { field: 'url.full' } }, { exists: { field: 'url.port' } }],
},
},
},
],
],
},
},
_source: ['url', 'config_id', '@timestamp'],
collapse: {
field: 'config_id',
},
},
_source: ['url', 'config_id', '@timestamp'],
collapse: {
field: 'config_id',
},
},
});
'getHydrateQuery',
'synthetics-*'
);
return data.body.hits.hits.map(
({ _source: doc }) => ({ ...(doc as any), timestamp: (doc as any)['@timestamp'] } as Ping)

View file

@ -85,6 +85,38 @@ export class ServiceAPIClient {
return this.callAPI('POST', { ...data, runOnce: true });
}
async checkIfAccountAllowed() {
if (this.authorization) {
// in case username/password is provided, we assume it's always allowed
return true;
}
const httpsAgent = this.getHttpsAgent();
if (this.locations.length > 0 && httpsAgent) {
// get a url from a random location
const url = this.locations[Math.floor(Math.random() * this.locations.length)].url;
try {
const { data } = await axios({
method: 'GET',
url: url + '/allowed',
headers:
process.env.NODE_ENV !== 'production' && this.authorization
? {
Authorization: this.authorization,
}
: undefined,
httpsAgent,
});
return data.allowed;
} catch (e) {
this.logger.error(e);
}
}
return false;
}
async callAPI(
method: 'POST' | 'PUT' | 'DELETE',
{ monitors: allMonitors, output, runOnce }: ServiceData

View file

@ -0,0 +1,68 @@
/*
* 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 { SyntheticsService } from './synthetics_service';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { loggerMock } from './../../../../../../src/core/server/logging/logger.mock';
import { UptimeServerSetup } from '../adapters';
describe('SyntheticsService', () => {
const mockEsClient = {
search: jest.fn(),
};
const serverMock: UptimeServerSetup = {
uptimeEsClient: mockEsClient,
authSavedObjectsClient: {
bulkUpdate: jest.fn(),
},
} as unknown as UptimeServerSetup;
const logger = loggerMock.create();
it('inits properly', async () => {
const service = new SyntheticsService(logger, serverMock, {});
service.init();
expect(service.isAllowed).toEqual(false);
expect(service.locations).toEqual([]);
});
it('inits properly with basic auth', async () => {
const service = new SyntheticsService(logger, serverMock, {
username: 'dev',
password: '12345',
});
await service.init();
expect(service.isAllowed).toEqual(true);
});
it('inits properly with locations with dev', async () => {
serverMock.config = { service: { devUrl: 'http://localhost' } };
const service = new SyntheticsService(logger, serverMock, {
username: 'dev',
password: '12345',
});
await service.init();
expect(service.isAllowed).toEqual(true);
expect(service.locations).toEqual([
{
geo: {
lat: 0,
lon: 0,
},
id: 'localhost',
label: 'Local Synthetics Service',
url: 'http://localhost',
},
]);
});
});

View file

@ -54,28 +54,25 @@ export class SyntheticsService {
private indexTemplateExists?: boolean;
private indexTemplateInstalling?: boolean;
public isAllowed: boolean;
constructor(logger: Logger, server: UptimeServerSetup, config: ServiceConfig) {
this.logger = logger;
this.server = server;
this.config = config;
this.isAllowed = false;
this.apiClient = new ServiceAPIClient(logger, this.config, this.server.kibanaVersion);
this.esHosts = getEsHosts({ config: this.config, cloud: server.cloud });
this.locations = [];
this.registerServiceLocations();
}
public init() {
// TODO: Figure out fake kibana requests to handle API keys on start up
// getAPIKeyForSyntheticsService({ server: this.server }).then((apiKey) => {
// if (apiKey) {
// this.apiKey = apiKey;
// }
// });
this.setupIndexTemplates();
public async init() {
await this.registerServiceLocations();
this.isAllowed = await this.apiClient.checkIfAccountAllowed();
}
private setupIndexTemplates() {
@ -105,12 +102,15 @@ export class SyntheticsService {
}
}
public registerServiceLocations() {
public async registerServiceLocations() {
const service = this;
getServiceLocations(service.server).then((result) => {
try {
const result = await getServiceLocations(service.server);
service.locations = result.locations;
service.apiClient.locations = result.locations;
});
} catch (e) {
this.logger.error(e);
}
}
public registerSyncTask(taskManager: TaskManagerSetupContract) {
@ -130,10 +130,14 @@ export class SyntheticsService {
async run() {
const { state } = taskInstance;
service.setupIndexTemplates();
service.registerServiceLocations();
await service.registerServiceLocations();
await service.pushConfigs();
service.isAllowed = await service.apiClient.checkIfAccountAllowed();
if (service.isAllowed) {
service.setupIndexTemplates();
await service.pushConfigs();
}
return { state };
},

View file

@ -39,14 +39,11 @@ export class Plugin implements PluginType {
private server?: UptimeServerSetup;
private syntheticService?: SyntheticsService;
private readonly telemetryEventsSender: TelemetryEventsSender;
private readonly isServiceEnabled?: boolean;
constructor(initializerContext: PluginInitializerContext<UptimeConfig>) {
this.initContext = initializerContext;
this.logger = initializerContext.logger.get();
this.telemetryEventsSender = new TelemetryEventsSender(this.logger);
const config = this.initContext.config.get<UptimeConfig>();
this.isServiceEnabled = config?.ui?.monitorManagement?.enabled && Boolean(config.service);
}
public setup(core: CoreSetup, plugins: UptimeCorePluginsSetup) {
@ -84,7 +81,7 @@ export class Plugin implements PluginType {
isDev: this.initContext.env.mode.dev,
} as UptimeServerSetup;
if (this.isServiceEnabled && this.server.config.service) {
if (this.server.config.service) {
this.syntheticService = new SyntheticsService(
this.logger,
this.server,
@ -100,7 +97,7 @@ export class Plugin implements PluginType {
registerUptimeSavedObjects(
core.savedObjects,
plugins.encryptedSavedObjects,
Boolean(this.isServiceEnabled)
Boolean(this.server.config.service)
);
KibanaTelemetryAdapter.registerUsageCollector(
@ -114,7 +111,7 @@ export class Plugin implements PluginType {
}
public start(coreStart: CoreStart, plugins: UptimeCorePluginsStart) {
if (this.isServiceEnabled) {
if (this.server?.config.service) {
this.savedObjectsClient = new SavedObjectsClient(
coreStart.savedObjects.createInternalRepository([syntheticsServiceApiKey.name])
);
@ -131,7 +128,7 @@ export class Plugin implements PluginType {
this.server.savedObjectsClient = this.savedObjectsClient;
}
if (this.isServiceEnabled) {
if (this.server?.config.service) {
this.syntheticService?.init();
this.syntheticService?.scheduleSyncTask(plugins.taskManager);
if (this.server && this.syntheticService) {

View file

@ -38,6 +38,7 @@ 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 { getServiceAllowedRoute } from './synthetics_service/get_service_allowed';
export * from './types';
export { createRouteWithAuth } from './create_route_with_auth';
@ -71,4 +72,5 @@ export const restApiRoutes: UMRestApiRouteFactory[] = [
deleteSyntheticsMonitorRoute,
runOnceSyntheticsMonitorRoute,
testNowMonitorRoute,
getServiceAllowedRoute,
];

View 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
* 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';
export const getServiceAllowedRoute: UMRestApiRouteFactory = () => ({
method: 'GET',
path: API_URLS.SERVICE_ALLOWED,
validate: {},
handler: async ({ server }): Promise<any> => {
return { serviceAllowed: server.syntheticsService.isAllowed };
},
});

View file

@ -23,7 +23,7 @@ export const uptimeRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute, server) =>
handler: async (context, request, response) => {
const { client: esClient } = context.core.elasticsearch;
let savedObjectsClient: SavedObjectsClientContract;
if (server.config?.ui?.monitorManagement?.enabled) {
if (server.config?.service) {
savedObjectsClient = context.core.savedObjects.getClient({
includedHiddenTypes: [syntheticsServiceApiKey.name],
});

View file

@ -35,7 +35,6 @@ export async function getApiIntegrationConfig({ readConfigFile }: FtrConfigProvi
'--xpack.ruleRegistry.write.enabled=true',
'--xpack.ruleRegistry.write.enabled=true',
'--xpack.ruleRegistry.write.cache.enabled=false',
'--xpack.uptime.ui.monitorManagement.enabled=true',
'--xpack.uptime.service.password=test',
'--xpack.uptime.service.username=localKibanaIntegrationTestsUser',
`--xpack.securitySolution.enableExperimental=${JSON.stringify(['ruleRegistryEnabled'])}`,