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 index 8dedd4672eeae..985b51891da99 100644 --- a/x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts @@ -6,10 +6,20 @@ import * as t from 'io-ts'; -export const DynamicSettingsType = t.type({ - heartbeatIndices: t.string, +export const CertificatesStatesThresholdType = t.interface({ + warningState: t.number, + errorState: t.number, }); +export const DynamicSettingsType = t.intersection([ + t.type({ + heartbeatIndices: t.string, + }), + t.partial({ + certificatesThresholds: CertificatesStatesThresholdType, + }), +]); + export const DynamicSettingsSaveType = t.intersection([ t.type({ success: t.boolean, @@ -21,7 +31,12 @@ export const DynamicSettingsSaveType = t.intersection([ export type DynamicSettings = t.TypeOf; export type DynamicSettingsSaveResponse = t.TypeOf; +export type CertificatesStatesThreshold = t.TypeOf; export const defaultDynamicSettings: DynamicSettings = { heartbeatIndices: 'heartbeat-8*', + certificatesThresholds: { + errorState: 7, + warningState: 30, + }, }; diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap new file mode 100644 index 0000000000000..36bc9bb860211 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap @@ -0,0 +1,69 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CertificateForm shallow renders expected elements for valid props 1`] = ` + + + +`; diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap new file mode 100644 index 0000000000000..93151198c0f49 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap @@ -0,0 +1,69 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CertificateForm shallow renders expected elements for valid props 1`] = ` + + + +`; diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx new file mode 100644 index 0000000000000..a3158f3d72445 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx @@ -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 React from 'react'; +import { CertificateExpirationForm } from '../certificate_form'; +import { shallowWithRouter } from '../../../lib'; + +describe('CertificateForm', () => { + it('shallow renders expected elements for valid props', () => { + expect( + shallowWithRouter( + + ) + ).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx new file mode 100644 index 0000000000000..654d51019d4e5 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx @@ -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 React from 'react'; +import { IndicesForm } from '../indices_form'; +import { shallowWithRouter } from '../../../lib'; + +describe('CertificateForm', () => { + it('shallow renders expected elements for valid props', () => { + expect( + shallowWithRouter( + + ) + ).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/certificate_form.tsx b/x-pack/legacy/plugins/uptime/public/components/settings/certificate_form.tsx new file mode 100644 index 0000000000000..5103caee1e1c0 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/settings/certificate_form.tsx @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useSelector } from 'react-redux'; +import { + EuiDescribedFormGroup, + EuiFormRow, + EuiCode, + EuiFieldNumber, + EuiTitle, + EuiSpacer, + EuiSelect, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { defaultDynamicSettings, DynamicSettings } from '../../../common/runtime_types'; +import { selectDynamicSettings } from '../../state/selectors'; + +type NumStr = string | number; + +export type OnFieldChangeType = (field: string, value?: NumStr) => void; + +export interface SettingsFormProps { + onChange: OnFieldChangeType; + formFields: DynamicSettings | null; + fieldErrors: any; + isDisabled: boolean; +} + +export const CertificateExpirationForm: React.FC = ({ + onChange, + formFields, + fieldErrors, + isDisabled, +}) => { + const dss = useSelector(selectDynamicSettings); + + return ( + <> + +

+ +

+
+ + + + + } + description={ + + } + > + {defaultDynamicSettings?.certificatesThresholds?.errorState} + ), + }} + /> + } + isInvalid={!!fieldErrors?.certificatesThresholds?.errorState} + label={ + + } + > + + + + onChange( + 'certificatesThresholds.errorState', + value === '' ? undefined : Number(value) + ) + } + /> + + + + + + + {defaultDynamicSettings?.certificatesThresholds?.warningState} + ), + }} + /> + } + isInvalid={!!fieldErrors?.certificatesThresholds?.warningState} + label={ + + } + > + + + + onChange('certificatesThresholds.warningState', Number(event.currentTarget.value)) + } + /> + + + + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/indices_form.tsx b/x-pack/legacy/plugins/uptime/public/components/settings/indices_form.tsx new file mode 100644 index 0000000000000..c28eca2ea229e --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/settings/indices_form.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useSelector } from 'react-redux'; +import { + EuiDescribedFormGroup, + EuiFormRow, + EuiCode, + EuiFieldText, + EuiTitle, + EuiSpacer, +} from '@elastic/eui'; +import { defaultDynamicSettings } from '../../../common/runtime_types'; +import { selectDynamicSettings } from '../../state/selectors'; +import { SettingsFormProps } from './certificate_form'; + +export const IndicesForm: React.FC = ({ + onChange, + formFields, + fieldErrors, + isDisabled, +}) => { + const dss = useSelector(selectDynamicSettings); + + return ( + <> + +

+ +

+
+ + + + + } + description={ + + } + > + {defaultDynamicSettings.heartbeatIndices}, + }} + /> + } + isInvalid={!!fieldErrors?.heartbeatIndices} + label={ + + } + > + onChange('heartbeatIndices', event.currentTarget.value)} + /> + + + + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/pages/settings.tsx b/x-pack/legacy/plugins/uptime/public/pages/settings.tsx index 765b0e3c664bc..049dffecd3f2e 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/settings.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/settings.tsx @@ -9,46 +9,54 @@ import { EuiButton, EuiButtonEmpty, EuiCallOut, - EuiCode, - EuiDescribedFormGroup, - EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiForm, - EuiFormRow, EuiPanel, EuiSpacer, - EuiTitle, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { connect } from 'react-redux'; -import { isEqual } from 'lodash'; +import { useDispatch, useSelector } from 'react-redux'; +import { cloneDeep, isEqual, set } 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 { defaultDynamicSettings, DynamicSettings } from '../../common/runtime_types'; +import { DynamicSettings } from '../../common/runtime_types'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; import { OVERVIEW_ROUTE } from '../../common/constants'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { UptimePage, useUptimeTelemetry } from '../hooks'; +import { IndicesForm } from '../components/settings/indices_form'; +import { + CertificateExpirationForm, + OnFieldChangeType, +} from '../components/settings/certificate_form'; + +const getFieldErrors = (formFields: DynamicSettings | null) => { + if (formFields) { + const blankStr = 'May not be blank'; + const { certificatesThresholds, heartbeatIndices } = formFields; + const heartbeatIndErr = heartbeatIndices.match(/^\S+$/) ? '' : blankStr; + const errorStateErr = certificatesThresholds?.errorState ? null : blankStr; + const warningStateErr = certificatesThresholds?.warningState ? null : blankStr; + return { + heartbeatIndices: heartbeatIndErr, + certificatesThresholds: + errorStateErr || warningStateErr + ? { + errorState: errorStateErr, + warningState: warningStateErr, + } + : null, + }; + } + return null; +}; -interface Props { - dynamicSettingsState: DynamicSettingsState; -} - -interface DispatchProps { - dispatchGetDynamicSettings: typeof getDynamicSettings; - dispatchSetDynamicSettings: typeof setDynamicSettings; -} +export const SettingsPage = () => { + const dss = useSelector(selectDynamicSettings); -export const SettingsPageComponent = ({ - dynamicSettingsState: dss, - dispatchGetDynamicSettings, - dispatchSetDynamicSettings, -}: Props & DispatchProps) => { const settingsBreadcrumbText = i18n.translate('xpack.uptime.settingsBreadcrumbText', { defaultMessage: 'Settings', }); @@ -56,9 +64,11 @@ export const SettingsPageComponent = ({ useUptimeTelemetry(UptimePage.Settings); + const dispatch = useDispatch(); + useEffect(() => { - dispatchGetDynamicSettings({}); - }, [dispatchGetDynamicSettings]); + dispatch(getDynamicSettings({})); + }, [dispatch]); const [formFields, setFormFields] = useState(dss.settings || null); @@ -66,22 +76,22 @@ export const SettingsPageComponent = ({ setFormFields({ ...dss.settings }); } - const fieldErrors = formFields && { - heartbeatIndices: formFields.heartbeatIndices.match(/^\S+$/) ? null : 'May not be blank', - }; + const fieldErrors = getFieldErrors(formFields); + const isFormValid = !(fieldErrors && Object.values(fieldErrors).find(v => !!v)); - const onChangeFormField = (field: keyof DynamicSettings, value: any) => { + const onChangeFormField: OnFieldChangeType = (field, value) => { if (formFields) { - formFields[field] = value; - setFormFields({ ...formFields }); + const newFormFields = cloneDeep(formFields); + set(newFormFields, field, value); + setFormFields(cloneDeep(newFormFields)); } }; const onApply = (event: React.FormEvent) => { event.preventDefault(); if (formFields) { - dispatchSetDynamicSettings(formFields); + dispatch(setDynamicSettings(formFields)); } }; @@ -128,68 +138,18 @@ export const SettingsPageComponent = ({
- -

- -

-
- - - - - } - description={ - - } - > - {defaultDynamicSettings.heartbeatIndices} - ), - }} - /> - } - isInvalid={!!fieldErrors?.heartbeatIndices} - label={ - - } - > - - onChangeFormField('heartbeatIndices', event.currentTarget.value) - } - /> - - + + @@ -230,18 +190,3 @@ export const SettingsPageComponent = ({ ); }; - -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/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts index 2eb17d588d297..2cc6f23ebaae5 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 @@ -88,6 +88,10 @@ describe('status check alert', () => { Object { "callES": [MockFunction], "dynamicSettings": Object { + "certificatesThresholds": Object { + "errorState": 7, + "warningState": 30, + }, "heartbeatIndices": "heartbeat-8*", }, "locations": Array [], @@ -131,6 +135,10 @@ describe('status check alert', () => { Object { "callES": [MockFunction], "dynamicSettings": Object { + "certificatesThresholds": Object { + "errorState": 7, + "warningState": 30, + }, "heartbeatIndices": "heartbeat-8*", }, "locations": Array [], diff --git a/x-pack/plugins/uptime/server/lib/saved_objects.ts b/x-pack/plugins/uptime/server/lib/saved_objects.ts index 175634ef797cc..9067fcb991900 100644 --- a/x-pack/plugins/uptime/server/lib/saved_objects.ts +++ b/x-pack/plugins/uptime/server/lib/saved_objects.ts @@ -7,14 +7,10 @@ import { DynamicSettings, defaultDynamicSettings, -} from '../../../../legacy/plugins/uptime/common/runtime_types/dynamic_settings'; +} from '../../../../legacy/plugins/uptime/common/runtime_types'; import { SavedObjectsType, SavedObjectsErrorHelpers } from '../../../../../src/core/server'; import { UMSavedObjectsQueryFn } from './adapters'; -export interface UMDynamicSettingsType { - heartbeatIndices: string; -} - export interface UMSavedObjectsAdapter { getUptimeDynamicSettings: UMSavedObjectsQueryFn; setUptimeDynamicSettings: UMSavedObjectsQueryFn; @@ -32,6 +28,16 @@ export const umDynamicSettings: SavedObjectsType = { heartbeatIndices: { type: 'keyword', }, + certificatesThresholds: { + properties: { + errorState: { + type: 'long', + }, + warningState: { + type: 'long', + }, + }, + }, }, }, }; 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 index f4dd7c244f8b5..a1b731169f0a0 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts @@ -18,7 +18,13 @@ export default function({ getService }: FtrProviderContext) { }); it('can change the settings', async () => { - const newSettings = { heartbeatIndices: 'myIndex1*' }; + const newSettings = { + heartbeatIndices: 'myIndex1*', + certificatesThresholds: { + errorState: 5, + warningState: 15, + }, + }; const postResponse = await supertest .post(`/api/uptime/dynamic_settings`) .set('kbn-xsrf', 'true') diff --git a/x-pack/test/functional/apps/uptime/settings.ts b/x-pack/test/functional/apps/uptime/settings.ts index 3294d928b61b3..64cfee50ac982 100644 --- a/x-pack/test/functional/apps/uptime/settings.ts +++ b/x-pack/test/functional/apps/uptime/settings.ts @@ -74,7 +74,41 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Verify that the settings page shows the value we previously saved await settings.go(); const fields = await settings.loadFields(); - expect(fields).to.eql(newFieldValues); + expect(fields.heartbeatIndices).to.eql(newFieldValues.heartbeatIndices); + }); + + it('changing certificate expiration error threshold is reflected in settings page', async () => { + const settings = uptimeService.settings; + + await settings.go(); + + const newErrorThreshold = '5'; + await settings.changeErrorThresholdInput(newErrorThreshold); + await settings.apply(); + + await uptimePage.goToRoot(); + + // Verify that the settings page shows the value we previously saved + await settings.go(); + const fields = await settings.loadFields(); + expect(fields.certificatesThresholds.errorState).to.eql(newErrorThreshold); + }); + + it('changing certificate expiration warning threshold is reflected in settings page', async () => { + const settings = uptimeService.settings; + + await settings.go(); + + const newWarningThreshold = '15'; + await settings.changeWarningThresholdInput(newWarningThreshold); + await settings.apply(); + + await uptimePage.goToRoot(); + + // Verify that the settings page shows the value we previously saved + await settings.go(); + const fields = await settings.loadFields(); + expect(fields.certificatesThresholds.warningState).to.eql(newWarningThreshold); }); }); }; diff --git a/x-pack/test/functional/services/uptime/settings.ts b/x-pack/test/functional/services/uptime/settings.ts index a64d39cd62a6d..14cab368b766a 100644 --- a/x-pack/test/functional/services/uptime/settings.ts +++ b/x-pack/test/functional/services/uptime/settings.ts @@ -10,20 +10,41 @@ export function UptimeSettingsProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); + const changeInputField = async (text: string, field: string) => { + const input = await testSubjects.find(field, 5000); + await input.clearValueWithKeyboard(); + await input.type(text); + }; + return { go: async () => { await testSubjects.click('settings-page-link', 5000); }, + changeHeartbeatIndicesInput: async (text: string) => { - const input = await testSubjects.find('heartbeat-indices-input-loaded', 5000); - await input.clearValueWithKeyboard(); - await input.type(text); + await changeInputField(text, 'heartbeat-indices-input-loaded'); + }, + changeErrorThresholdInput: async (text: string) => { + await changeInputField(text, 'error-state-threshold-input-loaded'); + }, + changeWarningThresholdInput: async (text: string) => { + await changeInputField(text, 'warning-state-threshold-input-loaded'); }, loadFields: async () => { - const input = await testSubjects.find('heartbeat-indices-input-loaded', 5000); - const heartbeatIndices = await input.getAttribute('value'); + const indInput = await testSubjects.find('heartbeat-indices-input-loaded', 5000); + const errorInput = await testSubjects.find('error-state-threshold-input-loaded', 5000); + const warningInput = await testSubjects.find('warning-state-threshold-input-loaded', 5000); + const heartbeatIndices = await indInput.getAttribute('value'); + const errorThreshold = await errorInput.getAttribute('value'); + const warningThreshold = await warningInput.getAttribute('value'); - return { heartbeatIndices }; + return { + heartbeatIndices, + certificatesThresholds: { + errorState: errorThreshold, + warningState: warningThreshold, + }, + }; }, applyButtonIsDisabled: async () => { return !!(await (await testSubjects.find('apply-settings-button')).getAttribute('disabled'));