diff --git a/x-pack/legacy/plugins/uptime/common/constants/ui.ts b/x-pack/legacy/plugins/uptime/common/constants/ui.ts index 8389d86fd2072..8d223dbbba556 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/ui.ts +++ b/x-pack/legacy/plugins/uptime/common/constants/ui.ts @@ -8,6 +8,8 @@ export const MONITOR_ROUTE = '/monitor/:monitorId?'; export const OVERVIEW_ROUTE = '/'; +export const SETTINGS_ROUTE = '/settings'; + export enum STATUS { UP = 'up', DOWN = 'down', diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts new file mode 100644 index 0000000000000..8dedd4672eeae --- /dev/null +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +export const DynamicSettingsType = t.type({ + heartbeatIndices: t.string, +}); + +export const DynamicSettingsSaveType = t.intersection([ + t.type({ + success: t.boolean, + }), + t.partial({ + error: t.string, + }), +]); + +export type DynamicSettings = t.TypeOf; +export type DynamicSettingsSaveResponse = t.TypeOf; + +export const defaultDynamicSettings: DynamicSettings = { + heartbeatIndices: 'heartbeat-8*', +}; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts index 82fc9807300ed..5e3fb2326bdb9 100644 --- a/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts @@ -9,3 +9,4 @@ export * from './common'; export * from './monitor'; export * from './overview_filters'; export * from './snapshot'; +export * from './dynamic_settings'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/data_missing.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/data_missing.test.tsx.snap index 36c54758cf116..2182bfb4e656c 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/data_missing.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/data_missing.test.tsx.snap @@ -2,6 +2,7 @@ exports[`DataMissing component renders basePath and headingMessage 1`] = `
{ const { basePath } = useContext(UptimeSettingsContext); return ( - + diff --git a/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_breadcrumbs.test.tsx b/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_breadcrumbs.test.tsx new file mode 100644 index 0000000000000..85961003fce72 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_breadcrumbs.test.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ChromeBreadcrumb } from 'kibana/public'; +import React from 'react'; +import { Route } from 'react-router-dom'; +import { mountWithRouter } from '../../lib'; +import { OVERVIEW_ROUTE } from '../../../common/constants'; +import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; +import { UptimeUrlParams, getSupportedUrlParams } from '../../lib/helper'; +import { makeBaseBreadcrumb, useBreadcrumbs } from '../../hooks/use_breadcrumbs'; + +describe('useBreadcrumbs', () => { + it('sets the given breadcrumbs', () => { + const [getBreadcrumbs, core] = mockCore(); + + const expectedCrumbs: ChromeBreadcrumb[] = [ + { + text: 'Crumb: ', + href: 'http://href.example.net', + }, + { + text: 'Crumb II: Son of Crumb', + href: 'http://href2.example.net', + }, + ]; + + const Component = () => { + useBreadcrumbs(expectedCrumbs); + return <>Hello; + }; + + mountWithRouter( + + + + + + ); + + const urlParams: UptimeUrlParams = getSupportedUrlParams({}); + expect(getBreadcrumbs()).toStrictEqual([makeBaseBreadcrumb(urlParams)].concat(expectedCrumbs)); + }); +}); + +const mockCore: () => [() => ChromeBreadcrumb[], any] = () => { + let breadcrumbObj: ChromeBreadcrumb[] = []; + const get = () => { + return breadcrumbObj; + }; + const core = { + chrome: { + setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => { + breadcrumbObj = newBreadcrumbs; + }, + }, + }; + + return [get, core]; +}; diff --git a/x-pack/legacy/plugins/uptime/public/hooks/use_breadcrumbs.ts b/x-pack/legacy/plugins/uptime/public/hooks/use_breadcrumbs.ts new file mode 100644 index 0000000000000..d1cc8e1897386 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/hooks/use_breadcrumbs.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ChromeBreadcrumb } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { useEffect } from 'react'; +import { UptimeUrlParams } from '../lib/helper'; +import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { useUrlParams } from '.'; + +export const makeBaseBreadcrumb = (params?: UptimeUrlParams): ChromeBreadcrumb => { + let href = '#/'; + if (params) { + const crumbParams: Partial = { ...params }; + // We don't want to encode this values because they are often set to Date.now(), the relative + // values in dateRangeStart are better for a URL. + delete crumbParams.absoluteDateRangeStart; + delete crumbParams.absoluteDateRangeEnd; + href += stringifyUrlParams(crumbParams, true); + } + return { + text: i18n.translate('xpack.uptime.breadcrumbs.overviewBreadcrumbText', { + defaultMessage: 'Uptime', + }), + href, + }; +}; + +export const useBreadcrumbs = (extraCrumbs: ChromeBreadcrumb[]) => { + const params = useUrlParams()[0](); + const setBreadcrumbs = useKibana().services.chrome?.setBreadcrumbs; + useEffect(() => { + if (setBreadcrumbs) { + setBreadcrumbs([makeBaseBreadcrumb(params)].concat(extraCrumbs)); + } + }, [extraCrumbs, params, setBreadcrumbs]); +}; diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap index 30e15ba132996..646bfeba951dd 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap @@ -1,12 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PageHeader shallow renders with breadcrumbs and the date picker: page_header_with_date_picker 1`] = ` +exports[`PageHeader shallow renders extra links: page_header_with_extra_links 1`] = ` Array [

-
- -
-
-

-
-
- + +
-
-
+
- - +
+
+
+
+ +
+
+
+
+
- Refresh + - - - +
+
+ @@ -149,13 +197,13 @@ Array [ ] `; -exports[`PageHeader shallow renders with breadcrumbs without the date picker: page_header_no_date_picker 1`] = ` +exports[`PageHeader shallow renders with the date picker: page_header_with_date_picker 1`] = ` Array [

+
- +
+
+
+
+ +
+
+
+

+
- Alerts - - - + + + +
+
+ @@ -202,3 +327,38 @@ Array [ />, ] `; + +exports[`PageHeader shallow renders without the date picker: page_header_no_date_picker 1`] = ` +Array [ +
+
+

+ TestingHeading +

+
+
+
+
+
+
+
+
, +
, +] +`; diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/page_header.test.tsx b/x-pack/legacy/plugins/uptime/public/pages/__tests__/page_header.test.tsx index 92dceece3ef40..c9e4eef386764 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/__tests__/page_header.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/__tests__/page_header.test.tsx @@ -5,67 +5,36 @@ */ import React from 'react'; -import { Route } from 'react-router-dom'; -import { PageHeader, makeBaseBreadcrumb } from '../page_header'; -import { mountWithRouter, renderWithRouter } from '../../lib'; -import { OVERVIEW_ROUTE } from '../../../common/constants'; -import { ChromeBreadcrumb } from 'kibana/public'; -import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; -import { UptimeUrlParams, getSupportedUrlParams } from '../../lib/helper'; +import { PageHeader } from '../page_header'; +import { renderWithRouter } from '../../lib'; import { Provider } from 'react-redux'; describe('PageHeader', () => { - const simpleBreadcrumbs: ChromeBreadcrumb[] = [ - { text: 'TestCrumb1', href: '#testHref1' }, - { text: 'TestCrumb2', href: '#testHref2' }, - ]; - - it('shallow renders with breadcrumbs and the date picker', () => { + it('shallow renders with the date picker', () => { const component = renderWithRouter( - + ); expect(component).toMatchSnapshot('page_header_with_date_picker'); }); - it('shallow renders with breadcrumbs without the date picker', () => { + it('shallow renders without the date picker', () => { const component = renderWithRouter( - + ); expect(component).toMatchSnapshot('page_header_no_date_picker'); }); - it('sets the given breadcrumbs', () => { - const [getBreadcrumbs, core] = mockCore(); - mountWithRouter( - - - - - - - - ); - - const urlParams: UptimeUrlParams = getSupportedUrlParams({}); - expect(getBreadcrumbs()).toStrictEqual( - [makeBaseBreadcrumb(urlParams)].concat(simpleBreadcrumbs) + it('shallow renders extra links', () => { + const component = renderWithRouter( + + + ); + expect(component).toMatchSnapshot('page_header_with_extra_links'); }); }); @@ -81,19 +50,3 @@ const MockReduxProvider = ({ children }: { children: React.ReactElement }) => ( {children} ); - -const mockCore: () => [() => ChromeBreadcrumb[], any] = () => { - let breadcrumbObj: ChromeBreadcrumb[] = []; - const get = () => { - return breadcrumbObj; - }; - const core = { - chrome: { - setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => { - breadcrumbObj = newBreadcrumbs; - }, - }, - }; - - return [get, core]; -}; diff --git a/x-pack/legacy/plugins/uptime/public/pages/index.ts b/x-pack/legacy/plugins/uptime/public/pages/index.ts index 3f74bda79bd46..cea47d6ccf79c 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/index.ts +++ b/x-pack/legacy/plugins/uptime/public/pages/index.ts @@ -5,4 +5,5 @@ */ export { MonitorPage } from './monitor'; +export { SettingsPage } from './settings'; export { NotFoundPage } from './not_found'; diff --git a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx index b9d29ed017a05..5871783dffdeb 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx @@ -7,7 +7,6 @@ import { EuiSpacer } from '@elastic/eui'; import React, { useContext, useState, useEffect } from 'react'; import { useParams } from 'react-router-dom'; -import { ChromeBreadcrumb } from 'kibana/public'; import { connect, MapDispatchToPropsFunction, MapStateToPropsParam } from 'react-redux'; import { MonitorCharts, PingList } from '../components/functional'; import { UptimeRefreshContext } from '../contexts'; @@ -19,6 +18,7 @@ import { AppState } from '../state'; import { selectSelectedMonitor } from '../state/selectors'; import { getSelectedMonitorAction } from '../state/actions'; import { PageHeader } from './page_header'; +import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; interface StateProps { selectedMonitor: Ping | null; @@ -65,10 +65,10 @@ export const MonitorPageComponent: React.FC = ({ useTrackPageview({ app: 'uptime', path: 'monitor', delay: 15000 }); const nameOrId = selectedMonitor?.monitor?.name || selectedMonitor?.monitor?.id || ''; - const breadcrumbs: ChromeBreadcrumb[] = [{ text: nameOrId }]; + useBreadcrumbs([{ text: nameOrId }]); return ( <> - + diff --git a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx index f9184e2a0587f..a8a35fd2681b6 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx @@ -21,6 +21,7 @@ import { UptimeThemeContext } from '../contexts'; import { EmptyState, FilterGroup, KueryBar } from '../components/connected'; import { useUpdateKueryString } from '../hooks'; import { PageHeader } from './page_header'; +import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; interface OverviewPageProps { autocomplete: DataPublicPluginSetup['autocomplete']; @@ -77,9 +78,10 @@ export const OverviewPageComponent = ({ autocomplete, indexPattern, setEsKueryFi description: `The text that will be displayed in the app's heading when the Overview page loads.`, }); + useBreadcrumbs([]); // No extra breadcrumbs on overview return ( <> - + diff --git a/x-pack/legacy/plugins/uptime/public/pages/page_header.tsx b/x-pack/legacy/plugins/uptime/public/pages/page_header.tsx index 56d9ae2d5caa6..821a70c85dc7c 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/page_header.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/page_header.tsx @@ -4,69 +4,63 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect } from 'react'; -import { ChromeBreadcrumb } from 'kibana/public'; -import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer } from '@elastic/eui'; +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer, EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { Link } from 'react-router-dom'; import { UptimeDatePicker } from '../components/functional/uptime_date_picker'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; -import { useUrlParams } from '../hooks'; -import { UptimeUrlParams } from '../lib/helper'; +import { SETTINGS_ROUTE } from '../../common/constants'; import { ToggleAlertFlyoutButton } from '../components/connected'; interface PageHeaderProps { headingText: string; - breadcrumbs: ChromeBreadcrumb[]; - datePicker: boolean; + extraLinks?: boolean; + datePicker?: boolean; } -export const makeBaseBreadcrumb = (params?: UptimeUrlParams): ChromeBreadcrumb => { - let href = '#/'; - if (params) { - const crumbParams: Partial = { ...params }; - // We don't want to encode this values because they are often set to Date.now(), the relative - // values in dateRangeStart are better for a URL. - delete crumbParams.absoluteDateRangeStart; - delete crumbParams.absoluteDateRangeEnd; - href += stringifyUrlParams(crumbParams, true); - } - return { - text: i18n.translate('xpack.uptime.breadcrumbs.overviewBreadcrumbText', { - defaultMessage: 'Uptime', - }), - href, - }; -}; - -export const PageHeader = ({ headingText, breadcrumbs, datePicker = true }: PageHeaderProps) => { - const setBreadcrumbs = useKibana().services.chrome?.setBreadcrumbs!; - - const params = useUrlParams()[0](); - useEffect(() => { - setBreadcrumbs([makeBaseBreadcrumb(params)].concat(breadcrumbs)); - }, [breadcrumbs, params, setBreadcrumbs]); +export const PageHeader = React.memo( + ({ headingText, extraLinks = false, datePicker = true }: PageHeaderProps) => { + const datePickerComponent = datePicker ? ( + + + + ) : null; - const datePickerComponent = datePicker ? ( - - - - ) : null; - - return ( - <> - - - -

{headingText}

-
-
+ const settingsLinkText = i18n.translate('xpack.uptime.page_header.settingsLink', { + defaultMessage: 'Settings', + }); + const extraLinkComponents = !extraLinks ? null : ( + - {datePickerComponent} + + + + {settingsLinkText} + + + - - - ); -}; + ); + + return ( + <> + + + +

{headingText}

+
+
+ + + {extraLinkComponents} + {datePickerComponent} + + +
+ + + ); + } +); diff --git a/x-pack/legacy/plugins/uptime/public/pages/settings.tsx b/x-pack/legacy/plugins/uptime/public/pages/settings.tsx new file mode 100644 index 0000000000000..679a61686e435 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/pages/settings.tsx @@ -0,0 +1,241 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState } from 'react'; +import { + EuiForm, + EuiTitle, + EuiSpacer, + EuiDescribedFormGroup, + EuiFieldText, + EuiFormRow, + EuiCode, + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiButtonEmpty, + EuiCallOut, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { connect } from 'react-redux'; +import { isEqual } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { Link } from 'react-router-dom'; +import { AppState } from '../state'; +import { selectDynamicSettings } from '../state/selectors'; +import { DynamicSettingsState } from '../state/reducers/dynamic_settings'; +import { getDynamicSettings, setDynamicSettings } from '../state/actions/dynamic_settings'; +import { DynamicSettings, defaultDynamicSettings } from '../../common/runtime_types'; +import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; +import { OVERVIEW_ROUTE } from '../../common/constants'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; + +interface Props { + dynamicSettingsState: DynamicSettingsState; +} + +interface DispatchProps { + dispatchGetDynamicSettings: typeof getDynamicSettings; + dispatchSetDynamicSettings: typeof setDynamicSettings; +} + +export const SettingsPageComponent = ({ + dynamicSettingsState: dss, + dispatchGetDynamicSettings, + dispatchSetDynamicSettings, +}: Props & DispatchProps) => { + const settingsBreadcrumbText = i18n.translate('xpack.uptime.settingsBreadcrumbText', { + defaultMessage: 'Settings', + }); + useBreadcrumbs([{ text: settingsBreadcrumbText }]); + + useEffect(() => { + dispatchGetDynamicSettings({}); + }, [dispatchGetDynamicSettings]); + + const [formFields, setFormFields] = useState(dss.settings || null); + + if (!dss.loadError && formFields == null && dss.settings) { + setFormFields({ ...dss.settings }); + } + + const fieldErrors = formFields && { + heartbeatIndices: formFields.heartbeatIndices.match(/^\S+$/) ? null : 'May not be blank', + }; + const isFormValid = !(fieldErrors && Object.values(fieldErrors).find(v => !!v)); + + const onChangeFormField = (field: keyof DynamicSettings, value: any) => { + if (formFields) { + formFields[field] = value; + setFormFields({ ...formFields }); + } + }; + + const onApply = () => { + if (formFields) { + dispatchSetDynamicSettings(formFields); + } + }; + + const resetForm = () => { + if (formFields && dss.settings) { + setFormFields({ ...dss.settings }); + } + }; + + const isFormDirty = dss.settings ? !isEqual(dss.settings, formFields) : true; + const canEdit: boolean = + !!useKibana().services?.application?.capabilities.uptime.configureSettings || false; + const isFormDisabled = dss.loading || !canEdit; + + const editNoticeTitle = i18n.translate('xpack.uptime.settings.cannotEditTitle', { + defaultMessage: 'You do not have permission to edit settings.', + }); + const editNoticeText = i18n.translate('xpack.uptime.settings.cannotEditText', { + defaultMessage: + "Your user currently has 'Read' permissions for the Uptime app. Enable a permissions-level of 'All' to edit these settings.", + }); + const cannotEditNotice = canEdit ? null : ( + <> + {editNoticeText} + + + ); + + return ( + <> + + + {i18n.translate('xpack.uptime.settings.returnToOverviewLinkLabel', { + defaultMessage: 'Return to overview', + })} + + + + + + {cannotEditNotice} + + + +
+ + +

+ +

+
+ + + + + } + description={ + + } + > + {defaultDynamicSettings.heartbeatIndices} + ), + }} + /> + } + isInvalid={!!fieldErrors?.heartbeatIndices} + label={ + + } + > + + onChangeFormField('heartbeatIndices', event.currentTarget.value) + } + /> + + + + + + + { + resetForm(); + }} + > + + + + + + + + + +
+
+
+
+
+ + ); +}; + +const mapStateToProps = (state: AppState) => ({ + dynamicSettingsState: selectDynamicSettings(state), +}); + +const mapDispatchToProps = (dispatch: any) => ({ + dispatchGetDynamicSettings: () => { + return dispatch(getDynamicSettings({})); + }, + dispatchSetDynamicSettings: (settings: DynamicSettings) => { + return dispatch(setDynamicSettings(settings)); + }, +}); + +export const SettingsPage = connect(mapStateToProps, mapDispatchToProps)(SettingsPageComponent); diff --git a/x-pack/legacy/plugins/uptime/public/routes.tsx b/x-pack/legacy/plugins/uptime/public/routes.tsx index 83be45083b645..590e00e92e1fb 100644 --- a/x-pack/legacy/plugins/uptime/public/routes.tsx +++ b/x-pack/legacy/plugins/uptime/public/routes.tsx @@ -8,8 +8,8 @@ import React, { FC } from 'react'; import { Route, Switch } from 'react-router-dom'; import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public'; import { OverviewPage } from './components/connected/pages/overview_container'; -import { MONITOR_ROUTE, OVERVIEW_ROUTE } from '../common/constants'; -import { MonitorPage, NotFoundPage } from './pages'; +import { MONITOR_ROUTE, OVERVIEW_ROUTE, SETTINGS_ROUTE } from '../common/constants'; +import { MonitorPage, NotFoundPage, SettingsPage } from './pages'; interface RouterProps { autocomplete: DataPublicPluginSetup['autocomplete']; @@ -20,6 +20,9 @@ export const PageRouter: FC = ({ autocomplete }) => ( + + + diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/dynamic_settings.ts b/x-pack/legacy/plugins/uptime/public/state/actions/dynamic_settings.ts new file mode 100644 index 0000000000000..d78c725c4b599 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/actions/dynamic_settings.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { createAction } from 'redux-actions'; +import { DynamicSettings } from '../../../common/runtime_types'; + +export const getDynamicSettings = createAction<{}>('GET_DYNAMIC_SETTINGS'); +export const getDynamicSettingsSuccess = createAction( + 'GET_DYNAMIC_SETTINGS_SUCCESS' +); +export const getDynamicSettingsFail = createAction('GET_DYNAMIC_SETTINGS_FAIL'); + +export const setDynamicSettings = createAction('SET_DYNAMIC_SETTINGS'); +export const setDynamicSettingsSuccess = createAction( + 'SET_DYNAMIC_SETTINGS_SUCCESS' +); +export const setDynamicSettingsFail = createAction('SET_DYNAMIC_SETTINGS_FAIL'); +export const acknowledgeSetDynamicSettings = createAction<{}>('ACKNOWLEDGE_SET_DYNAMIC_SETTINGS'); diff --git a/x-pack/legacy/plugins/uptime/public/state/api/dynamic_settings.ts b/x-pack/legacy/plugins/uptime/public/state/api/dynamic_settings.ts new file mode 100644 index 0000000000000..8ade2aa4595dc --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/api/dynamic_settings.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + DynamicSettingsType, + DynamicSettings, + DynamicSettingsSaveResponse, + DynamicSettingsSaveType, +} from '../../../common/runtime_types'; +import { apiService } from './utils'; + +const apiPath = '/api/uptime/dynamic_settings'; + +interface BaseApiRequest { + basePath: string; +} + +type SaveApiRequest = BaseApiRequest & { + settings: DynamicSettings; +}; + +export const getDynamicSettings = async ({ + basePath, +}: BaseApiRequest): Promise => { + return await apiService.get(apiPath, undefined, DynamicSettingsType); +}; + +export const setDynamicSettings = async ({ + basePath, + settings, +}: SaveApiRequest): Promise => { + return await apiService.post(apiPath, settings, DynamicSettingsSaveType); +}; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/index.ts b/x-pack/legacy/plugins/uptime/public/state/api/index.ts index 518091cb36dde..793762c0f4a10 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/index.ts @@ -8,6 +8,7 @@ export * from './monitor'; export * from './overview_filters'; export * from './snapshot'; export * from './monitor_status'; +export * from './dynamic_settings'; export * from './index_pattern'; export * from './index_status'; export * from './ping'; diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/dynamic_settings.ts b/x-pack/legacy/plugins/uptime/public/state/effects/dynamic_settings.ts new file mode 100644 index 0000000000000..9bc8bd95be68c --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/effects/dynamic_settings.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { takeLatest, put, call, select } from 'redux-saga/effects'; +import { Action } from 'redux-actions'; +import { i18n } from '@kbn/i18n'; +import { fetchEffectFactory } from './fetch_effect'; +import { + getDynamicSettings, + getDynamicSettingsSuccess, + getDynamicSettingsFail, + setDynamicSettingsSuccess, + setDynamicSettingsFail, + setDynamicSettings, +} from '../actions/dynamic_settings'; +import { + getDynamicSettings as getDynamicSettingsAPI, + setDynamicSettings as setDynamicSettingsAPI, +} from '../api'; +import { DynamicSettings } from '../../../common/runtime_types'; +import { getBasePath } from '../selectors'; +import { kibanaService } from '../kibana_service'; + +export function* fetchDynamicSettingsEffect() { + yield takeLatest( + String(getDynamicSettings), + fetchEffectFactory(getDynamicSettingsAPI, getDynamicSettingsSuccess, getDynamicSettingsFail) + ); +} + +export function* setDynamicSettingsEffect() { + const couldNotSaveSettingsText = i18n.translate('xpack.uptime.settings.error.couldNotSave', { + defaultMessage: 'Could not save settings!', + }); + yield takeLatest(String(setDynamicSettings), function*(action: Action) { + try { + if (!action.payload) { + const err = new Error('Cannot fetch effect without a payload'); + yield put(setDynamicSettingsFail(err)); + + kibanaService.core.notifications.toasts.addError(err, { + title: couldNotSaveSettingsText, + }); + return; + } + const basePath = yield select(getBasePath); + yield call(setDynamicSettingsAPI, { settings: action.payload, basePath }); + yield put(setDynamicSettingsSuccess(action.payload)); + kibanaService.core.notifications.toasts.addSuccess('Settings saved!'); + } catch (err) { + kibanaService.core.notifications.toasts.addError(err, { + title: couldNotSaveSettingsText, + }); + yield put(setDynamicSettingsFail(err)); + } + }); +} diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/index.ts b/x-pack/legacy/plugins/uptime/public/state/effects/index.ts index 7c45aa142ecfd..d11b79d60d154 100644 --- a/x-pack/legacy/plugins/uptime/public/state/effects/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/effects/index.ts @@ -9,6 +9,7 @@ import { fetchMonitorDetailsEffect } from './monitor'; import { fetchOverviewFiltersEffect } from './overview_filters'; import { fetchSnapshotCountEffect } from './snapshot'; import { fetchMonitorStatusEffect } from './monitor_status'; +import { fetchDynamicSettingsEffect, setDynamicSettingsEffect } from './dynamic_settings'; import { fetchIndexPatternEffect } from './index_pattern'; import { fetchPingHistogramEffect } from './ping'; import { fetchMonitorDurationEffect } from './monitor_duration'; @@ -19,6 +20,8 @@ export function* rootEffect() { yield fork(fetchSnapshotCountEffect); yield fork(fetchOverviewFiltersEffect); yield fork(fetchMonitorStatusEffect); + yield fork(fetchDynamicSettingsEffect); + yield fork(setDynamicSettingsEffect); yield fork(fetchIndexPatternEffect); yield fork(fetchPingHistogramEffect); yield fork(fetchMonitorDurationEffect); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/dynamic_settings.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/dynamic_settings.ts new file mode 100644 index 0000000000000..f003565e9873e --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/dynamic_settings.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { handleActions, Action } from 'redux-actions'; +import { + getDynamicSettings, + getDynamicSettingsSuccess, + getDynamicSettingsFail, + setDynamicSettings, + setDynamicSettingsSuccess, + setDynamicSettingsFail, +} from '../actions/dynamic_settings'; +import { DynamicSettings } from '../../../common/runtime_types'; + +export interface DynamicSettingsState { + settings?: DynamicSettings; + loadError?: Error; + saveError?: Error; + loading: boolean; +} + +const initialState: DynamicSettingsState = { + loading: true, +}; + +export const dynamicSettingsReducer = handleActions( + { + [String(getDynamicSettings)]: state => ({ + ...state, + loading: true, + }), + [String(getDynamicSettingsSuccess)]: (state, action: Action) => { + return { + loading: false, + settings: action.payload, + }; + }, + [String(getDynamicSettingsFail)]: (state, action: Action) => { + return { + loading: false, + loadError: action.payload, + }; + }, + [String(setDynamicSettings)]: state => ({ + ...state, + loading: true, + }), + [String(setDynamicSettingsSuccess)]: (state, action: Action) => ({ + settings: action.payload, + saveSucceded: true, + loading: false, + }), + [String(setDynamicSettingsFail)]: (state, action: Action) => ({ + ...state, + loading: false, + saveSucceeded: false, + saveError: action.payload, + }), + }, + initialState +); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts index 4a83b54504ca8..6617627aadaf3 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts @@ -10,6 +10,7 @@ import { overviewFiltersReducer } from './overview_filters'; import { snapshotReducer } from './snapshot'; import { uiReducer } from './ui'; import { monitorStatusReducer } from './monitor_status'; +import { dynamicSettingsReducer } from './dynamic_settings'; import { indexPatternReducer } from './index_pattern'; import { pingReducer } from './ping'; import { monitorDurationReducer } from './monitor_duration'; @@ -21,6 +22,7 @@ export const rootReducer = combineReducers({ snapshot: snapshotReducer, ui: uiReducer, monitorStatus: monitorStatusReducer, + dynamicSettings: dynamicSettingsReducer, indexPattern: indexPatternReducer, ping: pingReducer, monitorDuration: monitorDurationReducer, diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts b/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts index b1da995709f93..1aea90c70cd0e 100644 --- a/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts +++ b/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts @@ -19,6 +19,9 @@ describe('state selectors', () => { errors: [], loading: false, }, + dynamicSettings: { + loading: false, + }, monitor: { monitorDetailsList: [], monitorLocationsList: new Map(), diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts b/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts index 7b5a5ddf8d3ca..6844b31d4973c 100644 --- a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts @@ -29,6 +29,10 @@ export const selectMonitorStatus = (state: AppState) => { return state.monitorStatus.status; }; +export const selectDynamicSettings = (state: AppState) => { + return state.dynamicSettings; +}; + export const selectIndexPattern = ({ indexPattern }: AppState) => { return { indexPattern: indexPattern.index_pattern, loading: indexPattern.loading }; }; diff --git a/x-pack/plugins/uptime/server/graphql/monitor_states/resolvers.ts b/x-pack/plugins/uptime/server/graphql/monitor_states/resolvers.ts index 1560b78b3c050..08973b217b96c 100644 --- a/x-pack/plugins/uptime/server/graphql/monitor_states/resolvers.ts +++ b/x-pack/plugins/uptime/server/graphql/monitor_states/resolvers.ts @@ -12,6 +12,7 @@ import { MonitorSummaryResult, } from '../../../../../legacy/plugins/uptime/common/graphql/types'; import { CONTEXT_DEFAULTS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { savedObjectsAdapter } from '../../lib/saved_objects'; export type UMGetMonitorStatesResolver = UMResolver< MonitorSummaryResult | Promise, @@ -32,8 +33,12 @@ export const createMonitorStatesResolvers: CreateUMGraphQLResolvers = ( async getMonitorStates( _resolver, { dateRangeStart, dateRangeEnd, filters, pagination, statusFilter }, - { APICaller } + { APICaller, savedObjectsClient } ): Promise { + const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings( + savedObjectsClient + ); + const decodedPagination = pagination ? JSON.parse(decodeURIComponent(pagination)) : CONTEXT_DEFAULTS.CURSOR_PAGINATION; @@ -41,9 +46,10 @@ export const createMonitorStatesResolvers: CreateUMGraphQLResolvers = ( indexStatus, { summaries, nextPagePagination, prevPagePagination }, ] = await Promise.all([ - libs.requests.getIndexStatus({ callES: APICaller }), + libs.requests.getIndexStatus({ callES: APICaller, dynamicSettings }), libs.requests.getMonitorStates({ callES: APICaller, + dynamicSettings, dateRangeStart, dateRangeEnd, pagination: decodedPagination, diff --git a/x-pack/plugins/uptime/server/graphql/pings/resolvers.ts b/x-pack/plugins/uptime/server/graphql/pings/resolvers.ts index b383fc5d5fb15..8153d8c8f3b8c 100644 --- a/x-pack/plugins/uptime/server/graphql/pings/resolvers.ts +++ b/x-pack/plugins/uptime/server/graphql/pings/resolvers.ts @@ -12,6 +12,7 @@ import { import { UMServerLibs } from '../../lib/lib'; import { UMContext } from '../types'; import { CreateUMGraphQLResolvers } from '../types'; +import { savedObjectsAdapter } from '../../lib/saved_objects'; export type UMAllPingsResolver = UMResolver< PingResults | Promise, @@ -35,10 +36,15 @@ export const createPingsResolvers: CreateUMGraphQLResolvers = ( async allPings( _resolver, { monitorId, sort, size, status, dateRangeStart, dateRangeEnd, location }, - { APICaller } + { APICaller, savedObjectsClient } ): Promise { + const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings( + savedObjectsClient + ); + return await libs.requests.getPings({ callES: APICaller, + dynamicSettings, dateRangeStart, dateRangeEnd, monitorId, diff --git a/x-pack/plugins/uptime/server/graphql/types.ts b/x-pack/plugins/uptime/server/graphql/types.ts index 8508066a71f98..5f0a6749eb599 100644 --- a/x-pack/plugins/uptime/server/graphql/types.ts +++ b/x-pack/plugins/uptime/server/graphql/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandlerContext, CallAPIOptions } from 'src/core/server'; +import { RequestHandlerContext, CallAPIOptions, SavedObjectsClient } from 'src/core/server'; import { UMServerLibs } from '../lib/lib'; export type UMContext = RequestHandlerContext & { @@ -13,6 +13,7 @@ export type UMContext = RequestHandlerContext & { clientParams?: Record, options?: CallAPIOptions | undefined ) => Promise; + savedObjectsClient: SavedObjectsClient; }; export interface UMGraphQLResolver { diff --git a/x-pack/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts index 2c1f34aa8a8e7..19506bb316a05 100644 --- a/x-pack/plugins/uptime/server/kibana.index.ts +++ b/x-pack/plugins/uptime/server/kibana.index.ts @@ -10,6 +10,7 @@ import { KibanaTelemetryAdapter } from './lib/adapters/telemetry'; import { compose } from './lib/compose/kibana'; import { initUptimeServer } from './uptime_server'; import { UptimeCorePlugins, UptimeCoreSetup } from './lib/adapters/framework'; +import { umDynamicSettings } from './lib/saved_objects'; export interface KibanaRouteOptions { path: string; @@ -37,20 +38,20 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor catalogue: ['uptime'], privileges: { all: { - api: ['uptime'], + api: ['uptime-read', 'uptime-write'], savedObject: { - all: [], + all: [umDynamicSettings.name], read: [], }, - ui: ['save'], + ui: ['save', 'configureSettings', 'show'], }, read: { - api: ['uptime'], + api: ['uptime-read'], savedObject: { all: [], - read: [], + read: [umDynamicSettings.name], }, - ui: [], + ui: ['show'], }, }, }); diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts index 6fc488e949e9c..a6dd8efd57c14 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts @@ -9,6 +9,7 @@ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { IRouter, CallAPIOptions, SavedObjectsClientContract } from 'src/core/server'; import { UMKibanaRoute } from '../../../rest_api'; import { PluginSetupContract } from '../../../../../features/server'; +import { DynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; type APICaller = ( endpoint: string, @@ -17,12 +18,12 @@ type APICaller = ( ) => Promise; export type UMElasticsearchQueryFn = ( - params: { callES: APICaller } & P + params: { callES: APICaller; dynamicSettings: DynamicSettings } & P ) => Promise | R; export type UMSavedObjectsQueryFn = ( client: SavedObjectsClientContract, - params: P + params?: P ) => Promise | T; export interface UptimeCoreSetup { diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts index 7ac3db9d0f3d7..1f92c8212b393 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -46,7 +46,7 @@ export class UMKibanaBackendFrameworkAdapter implements UMBackendFrameworkAdapte }), }, options: { - tags: ['access:uptime'], + tags: ['access:uptime-read'], }, }, async (context, request, resp): Promise => { @@ -60,13 +60,17 @@ export class UMKibanaBackendFrameworkAdapter implements UMBackendFrameworkAdapte const options = { graphQLOptions: (_req: any) => { return { - context: { ...context, APICaller: callAsCurrentUser }, + context: { + ...context, + APICaller: callAsCurrentUser, + savedObjectsClient: context.core.savedObjects.client, + }, schema, }; }, path: routePath, route: { - tags: ['access:uptime'], + tags: ['access:uptime-read'], }, }; try { diff --git a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts index 8a11270a4740a..609d84cb521fc 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts @@ -16,6 +16,7 @@ import { AlertType } from '../../../../../alerting/server'; import { IRouter } from 'kibana/server'; import { UMServerLibs } from '../../lib'; import { UptimeCoreSetup } from '../../adapters'; +import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; /** * The alert takes some dependencies as parameters; these are things like @@ -43,7 +44,7 @@ const bootstrapDependencies = (customRequests?: any) => { */ const mockOptions = ( params = { numTimes: 5, locations: [], timerange: { from: 'now-15m', to: 'now' } }, - services = { callCluster: 'mockESFunction' }, + services = { callCluster: 'mockESFunction', savedObjectsClient: mockSavedObjectsClient }, state = {} ): any => ({ params, @@ -51,6 +52,9 @@ const mockOptions = ( state, }); +const mockSavedObjectsClient = { get: jest.fn() }; +mockSavedObjectsClient.get.mockReturnValue(defaultDynamicSettings); + describe('status check alert', () => { describe('executor', () => { it('does not trigger when there are no monitors down', async () => { @@ -69,6 +73,7 @@ describe('status check alert', () => { Array [ Object { "callES": "mockESFunction", + "dynamicSettings": undefined, "locations": Array [], "numTimes": 5, "timerange": Object { @@ -118,6 +123,7 @@ describe('status check alert', () => { Array [ Object { "callES": "mockESFunction", + "dynamicSettings": undefined, "locations": Array [], "numTimes": 5, "timerange": Object { diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index 3e90d2ce95a10..d999f0fda3937 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -17,6 +17,7 @@ import { StatusCheckAlertStateType, StatusCheckAlertState, } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { savedObjectsAdapter } from '../saved_objects'; const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS; @@ -202,13 +203,17 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (server, libs) => } const params = decoded.right; - + const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings( + options.services.savedObjectsClient, + undefined + ); /* This is called `monitorsByLocation` but it's really * monitors by location by status. The query we run to generate this * filters on the status field, so effectively there should be one and only one * status represented in the result set. */ const monitorsByLocation = await libs.requests.getMonitorStatus({ callES: options.services.callCluster, + dynamicSettings, ...params, }); diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts index ad0987a7f6faf..b7e340fddbd2c 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts @@ -5,13 +5,14 @@ */ import { getLatestMonitor } from '../get_latest_monitor'; +import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; describe('getLatestMonitor', () => { let expectedGetLatestSearchParams: any; let mockEsSearchResult: any; beforeEach(() => { expectedGetLatestSearchParams = { - index: 'heartbeat-8*', + index: defaultDynamicSettings.heartbeatIndices, body: { query: { bool: { @@ -81,6 +82,7 @@ describe('getLatestMonitor', () => { const mockEsClient = jest.fn(async (_request: any, _params: any) => mockEsSearchResult); const result = await getLatestMonitor({ callES: mockEsClient, + dynamicSettings: defaultDynamicSettings, dateStart: 'now-1h', dateEnd: 'now', monitorId: 'testMonitor', diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts index 24411f48672cd..e54a17f934bcc 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts @@ -8,6 +8,7 @@ import { get, set } from 'lodash'; import mockChartsData from './monitor_charts_mock.json'; import { assertCloseTo } from '../../helper'; import { getMonitorDurationChart } from '../get_monitor_duration'; +import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; describe('ElasticsearchMonitorsAdapter', () => { it('getMonitorChartsData will run expected parameters when no location is specified', async () => { @@ -16,6 +17,7 @@ describe('ElasticsearchMonitorsAdapter', () => { const search = searchMock.bind({}); await getMonitorDurationChart({ callES: search, + dynamicSettings: defaultDynamicSettings, monitorId: 'fooID', dateStart: 'now-15m', dateEnd: 'now', @@ -51,6 +53,7 @@ describe('ElasticsearchMonitorsAdapter', () => { const search = searchMock.bind({}); await getMonitorDurationChart({ callES: search, + dynamicSettings: defaultDynamicSettings, monitorId: 'fooID', dateStart: 'now-15m', dateEnd: 'now', @@ -87,6 +90,7 @@ describe('ElasticsearchMonitorsAdapter', () => { expect( await getMonitorDurationChart({ callES: search, + dynamicSettings: defaultDynamicSettings, monitorId: 'id', dateStart: 'now-15m', dateEnd: 'now', diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts index 74b8c352c8553..e429de9ae0d68 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts @@ -7,6 +7,7 @@ import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks'; import { getMonitorStatus } from '../get_monitor_status'; import { ScopedClusterClient } from 'src/core/server/elasticsearch'; +import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; interface BucketItemCriteria { monitor_id: string; @@ -102,6 +103,7 @@ describe('getMonitorStatus', () => { }`; await getMonitorStatus({ callES, + dynamicSettings: defaultDynamicSettings, filters: exampleFilter, locations: [], numTimes: 5, @@ -204,6 +206,7 @@ describe('getMonitorStatus', () => { const [callES, esMock] = setupMock([]); await getMonitorStatus({ callES, + dynamicSettings: defaultDynamicSettings, locations: ['fairbanks', 'harrisburg'], numTimes: 1, timerange: { @@ -326,6 +329,7 @@ describe('getMonitorStatus', () => { }; const result = await getMonitorStatus({ callES, + dynamicSettings: defaultDynamicSettings, ...clientParameters, }); expect(esMock.callAsCurrentUser).toHaveBeenCalledTimes(1); @@ -490,6 +494,7 @@ describe('getMonitorStatus', () => { const [callES] = setupMock(criteria); const result = await getMonitorStatus({ callES, + dynamicSettings: defaultDynamicSettings, locations: [], numTimes: 5, timerange: { diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts index 7d98b77069264..faeb291bb533b 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts @@ -5,6 +5,7 @@ */ import { getPingHistogram } from '../get_ping_histogram'; +import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; describe('getPingHistogram', () => { const standardMockResponse: any = { @@ -58,6 +59,7 @@ describe('getPingHistogram', () => { const result = await getPingHistogram({ callES: mockEsClient, + dynamicSettings: defaultDynamicSettings, from: 'now-15m', to: 'now', filters: null, @@ -76,6 +78,7 @@ describe('getPingHistogram', () => { const result = await getPingHistogram({ callES: mockEsClient, + dynamicSettings: defaultDynamicSettings, from: 'now-15m', to: 'now', filters: null, @@ -137,6 +140,7 @@ describe('getPingHistogram', () => { const result = await getPingHistogram({ callES: mockEsClient, + dynamicSettings: defaultDynamicSettings, from: '1234', to: '5678', filters: JSON.stringify(searchFilter), @@ -192,6 +196,7 @@ describe('getPingHistogram', () => { const filters = `{"bool":{"must":[{"simple_query_string":{"query":"http"}}]}}`; const result = await getPingHistogram({ callES: mockEsClient, + dynamicSettings: defaultDynamicSettings, from: 'now-15m', to: 'now', filters, @@ -208,6 +213,7 @@ describe('getPingHistogram', () => { mockEsClient.mockReturnValue(standardMockResponse); const result = await getPingHistogram({ callES: mockEsClient, + dynamicSettings: defaultDynamicSettings, from: '1234', to: '5678', filters: '', @@ -228,6 +234,7 @@ describe('getPingHistogram', () => { const result = await getPingHistogram({ callES: mockEsClient, + dynamicSettings: defaultDynamicSettings, from: '1234', to: '5678', filters: '', diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts index ab20a958f3d97..9145ccca1b6d1 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts @@ -6,6 +6,7 @@ import { getPings } from '../get_pings'; import { set } from 'lodash'; +import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; describe('getAll', () => { let mockEsSearchResult: any; @@ -43,7 +44,7 @@ describe('getAll', () => { }, }; expectedGetAllParams = { - index: 'heartbeat-8*', + index: defaultDynamicSettings.heartbeatIndices, body: { query: { bool: { @@ -70,6 +71,7 @@ describe('getAll', () => { mockEsClient.mockReturnValue(mockEsSearchResult); const result = await getPings({ callES: mockEsClient, + dynamicSettings: defaultDynamicSettings, dateRangeStart: 'now-1h', dateRangeEnd: 'now', sort: 'asc', @@ -92,6 +94,7 @@ describe('getAll', () => { mockEsClient.mockReturnValue(mockEsSearchResult); await getPings({ callES: mockEsClient, + dynamicSettings: defaultDynamicSettings, dateRangeStart: 'now-1h', dateRangeEnd: 'now', sort: 'asc', @@ -108,6 +111,7 @@ describe('getAll', () => { mockEsClient.mockReturnValue(mockEsSearchResult); await getPings({ callES: mockEsClient, + dynamicSettings: defaultDynamicSettings, dateRangeStart: 'now-1h', dateRangeEnd: 'now', size: 12, @@ -121,6 +125,7 @@ describe('getAll', () => { mockEsClient.mockReturnValue(mockEsSearchResult); await getPings({ callES: mockEsClient, + dynamicSettings: defaultDynamicSettings, dateRangeStart: 'now-1h', dateRangeEnd: 'now', sort: 'desc', @@ -136,6 +141,7 @@ describe('getAll', () => { mockEsClient.mockReturnValue(mockEsSearchResult); await getPings({ callES: mockEsClient, + dynamicSettings: defaultDynamicSettings, dateRangeStart: 'now-1h', dateRangeEnd: 'now', monitorId: 'testmonitorid', @@ -151,6 +157,7 @@ describe('getAll', () => { mockEsClient.mockReturnValue(mockEsSearchResult); await getPings({ callES: mockEsClient, + dynamicSettings: defaultDynamicSettings, dateRangeStart: 'now-1h', dateRangeEnd: 'now', status: 'down', diff --git a/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts b/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts index affe205a46844..b533c990083ab 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts @@ -7,7 +7,6 @@ import { UMElasticsearchQueryFn } from '../adapters'; import { OverviewFilters } from '../../../../../legacy/plugins/uptime/common/runtime_types'; import { generateFilterAggs } from './generate_filter_aggs'; -import { INDEX_NAMES } from '../../../../../legacy/plugins/uptime/common/constants'; export interface GetFilterBarParams { /** @param dateRangeStart timestamp bounds */ @@ -67,6 +66,7 @@ export const extractFilterAggsResults = ( export const getFilterBar: UMElasticsearchQueryFn = async ({ callES, + dynamicSettings, dateRangeStart, dateRangeEnd, search, @@ -83,7 +83,7 @@ export const getFilterBar: UMElasticsearchQueryFn = async callES => { - const indexPatternsFetcher = new IndexPatternsFetcher((...rest: Parameters) => - callES(...rest) - ); +export const getUptimeIndexPattern: UMElasticsearchQueryFn<{}, {}> = async ({ + callES, + dynamicSettings, +}) => { + const callAsCurrentUser: APICaller = async ( + endpoint: string, + clientParams: Record = {}, + options?: CallAPIOptions + ) => callES(endpoint, clientParams, options); + const indexPatternsFetcher = new IndexPatternsFetcher(callAsCurrentUser); // Since `getDynamicIndexPattern` is called in setup_request (and thus by every endpoint) // and since `getFieldsForWildcard` will throw if the specified indices don't exist, @@ -20,12 +25,12 @@ export const getUptimeIndexPattern: UMElasticsearchQueryFn = async call // (would be a bad first time experience) try { const fields = await indexPatternsFetcher.getFieldsForWildcard({ - pattern: INDEX_NAMES.HEARTBEAT, + pattern: dynamicSettings.heartbeatIndices, }); const indexPattern: IIndexPattern = { fields, - title: INDEX_NAMES.HEARTBEAT, + title: dynamicSettings.heartbeatIndices, }; return indexPattern; @@ -34,7 +39,7 @@ export const getUptimeIndexPattern: UMElasticsearchQueryFn = async call if (notExists) { // eslint-disable-next-line no-console console.error( - `Could not get dynamic index pattern because indices "${INDEX_NAMES.HEARTBEAT}" don't exist` + `Could not get dynamic index pattern because indices "${dynamicSettings.heartbeatIndices}" don't exist` ); return; } diff --git a/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts b/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts index d8a05c08b1417..6f7854d35b308 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts @@ -5,14 +5,16 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { INDEX_NAMES } from '../../../../../legacy/plugins/uptime/common/constants'; import { StatesIndexStatus } from '../../../../../legacy/plugins/uptime/common/runtime_types'; -export const getIndexStatus: UMElasticsearchQueryFn<{}, StatesIndexStatus> = async ({ callES }) => { +export const getIndexStatus: UMElasticsearchQueryFn<{}, StatesIndexStatus> = async ({ + callES, + dynamicSettings, +}) => { const { _shards: { total }, count, - } = await callES('count', { index: INDEX_NAMES.HEARTBEAT }); + } = await callES('count', { index: dynamicSettings.heartbeatIndices }); return { indexExists: total > 0, docCount: count, diff --git a/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts index 2d549fce06884..85749ac66b80c 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts @@ -6,7 +6,6 @@ import { UMElasticsearchQueryFn } from '../adapters'; import { Ping } from '../../../../../legacy/plugins/uptime/common/graphql/types'; -import { INDEX_NAMES } from '../../../../../legacy/plugins/uptime/common/constants'; export interface GetLatestMonitorParams { /** @member dateRangeStart timestamp bounds */ @@ -22,6 +21,7 @@ export interface GetLatestMonitorParams { // Get The monitor latest state sorted by timestamp with date range export const getLatestMonitor: UMElasticsearchQueryFn = async ({ callES, + dynamicSettings, dateStart, dateEnd, monitorId, @@ -29,7 +29,7 @@ export const getLatestMonitor: UMElasticsearchQueryFn = async ({ callES, + dynamicSettings, monitorId, }) => { const params = { - index: INDEX_NAMES.HEARTBEAT, + index: dynamicSettings.heartbeatIndices, body: { size: 1, _source: ['url', 'monitor', 'observer'], diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts index eb3657e60a7bb..8393370e1516b 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts @@ -9,7 +9,6 @@ import { MonitorDetails, MonitorError, } from '../../../../../legacy/plugins/uptime/common/runtime_types'; -import { INDEX_NAMES } from '../../../../../legacy/plugins/uptime/common/constants'; export interface GetMonitorDetailsParams { monitorId: string; @@ -20,7 +19,7 @@ export interface GetMonitorDetailsParams { export const getMonitorDetails: UMElasticsearchQueryFn< GetMonitorDetailsParams, MonitorDetails -> = async ({ callES, monitorId, dateStart, dateEnd }) => { +> = async ({ callES, dynamicSettings, monitorId, dateStart, dateEnd }) => { const queryFilters: any = [ { range: { @@ -38,7 +37,7 @@ export const getMonitorDetails: UMElasticsearchQueryFn< ]; const params = { - index: INDEX_NAMES.HEARTBEAT, + index: dynamicSettings.heartbeatIndices, body: { size: 1, _source: ['error', '@timestamp'], diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_duration.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_duration.ts index 5fb9df3738533..40156132aafcf 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_duration.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_duration.ts @@ -5,7 +5,6 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { INDEX_NAMES } from '../../../../../legacy/plugins/uptime/common/constants'; import { getHistogramIntervalFormatted } from '../helper'; import { LocationDurationLine, @@ -47,9 +46,9 @@ const formatStatusBuckets = (time: any, buckets: any, docCount: any) => { export const getMonitorDurationChart: UMElasticsearchQueryFn< GetMonitorChartsParams, MonitorDurationResult -> = async ({ callES, dateStart, dateEnd, monitorId }) => { +> = async ({ callES, dynamicSettings, dateStart, dateEnd, monitorId }) => { const params = { - index: INDEX_NAMES.HEARTBEAT, + index: dynamicSettings.heartbeatIndices, body: { query: { bool: { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts index 328ef54c404d3..f49e404ffb084 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts @@ -5,10 +5,7 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { - INDEX_NAMES, - UNNAMED_LOCATION, -} from '../../../../../legacy/plugins/uptime/common/constants'; +import { UNNAMED_LOCATION } from '../../../../../legacy/plugins/uptime/common/constants'; import { MonitorLocations, MonitorLocation, @@ -29,9 +26,9 @@ export interface GetMonitorLocationsParams { export const getMonitorLocations: UMElasticsearchQueryFn< GetMonitorLocationsParams, MonitorLocations -> = async ({ callES, monitorId, dateStart, dateEnd }) => { +> = async ({ callES, dynamicSettings, monitorId, dateStart, dateEnd }) => { const params = { - index: INDEX_NAMES.HEARTBEAT, + index: dynamicSettings.heartbeatIndices, body: { size: 0, query: { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts index 5b02e2502a27e..bfccb34ab94de 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts @@ -47,13 +47,22 @@ const jsonifyPagination = (p: any): string | null => { export const getMonitorStates: UMElasticsearchQueryFn< GetMonitorStatesParams, GetMonitorStatesResult -> = async ({ callES, dateRangeStart, dateRangeEnd, pagination, filters, statusFilter }) => { +> = async ({ + callES, + dynamicSettings, + dateRangeStart, + dateRangeEnd, + pagination, + filters, + statusFilter, +}) => { pagination = pagination || CONTEXT_DEFAULTS.CURSOR_PAGINATION; statusFilter = statusFilter === null ? undefined : statusFilter; const size = 10; const queryContext = new QueryContext( callES, + dynamicSettings.heartbeatIndices, dateRangeStart, dateRangeEnd, pagination, diff --git a/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts index 339409b63a4f6..00f1fc7de4c12 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts @@ -5,7 +5,7 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { INDEX_NAMES, QUERY } from '../../../../../legacy/plugins/uptime/common/constants'; +import { QUERY } from '../../../../../legacy/plugins/uptime/common/constants'; import { getFilterClause } from '../helper'; import { HistogramQueryResult } from './types'; import { HistogramResult } from '../../../../../legacy/plugins/uptime/common/types'; @@ -26,7 +26,7 @@ export interface GetPingHistogramParams { export const getPingHistogram: UMElasticsearchQueryFn< GetPingHistogramParams, HistogramResult -> = async ({ callES, from, to, filters, monitorId, statusFilter }) => { +> = async ({ callES, dynamicSettings, from, to, filters, monitorId, statusFilter }) => { const boolFilters = filters ? JSON.parse(filters) : null; const additionalFilters = []; if (monitorId) { @@ -38,7 +38,7 @@ export const getPingHistogram: UMElasticsearchQueryFn< const filter = getFilterClause(from, to, additionalFilters); const params = { - index: INDEX_NAMES.HEARTBEAT, + index: dynamicSettings.heartbeatIndices, body: { query: { bool: { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_pings.ts b/x-pack/plugins/uptime/server/lib/requests/get_pings.ts index ddca27d782066..59d8aa1ab0e63 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_pings.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_pings.ts @@ -10,7 +10,6 @@ import { Ping, HttpBody, } from '../../../../../legacy/plugins/uptime/common/graphql/types'; -import { INDEX_NAMES } from '../../../../../legacy/plugins/uptime/common/constants'; export interface GetPingsParams { /** @member dateRangeStart timestamp bounds */ @@ -37,6 +36,7 @@ export interface GetPingsParams { export const getPings: UMElasticsearchQueryFn = async ({ callES, + dynamicSettings, dateRangeStart, dateRangeEnd, monitorId, @@ -61,7 +61,7 @@ export const getPings: UMElasticsearchQueryFn = asy } const queryContext = { bool: { filter } }; const params = { - index: INDEX_NAMES.HEARTBEAT, + index: dynamicSettings.heartbeatIndices, body: { query: { ...queryContext, diff --git a/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts index 1783c6e91df34..01f2ad88161cf 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts @@ -6,10 +6,7 @@ import { UMElasticsearchQueryFn } from '../adapters'; import { Snapshot } from '../../../../../legacy/plugins/uptime/common/runtime_types'; -import { - CONTEXT_DEFAULTS, - INDEX_NAMES, -} from '../../../../../legacy/plugins/uptime/common/constants'; +import { CONTEXT_DEFAULTS } from '../../../../../legacy/plugins/uptime/common/constants'; import { QueryContext } from './search'; export interface GetSnapshotCountParams { @@ -21,6 +18,7 @@ export interface GetSnapshotCountParams { export const getSnapshotCount: UMElasticsearchQueryFn = async ({ callES, + dynamicSettings: { heartbeatIndices }, dateRangeStart, dateRangeEnd, filters, @@ -32,6 +30,7 @@ export const getSnapshotCount: UMElasticsearchQueryFn => { const res = await context.search({ - index: INDEX_NAMES.HEARTBEAT, + index: context.heartbeatIndices, body: statusCountBody(await context.dateAndCustomFilters()), }); diff --git a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts index ea81ec623e01c..a6c98541fba1d 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts @@ -22,7 +22,9 @@ describe(QueryContext, () => { }; let qc: QueryContext; - beforeEach(() => (qc = new QueryContext({}, rangeStart, rangeEnd, pagination, null, 10))); + beforeEach( + () => (qc = new QueryContext({}, 'indexName', rangeStart, rangeEnd, pagination, null, 10)) + ); describe('dateRangeFilter()', () => { const expectedRange = { diff --git a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts index d96f8dc95aa72..d1212daf5304f 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts @@ -26,5 +26,14 @@ export const nextPagination = (key: any): CursorPagination => { }; }; export const simpleQueryContext = (): QueryContext => { - return new QueryContext(undefined, '', '', nextPagination('something'), undefined, 0, ''); + return new QueryContext( + undefined, + 'indexName', + '', + '', + nextPagination('something'), + undefined, + 0, + '' + ); }; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts index 9ad3928a3b1b2..bcb106eef0ba6 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts @@ -7,7 +7,7 @@ import { get, sortBy } from 'lodash'; import { QueryContext } from './query_context'; import { getHistogramIntervalFormatted } from '../../helper'; -import { INDEX_NAMES, STATES } from '../../../../../../legacy/plugins/uptime/common/constants'; +import { STATES } from '../../../../../../legacy/plugins/uptime/common/constants'; import { MonitorSummary, SummaryHistogram, @@ -25,7 +25,7 @@ export const enrichMonitorGroups: MonitorEnricher = async ( // redundant with the way the code works now. This could be simplified // to a much simpler query + some JS processing. const params = { - index: INDEX_NAMES.HEARTBEAT, + index: queryContext.heartbeatIndices, body: { query: { bool: { @@ -292,7 +292,7 @@ const getHistogramForMonitors = async ( monitorIds: string[] ): Promise<{ [key: string]: SummaryHistogram }> => { const params = { - index: INDEX_NAMES.HEARTBEAT, + index: queryContext.heartbeatIndices, body: { size: 0, query: { diff --git a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts index 9b3b1186472be..424c097853ad3 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts @@ -6,7 +6,6 @@ import { get, set } from 'lodash'; import { CursorDirection } from '../../../../../../legacy/plugins/uptime/common/graphql/types'; -import { INDEX_NAMES } from '../../../../../../legacy/plugins/uptime/common/constants'; import { QueryContext } from './query_context'; // This is the first phase of the query. In it, we find the most recent check groups that matched the given query. @@ -56,7 +55,7 @@ const query = async (queryContext: QueryContext, searchAfter: any, size: number) const body = await queryBody(queryContext, searchAfter, size); const params = { - index: INDEX_NAMES.HEARTBEAT, + index: queryContext.heartbeatIndices, body, }; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts b/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts index c1f5d89ec1a38..6d62ae7ba2b29 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts @@ -6,12 +6,12 @@ import moment from 'moment'; import { APICaller } from 'src/core/server'; -import { INDEX_NAMES } from '../../../../../../legacy/plugins/uptime/common/constants'; import { CursorPagination } from './types'; import { parseRelativeDate } from '../../helper'; export class QueryContext { callES: APICaller; + heartbeatIndices: string; dateRangeStart: string; dateRangeEnd: string; pagination: CursorPagination; @@ -22,6 +22,7 @@ export class QueryContext { constructor( database: any, + heartbeatIndices: string, dateRangeStart: string, dateRangeEnd: string, pagination: CursorPagination, @@ -30,6 +31,7 @@ export class QueryContext { statusFilter?: string ) { this.callES = database; + this.heartbeatIndices = heartbeatIndices; this.dateRangeStart = dateRangeStart; this.dateRangeEnd = dateRangeEnd; this.pagination = pagination; @@ -39,12 +41,12 @@ export class QueryContext { } async search(params: any): Promise { - params.index = INDEX_NAMES.HEARTBEAT; + params.index = this.heartbeatIndices; return this.callES('search', params); } async count(params: any): Promise { - params.index = INDEX_NAMES.HEARTBEAT; + params.index = this.heartbeatIndices; return this.callES('count', params); } @@ -135,6 +137,7 @@ export class QueryContext { clone(): QueryContext { return new QueryContext( this.callES, + this.heartbeatIndices, this.dateRangeStart, this.dateRangeEnd, this.pagination, diff --git a/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts index c55aff3e8c4cd..7d69ff6751f05 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { INDEX_NAMES } from '../../../../../../legacy/plugins/uptime/common/constants'; import { QueryContext } from './query_context'; import { CursorDirection } from '../../../../../../legacy/plugins/uptime/common/graphql/types'; import { MonitorGroups, MonitorLocCheckGroup } from './fetch_page'; @@ -96,7 +95,7 @@ export const mostRecentCheckGroups = async ( potentialMatchMonitorIDs: string[] ): Promise => { const params = { - index: INDEX_NAMES.HEARTBEAT, + index: queryContext.heartbeatIndices, body: { size: 0, query: { diff --git a/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts b/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts index ddf506786f145..6eeea5ba4c3e9 100644 --- a/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts +++ b/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts @@ -37,7 +37,7 @@ type ESQ = UMElasticsearchQueryFn; export interface UptimeRequests { getFilterBar: ESQ; - getIndexPattern: ESQ; + getIndexPattern: ESQ<{}, {}>; getLatestMonitor: ESQ; getMonitor: ESQ; getMonitorDurationChart: ESQ; diff --git a/x-pack/plugins/uptime/server/lib/saved_objects.ts b/x-pack/plugins/uptime/server/lib/saved_objects.ts new file mode 100644 index 0000000000000..175634ef797cc --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/saved_objects.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + DynamicSettings, + defaultDynamicSettings, +} from '../../../../legacy/plugins/uptime/common/runtime_types/dynamic_settings'; +import { SavedObjectsType, SavedObjectsErrorHelpers } from '../../../../../src/core/server'; +import { UMSavedObjectsQueryFn } from './adapters'; + +export interface UMDynamicSettingsType { + heartbeatIndices: string; +} + +export interface UMSavedObjectsAdapter { + getUptimeDynamicSettings: UMSavedObjectsQueryFn; + setUptimeDynamicSettings: UMSavedObjectsQueryFn; +} + +export const settingsObjectType = 'uptime-dynamic-settings'; +export const settingsObjectId = 'uptime-dynamic-settings-singleton'; + +export const umDynamicSettings: SavedObjectsType = { + name: settingsObjectType, + hidden: false, + namespaceAgnostic: false, + mappings: { + properties: { + heartbeatIndices: { + type: 'keyword', + }, + }, + }, +}; + +export const savedObjectsAdapter: UMSavedObjectsAdapter = { + getUptimeDynamicSettings: async (client): Promise => { + try { + const obj = await client.get(umDynamicSettings.name, settingsObjectId); + return obj.attributes; + } catch (getErr) { + if (SavedObjectsErrorHelpers.isNotFoundError(getErr)) { + return defaultDynamicSettings; + } + throw getErr; + } + }, + setUptimeDynamicSettings: async (client, settings): Promise => { + await client.create(umDynamicSettings.name, settings, { + id: settingsObjectId, + overwrite: true, + }); + }, +}; diff --git a/x-pack/plugins/uptime/server/plugin.ts b/x-pack/plugins/uptime/server/plugin.ts index e217b0e2f1ad8..00e36be50d24e 100644 --- a/x-pack/plugins/uptime/server/plugin.ts +++ b/x-pack/plugins/uptime/server/plugin.ts @@ -7,11 +7,13 @@ import { PluginInitializerContext, CoreStart, CoreSetup } from '../../../../src/core/server'; import { initServerWithKibana } from './kibana.index'; import { UptimeCorePlugins } from './lib/adapters'; +import { umDynamicSettings } from './lib/saved_objects'; export class Plugin { constructor(_initializerContext: PluginInitializerContext) {} public setup(core: CoreSetup, plugins: UptimeCorePlugins) { initServerWithKibana({ route: core.http.createRouter() }, plugins); + core.savedObjects.registerType(umDynamicSettings); } public start(_core: CoreStart, _plugins: any) {} } diff --git a/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts b/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts new file mode 100644 index 0000000000000..2235379ba6f03 --- /dev/null +++ b/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { isRight } from 'fp-ts/lib/Either'; +import { PathReporter } from 'io-ts/lib/PathReporter'; +import { UMServerLibs } from '../lib/lib'; +import { + DynamicSettings, + DynamicSettingsType, +} from '../../../../legacy/plugins/uptime/common/runtime_types'; +import { UMRestApiRouteFactory } from '.'; +import { savedObjectsAdapter } from '../lib/saved_objects'; + +export const createGetDynamicSettingsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ + method: 'GET', + path: '/api/uptime/dynamic_settings', + validate: false, + options: { + tags: ['access:uptime-read'], + }, + handler: async ({ dynamicSettings }, _context, _request, response): Promise => { + return response.ok({ + body: dynamicSettings, + }); + }, +}); + +export const createPostDynamicSettingsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ + method: 'POST', + path: '/api/uptime/dynamic_settings', + validate: { + body: schema.object({}, { unknowns: 'allow' }), + }, + options: { + tags: ['access:uptime-write'], + }, + handler: async ({ savedObjectsClient }, _context, request, response): Promise => { + const decoded = DynamicSettingsType.decode(request.body); + if (isRight(decoded)) { + const newSettings: DynamicSettings = decoded.right; + await savedObjectsAdapter.setUptimeDynamicSettings(savedObjectsClient, newSettings); + + return response.ok({ + body: { + success: true, + }, + }); + } else { + const error = PathReporter.report(decoded).join(', '); + return response.badRequest({ + body: error, + }); + } + }, +}); diff --git a/x-pack/plugins/uptime/server/rest_api/index.ts b/x-pack/plugins/uptime/server/rest_api/index.ts index b0cc38ebfb4b6..000fba69fab00 100644 --- a/x-pack/plugins/uptime/server/rest_api/index.ts +++ b/x-pack/plugins/uptime/server/rest_api/index.ts @@ -6,6 +6,7 @@ import { createGetOverviewFilters } from './overview_filters'; import { createGetPingsRoute } from './pings'; +import { createGetDynamicSettingsRoute, createPostDynamicSettingsRoute } from './dynamic_settings'; import { createLogMonitorPageRoute, createLogOverviewPageRoute } from './telemetry'; import { createGetSnapshotCount } from './snapshot'; import { UMRestApiRouteFactory } from './types'; @@ -28,6 +29,8 @@ export const restApiRoutes: UMRestApiRouteFactory[] = [ createGetPingsRoute, createGetIndexPatternRoute, createGetIndexStatusRoute, + createGetDynamicSettingsRoute, + createPostDynamicSettingsRoute, createGetMonitorRoute, createGetMonitorDetailsRoute, createGetMonitorLocationsRoute, diff --git a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts index 806d6e789a890..cec5bb1be245f 100644 --- a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts +++ b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts @@ -13,13 +13,13 @@ export const createGetIndexPatternRoute: UMRestApiRouteFactory = (libs: UMServer path: API_URLS.INDEX_PATTERN, validate: false, options: { - tags: ['access:uptime'], + tags: ['access:uptime-read'], }, - handler: async ({ callES }, _context, _request, response): Promise => { + handler: async ({ callES, dynamicSettings }, _context, _request, response): Promise => { try { return response.ok({ body: { - ...(await libs.requests.getIndexPattern(callES)), + ...(await libs.requests.getIndexPattern({ callES, dynamicSettings })), }, }); } catch (e) { diff --git a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts index d4d76c86870ee..9c94ef92f9b6e 100644 --- a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts +++ b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts @@ -13,13 +13,13 @@ export const createGetIndexStatusRoute: UMRestApiRouteFactory = (libs: UMServerL path: API_URLS.INDEX_STATUS, validate: false, options: { - tags: ['access:uptime'], + tags: ['access:uptime-read'], }, - handler: async ({ callES }, _context, _request, response): Promise => { + handler: async ({ callES, dynamicSettings }, _context, _request, response): Promise => { try { return response.ok({ body: { - ...(await libs.requests.getIndexStatus({ callES })), + ...(await libs.requests.getIndexStatus({ callES, dynamicSettings })), }, }); } catch (e) { diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts index 131b3cbe2ab44..befa5fd7e0e55 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts @@ -20,15 +20,16 @@ export const createGetMonitorLocationsRoute: UMRestApiRouteFactory = (libs: UMSe }), }, options: { - tags: ['access:uptime'], + tags: ['access:uptime-read'], }, - handler: async ({ callES }, _context, request, response): Promise => { + handler: async ({ callES, dynamicSettings }, _context, request, response): Promise => { const { monitorId, dateStart, dateEnd } = request.query; return response.ok({ body: { ...(await libs.requests.getMonitorLocations({ callES, + dynamicSettings, monitorId, dateStart, dateEnd, diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts index 66e952813eb3e..b14eb2c138a75 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts @@ -20,14 +20,15 @@ export const createGetMonitorDetailsRoute: UMRestApiRouteFactory = (libs: UMServ }), }, options: { - tags: ['access:uptime'], + tags: ['access:uptime-read'], }, - handler: async ({ callES }, _context, request, response): Promise => { + handler: async ({ callES, dynamicSettings }, _context, request, response): Promise => { const { monitorId, dateStart, dateEnd } = request.query; return response.ok({ body: { ...(await libs.requests.getMonitorDetails({ callES, + dynamicSettings, monitorId, dateStart, dateEnd, diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts index f4a4cadc99976..10008c4f6c7ea 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts @@ -21,14 +21,15 @@ export const createGetMonitorDurationRoute: UMRestApiRouteFactory = (libs: UMSer }), }, options: { - tags: ['access:uptime'], + tags: ['access:uptime-read'], }, - handler: async ({ callES }, _context, request, response): Promise => { + handler: async ({ callES, dynamicSettings }, _context, request, response): Promise => { const { monitorId, dateStart, dateEnd } = request.query; return response.ok({ body: { ...(await libs.requests.getMonitorDurationChart({ callES, + dynamicSettings, monitorId, dateStart, dateEnd, diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/status.ts b/x-pack/plugins/uptime/server/rest_api/monitors/status.ts index 08cbc2d70e515..e1fcaf54f2824 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/status.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/status.ts @@ -19,14 +19,14 @@ export const createGetMonitorRoute: UMRestApiRouteFactory = (libs: UMServerLibs) }), }, options: { - tags: ['access:uptime'], + tags: ['access:uptime-read'], }, - handler: async ({ callES }, _context, request, response): Promise => { + handler: async ({ callES, dynamicSettings }, _context, request, response): Promise => { const { monitorId } = request.query; return response.ok({ body: { - ...(await libs.requests.getMonitor({ callES, monitorId })), + ...(await libs.requests.getMonitor({ callES, dynamicSettings, monitorId })), }, }); }, @@ -44,12 +44,13 @@ export const createGetStatusBarRoute: UMRestApiRouteFactory = (libs: UMServerLib }), }, options: { - tags: ['access:uptime'], + tags: ['access:uptime-read'], }, - handler: async ({ callES }, _context, request, response): Promise => { + handler: async ({ callES, dynamicSettings }, _context, request, response): Promise => { const { monitorId, dateStart, dateEnd } = request.query; const result = await libs.requests.getLatestMonitor({ callES, + dynamicSettings, monitorId, dateStart, dateEnd, diff --git a/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts b/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts index 5525771539c63..05376f061c05f 100644 --- a/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts +++ b/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts @@ -30,9 +30,9 @@ export const createGetOverviewFilters: UMRestApiRouteFactory = (libs: UMServerLi }, options: { - tags: ['access:uptime'], + tags: ['access:uptime-read'], }, - handler: async ({ callES }, _context, request, response) => { + handler: async ({ callES, dynamicSettings }, _context, request, response) => { const { dateRangeStart, dateRangeEnd, locations, schemes, search, ports, tags } = request.query; let parsedSearch: Record | undefined; @@ -46,6 +46,7 @@ export const createGetOverviewFilters: UMRestApiRouteFactory = (libs: UMServerLi const filtersResponse = await libs.requests.getFilterBar({ callES, + dynamicSettings, dateRangeStart, dateRangeEnd, search: parsedSearch, diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_all.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_all.ts index e301a2cbf9af9..d64c76fc18a80 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_all.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_all.ts @@ -24,13 +24,14 @@ export const createGetAllRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => }), }, options: { - tags: ['access:uptime'], + tags: ['access:uptime-read'], }, - handler: async ({ callES }, _context, request, response): Promise => { + handler: async ({ callES, dynamicSettings }, _context, request, response): Promise => { const { dateRangeStart, dateRangeEnd, location, monitorId, size, sort, status } = request.query; const result = await libs.requests.getPings({ callES, + dynamicSettings, dateRangeStart, dateRangeEnd, monitorId, diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts index dfaabcdf93a06..cbd9ada027b31 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts @@ -22,13 +22,14 @@ export const createGetPingHistogramRoute: UMRestApiRouteFactory = (libs: UMServe }), }, options: { - tags: ['access:uptime'], + tags: ['access:uptime-read'], }, - handler: async ({ callES }, _context, request, response): Promise => { + handler: async ({ callES, dynamicSettings }, _context, request, response): Promise => { const { dateStart, dateEnd, statusFilter, monitorId, filters } = request.query; const result = await libs.requests.getPingHistogram({ callES, + dynamicSettings, from: dateStart, to: dateEnd, monitorId, diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts index 458107dd87a77..8129ad70e6d7d 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts @@ -24,13 +24,14 @@ export const createGetPingsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) = }), }, options: { - tags: ['access:uptime'], + tags: ['access:uptime-read'], }, - handler: async ({ callES }, _context, request, response): Promise => { + handler: async ({ callES, dynamicSettings }, _context, request, response): Promise => { const { dateRangeStart, dateRangeEnd, location, monitorId, size, sort, status } = request.query; const result = await libs.requests.getPings({ callES, + dynamicSettings, dateRangeStart, dateRangeEnd, monitorId, diff --git a/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts b/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts index 697c49dc8300b..4fda95bbf86da 100644 --- a/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts +++ b/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts @@ -21,12 +21,13 @@ export const createGetSnapshotCount: UMRestApiRouteFactory = (libs: UMServerLibs }), }, options: { - tags: ['access:uptime'], + tags: ['access:uptime-read'], }, - handler: async ({ callES }, _context, request, response): Promise => { + handler: async ({ callES, dynamicSettings }, _context, request, response): Promise => { const { dateRangeStart, dateRangeEnd, filters, statusFilter } = request.query; const result = await libs.requests.getSnapshotCount({ callES, + dynamicSettings, dateRangeStart, dateRangeEnd, filters, diff --git a/x-pack/plugins/uptime/server/rest_api/telemetry/log_monitor_page.ts b/x-pack/plugins/uptime/server/rest_api/telemetry/log_monitor_page.ts index fca1e6c8d5d46..71d6b8025dff2 100644 --- a/x-pack/plugins/uptime/server/rest_api/telemetry/log_monitor_page.ts +++ b/x-pack/plugins/uptime/server/rest_api/telemetry/log_monitor_page.ts @@ -16,6 +16,6 @@ export const createLogMonitorPageRoute: UMRestApiRouteFactory = () => ({ return response.ok(); }, options: { - tags: ['access:uptime'], + tags: ['access:uptime-read'], }, }); diff --git a/x-pack/plugins/uptime/server/rest_api/telemetry/log_overview_page.ts b/x-pack/plugins/uptime/server/rest_api/telemetry/log_overview_page.ts index 37ed2e5ff5c2c..de1ac5f4ed735 100644 --- a/x-pack/plugins/uptime/server/rest_api/telemetry/log_overview_page.ts +++ b/x-pack/plugins/uptime/server/rest_api/telemetry/log_overview_page.ts @@ -16,6 +16,6 @@ export const createLogOverviewPageRoute: UMRestApiRouteFactory = () => ({ return response.ok(); }, options: { - tags: ['access:uptime'], + tags: ['access:uptime-read'], }, }); diff --git a/x-pack/plugins/uptime/server/rest_api/types.ts b/x-pack/plugins/uptime/server/rest_api/types.ts index a0566c225eae7..8bb1e8a6a86c0 100644 --- a/x-pack/plugins/uptime/server/rest_api/types.ts +++ b/x-pack/plugins/uptime/server/rest_api/types.ts @@ -16,6 +16,7 @@ import { KibanaResponseFactory, IKibanaResponse, } from 'src/core/server'; +import { DynamicSettings } from '../../../../legacy/plugins/uptime/common/runtime_types'; import { UMServerLibs } from '../lib/lib'; /** @@ -66,6 +67,7 @@ export interface UMRouteParams { clientParams?: Record, options?: CallAPIOptions | undefined ) => Promise; + dynamicSettings: DynamicSettings; savedObjectsClient: Pick< SavedObjectsClient, | 'errors' diff --git a/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts b/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts index fb874edebee60..676aced23a25e 100644 --- a/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts +++ b/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts @@ -5,6 +5,7 @@ */ import { UMKibanaRouteWrapper } from './types'; +import { savedObjectsAdapter } from '../lib/saved_objects'; export const uptimeRouteWrapper: UMKibanaRouteWrapper = uptimeRoute => { return { @@ -12,7 +13,15 @@ export const uptimeRouteWrapper: UMKibanaRouteWrapper = uptimeRoute => { handler: async (context, request, response) => { const { callAsCurrentUser: callES } = context.core.elasticsearch.dataClient; const { client: savedObjectsClient } = context.core.savedObjects; - return await uptimeRoute.handler({ callES, savedObjectsClient }, context, request, response); + const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings( + savedObjectsClient + ); + return await uptimeRoute.handler( + { callES, savedObjectsClient, dynamicSettings }, + context, + request, + response + ); }, }; }; diff --git a/x-pack/test/api_integration/apis/uptime/feature_controls.ts b/x-pack/test/api_integration/apis/uptime/feature_controls.ts index 91ea1bedb061a..4c3b7f97c9544 100644 --- a/x-pack/test/api_integration/apis/uptime/feature_controls.ts +++ b/x-pack/test/api_integration/apis/uptime/feature_controls.ts @@ -40,10 +40,9 @@ export default function featureControlsTests({ getService }: FtrProviderContext) const executePingsRequest = async (username: string, password: string, spaceId?: string) => { const basePath = spaceId ? `/s/${spaceId}` : ''; + const url = `${basePath}${API_URLS.PINGS}?sort=desc&dateRangeStart=${PINGS_DATE_RANGE_START}&dateRangeEnd=${PINGS_DATE_RANGE_END}`; return await supertest - .get( - `${basePath}/api/uptime/pings?sort=desc&dateRangeStart=${PINGS_DATE_RANGE_START}&dateRangeEnd=${PINGS_DATE_RANGE_END}` - ) + .get(url) .auth(username, password) .set('kbn-xsrf', 'foo') .then((response: any) => ({ error: undefined, response })) @@ -51,9 +50,9 @@ export default function featureControlsTests({ getService }: FtrProviderContext) }; describe('feature controls', () => { - it(`APIs can't be accessed by heartbeat-* read privileges role`, async () => { - const username = 'logstash_read'; - const roleName = 'logstash_read'; + it(`APIs can be accessed by heartbeat-* read privileges role`, async () => { + const username = 'heartbeat_read'; + const roleName = 'heartbeat_read'; const password = `${username}-password`; try { await security.role.create(roleName, { diff --git a/x-pack/test/api_integration/apis/uptime/graphql/helpers/expect_fixture_eql.ts b/x-pack/test/api_integration/apis/uptime/graphql/helpers/expect_fixture_eql.ts index 45cc9011773a9..d5a4f3976e079 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/helpers/expect_fixture_eql.ts +++ b/x-pack/test/api_integration/apis/uptime/graphql/helpers/expect_fixture_eql.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import fs from 'fs'; import { join } from 'path'; -import { cloneDeep } from 'lodash'; +import { cloneDeep, isEqual } from 'lodash'; const fixturesDir = join(__dirname, '..', 'fixtures'); const restFixturesDir = join(__dirname, '../../rest/', 'fixtures'); @@ -21,14 +21,26 @@ const excludeFieldsFrom = (from: any, excluder?: (d: any) => any): any => { }; export const expectFixtureEql = (data: T, fixtureName: string, excluder?: (d: T) => void) => { + expect(data).not.to.eql(null); + expect(data).not.to.eql(undefined); + let fixturePath = join(fixturesDir, `${fixtureName}.json`); if (!fs.existsSync(fixturePath)) { fixturePath = join(restFixturesDir, `${fixtureName}.json`); } + excluder = excluder || (d => d); const dataExcluded = excludeFieldsFrom(data, excluder); expect(dataExcluded).not.to.be(undefined); - if (process.env.UPDATE_UPTIME_FIXTURES) { + const fixtureExists = () => fs.existsSync(dataExcluded); + const fixtureChanged = () => + !isEqual( + dataExcluded, + excludeFieldsFrom(JSON.parse(fs.readFileSync(fixturePath, 'utf8')), excluder) + ); + if (process.env.UPDATE_UPTIME_FIXTURES && (!fixtureExists() || fixtureChanged())) { + // Check if the data has changed. We can't simply write it because the order of attributes + // can change leading to different bytes on disk, which we don't care about fs.writeFileSync(fixturePath, JSON.stringify(dataExcluded, null, 2)); } const fileContents = fs.readFileSync(fixturePath, 'utf8'); diff --git a/x-pack/test/api_integration/apis/uptime/graphql/helpers/make_checks.ts b/x-pack/test/api_integration/apis/uptime/graphql/helpers/make_checks.ts index 02ae194be98a7..ae326c8b2aee0 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/helpers/make_checks.ts +++ b/x-pack/test/api_integration/apis/uptime/graphql/helpers/make_checks.ts @@ -7,7 +7,7 @@ import uuid from 'uuid'; import { merge, flattenDeep } from 'lodash'; -const INDEX_NAME = 'heartbeat-8.0.0'; +const INDEX_NAME = 'heartbeat-8-generated-test'; export const makePing = async ( es: any, diff --git a/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts b/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts new file mode 100644 index 0000000000000..f4dd7c244f8b5 --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { defaultDynamicSettings } from '../../../../../legacy/plugins/uptime/common/runtime_types/dynamic_settings'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('dynamic settings', () => { + it('returns the defaults when no user settings have been saved', async () => { + const apiResponse = await supertest.get(`/api/uptime/dynamic_settings`); + expect(apiResponse.body).to.eql(defaultDynamicSettings as any); + }); + + it('can change the settings', async () => { + const newSettings = { heartbeatIndices: 'myIndex1*' }; + const postResponse = await supertest + .post(`/api/uptime/dynamic_settings`) + .set('kbn-xsrf', 'true') + .send(newSettings); + + expect(postResponse.body).to.eql({ success: true }); + expect(postResponse.status).to.eql(200); + + const getResponse = await supertest.get(`/api/uptime/dynamic_settings`); + expect(getResponse.body).to.eql(newSettings); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/uptime/rest/index.ts b/x-pack/test/api_integration/apis/uptime/rest/index.ts index 67b94f19c638f..712a8bc40c41c 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/index.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/index.ts @@ -5,18 +5,42 @@ */ import { FtrProviderContext } from '../../../ftr_provider_context'; +import { + settingsObjectId, + settingsObjectType, +} from '../../../../../plugins/uptime/server/lib/saved_objects'; export default function({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); + const server = getService('kibanaServer'); + describe('uptime REST endpoints', () => { + beforeEach('clear settings', async () => { + try { + server.savedObjects.delete({ + type: settingsObjectType, + id: settingsObjectId, + }); + } catch (e) { + // a 404 just means the doc is already missing + if (e.statuscode !== 404) { + throw new Error( + `error attempting to delete settings (${e.statuscode}): ${JSON.stringify(e)}` + ); + } + } + }); + describe('with generated data', () => { - before('load heartbeat data', () => esArchiver.load('uptime/blank')); - after('unload', () => esArchiver.unload('uptime/blank')); + before('load heartbeat data', async () => await esArchiver.load('uptime/blank')); + after('unload', async () => await esArchiver.unload('uptime/blank')); + loadTestFile(require.resolve('./snapshot')); + loadTestFile(require.resolve('./dynamic_settings')); }); describe('with real-world data', () => { - before('load heartbeat data', () => esArchiver.load('uptime/full_heartbeat')); - after('unload', () => esArchiver.unload('uptime/full_heartbeat')); + before('load heartbeat data', async () => await esArchiver.load('uptime/full_heartbeat')); + after('unload', async () => await esArchiver.unload('uptime/full_heartbeat')); loadTestFile(require.resolve('./monitor_latest_status')); loadTestFile(require.resolve('./selected_monitor')); loadTestFile(require.resolve('./ping_histogram')); diff --git a/x-pack/test/functional/apps/uptime/index.ts b/x-pack/test/functional/apps/uptime/index.ts index 273b7659b5f46..446f28d182926 100644 --- a/x-pack/test/functional/apps/uptime/index.ts +++ b/x-pack/test/functional/apps/uptime/index.ts @@ -5,23 +5,51 @@ */ import { FtrProviderContext } from '../../ftr_provider_context'; +import { + settingsObjectId, + settingsObjectType, +} from '../../../../plugins/uptime/server/lib/saved_objects'; const ARCHIVE = 'uptime/full_heartbeat'; export default ({ loadTestFile, getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); + const server = getService('kibanaServer'); describe('Uptime app', function() { this.tags('ciGroup6'); + + beforeEach('delete settings', async () => { + // delete the saved object + try { + await server.savedObjects.delete({ + type: settingsObjectType, + id: settingsObjectId, + }); + } catch (e) { + // If it's not found that's fine, we just want to ensure + // this is the default state + if (e.response?.status !== 404) { + throw e; + } + } + }); + describe('with generated data', () => { - before('load heartbeat data', async () => await esArchiver.load('uptime/blank')); - after('unload', async () => await esArchiver.unload('uptime/blank')); + beforeEach('load heartbeat data', async () => { + await esArchiver.load('uptime/blank'); + }); + afterEach('unload', async () => { + await esArchiver.unload('uptime/blank'); + }); loadTestFile(require.resolve('./locations')); + loadTestFile(require.resolve('./settings')); }); describe('with real-world data', () => { before(async () => { + await esArchiver.unload(ARCHIVE); await esArchiver.load(ARCHIVE); await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'UTC' }); }); diff --git a/x-pack/test/functional/apps/uptime/locations.ts b/x-pack/test/functional/apps/uptime/locations.ts index fe9030109145d..7f6932ab50319 100644 --- a/x-pack/test/functional/apps/uptime/locations.ts +++ b/x-pack/test/functional/apps/uptime/locations.ts @@ -15,7 +15,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const end = new Date().toISOString(); const MONITOR_ID = 'location-testing-id'; - before(async () => { + beforeEach(async () => { /** * This mogrify function will strip the documents of their location * data (but preserve their location name), which is necessary for diff --git a/x-pack/test/functional/apps/uptime/settings.ts b/x-pack/test/functional/apps/uptime/settings.ts new file mode 100644 index 0000000000000..22d272bc9aa36 --- /dev/null +++ b/x-pack/test/functional/apps/uptime/settings.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { + defaultDynamicSettings, + DynamicSettings, +} from '../../../../legacy/plugins/uptime/common/runtime_types/dynamic_settings'; +import { makeChecks } from '../../../api_integration/apis/uptime/graphql/helpers/make_checks'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const pageObjects = getPageObjects(['uptime']); + const es = getService('es'); + + describe('uptime settings page', () => { + const settingsPage = () => pageObjects.uptime.settings; + beforeEach('navigate to clean app root', async () => { + // make 10 checks + await makeChecks(es, 'myMonitor', 1, 1, 1); + await pageObjects.uptime.goToRoot(); + }); + + it('loads the default settings', async () => { + await pageObjects.uptime.settings.go(); + + const fields = await settingsPage().loadFields(); + expect(fields).to.eql(defaultDynamicSettings); + }); + + it('should disable the apply button when invalid or unchanged', async () => { + await pageObjects.uptime.settings.go(); + + // Disabled because it's the original value + expect(await settingsPage().applyButtonIsDisabled()).to.eql(true); + + // Enabled because it's a new, different, value + await settingsPage().changeHeartbeatIndicesInput('somethingNew'); + expect(await settingsPage().applyButtonIsDisabled()).to.eql(false); + + // Disabled because it's blank + await settingsPage().changeHeartbeatIndicesInput(''); + expect(await settingsPage().applyButtonIsDisabled()).to.eql(true); + }); + + it('changing index pattern setting is reflected elsewhere in UI', async () => { + const originalCount = await pageObjects.uptime.getSnapshotCount(); + // We should find 1 monitor up with the default index pattern + expect(originalCount.up).to.eql(1); + + await pageObjects.uptime.settings.go(); + + const newFieldValues: DynamicSettings = { heartbeatIndices: 'new*' }; + await settingsPage().changeHeartbeatIndicesInput(newFieldValues.heartbeatIndices); + await settingsPage().apply(); + + await pageObjects.uptime.goToRoot(); + + // We should no longer find any monitors since the new pattern matches nothing + await pageObjects.uptime.pageHasDataMissing(); + + // Verify that the settings page shows the value we previously saved + await pageObjects.uptime.settings.go(); + const fields = await settingsPage().loadFields(); + expect(fields).to.eql(newFieldValues); + }); + }); +}; diff --git a/x-pack/test/functional/es_archives/uptime/blank/mappings.json b/x-pack/test/functional/es_archives/uptime/blank/mappings.json index a1b0696cdaadc..7879c82612a96 100644 --- a/x-pack/test/functional/es_archives/uptime/blank/mappings.json +++ b/x-pack/test/functional/es_archives/uptime/blank/mappings.json @@ -6,7 +6,7 @@ "is_write_index": true } }, - "index": "heartbeat-8-test", + "index": "heartbeat-8-generated-test", "mappings": { "_meta": { "beat": "heartbeat", diff --git a/x-pack/test/functional/es_archives/uptime/full_heartbeat/data.json.gz b/x-pack/test/functional/es_archives/uptime/full_heartbeat/data.json.gz index edc29c000e2e1..250db8c8471d7 100644 Binary files a/x-pack/test/functional/es_archives/uptime/full_heartbeat/data.json.gz and b/x-pack/test/functional/es_archives/uptime/full_heartbeat/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/uptime/full_heartbeat/mappings.json b/x-pack/test/functional/es_archives/uptime/full_heartbeat/mappings.json index be0e98a5a4927..2b6002ddb3fab 100644 --- a/x-pack/test/functional/es_archives/uptime/full_heartbeat/mappings.json +++ b/x-pack/test/functional/es_archives/uptime/full_heartbeat/mappings.json @@ -2,11 +2,11 @@ "type": "index", "value": { "aliases": { - "heartbeat-8.0.0": { + "heartbeat-8.0.0-full": { "is_write_index": true } }, - "index": "heartbeat-8.0.0-2019.09.11-000001", + "index": "heartbeat-8-full-test", "mappings": { "_meta": { "beat": "heartbeat", diff --git a/x-pack/test/functional/page_objects/uptime_page.ts b/x-pack/test/functional/page_objects/uptime_page.ts index 57842ffbb2c5d..e18c7d4154728 100644 --- a/x-pack/test/functional/page_objects/uptime_page.ts +++ b/x-pack/test/functional/page_objects/uptime_page.ts @@ -13,6 +13,14 @@ export function UptimePageProvider({ getPageObjects, getService }: FtrProviderCo const retry = getService('retry'); return new (class UptimePage { + public get settings() { + return uptimeService.settings; + } + + public async goToRoot() { + await pageObjects.common.navigateToApp('uptime'); + } + public async goToUptimePageAndSetDateRange( datePickerStartValue: string, datePickerEndValue: string @@ -54,6 +62,10 @@ export function UptimePageProvider({ getPageObjects, getService }: FtrProviderCo await uptimeService.setFilterText(filterQuery); } + public async pageHasDataMissing() { + return await uptimeService.pageHasDataMissing(); + } + public async pageHasExpectedIds(monitorIdsToCheck: string[]) { await Promise.all(monitorIdsToCheck.map(id => uptimeService.monitorPageLinkExists(id))); } diff --git a/x-pack/test/functional/services/uptime.ts b/x-pack/test/functional/services/uptime.ts index 7994a7e934033..57beedc5e0f29 100644 --- a/x-pack/test/functional/services/uptime.ts +++ b/x-pack/test/functional/services/uptime.ts @@ -11,7 +11,38 @@ export function UptimeProvider({ getService }: FtrProviderContext) { const browser = getService('browser'); const retry = getService('retry'); + const settings = { + go: async () => { + await testSubjects.click('settings-page-link', 5000); + }, + changeHeartbeatIndicesInput: async (text: string) => { + const input = await testSubjects.find('heartbeat-indices-input', 5000); + await input.clearValueWithKeyboard(); + await input.type(text); + }, + loadFields: async () => { + const heartbeatIndices = await ( + await testSubjects.find('heartbeat-indices-input', 5000) + ).getAttribute('value'); + return { heartbeatIndices }; + }, + applyButtonIsDisabled: async () => { + return !!(await (await testSubjects.find('apply-settings-button')).getAttribute('disabled')); + }, + apply: async () => { + await (await testSubjects.find('apply-settings-button')).click(); + await retry.waitFor('submit to succeed', async () => { + // When the form submit is complete the form will no longer be disabled + const disabled = await ( + await testSubjects.find('heartbeat-indices-input', 5000) + ).getAttribute('disabled'); + return disabled === null; + }); + }, + }; + return { + settings, alerts: { async openFlyout() { await testSubjects.click('xpack.uptime.alertsPopover.toggleButton', 5000); @@ -120,6 +151,9 @@ export function UptimeProvider({ getService }: FtrProviderContext) { async getMonitorNameDisplayedOnPageTitle() { return await testSubjects.getVisibleText('monitor-page-title'); }, + async pageHasDataMissing() { + return await testSubjects.find('data-missing', 5000); + }, async setKueryBarText(attribute: string, value: string) { await testSubjects.click(attribute); await testSubjects.setValue(attribute, value);