[Uptime] Certificate expiration threshold settings (#63682)

* update settings

* added cert form

* update settings

* update types

* update test

* updated tests

* updated snapshots
This commit is contained in:
Shahzad 2020-04-16 21:13:35 +02:00 committed by GitHub
parent d72de0ea16
commit a9399c3d91
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 600 additions and 123 deletions

View file

@ -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<typeof DynamicSettingsType>;
export type DynamicSettingsSaveResponse = t.TypeOf<typeof DynamicSettingsSaveType>;
export type CertificatesStatesThreshold = t.TypeOf<typeof CertificatesStatesThresholdType>;
export const defaultDynamicSettings: DynamicSettings = {
heartbeatIndices: 'heartbeat-8*',
certificatesThresholds: {
errorState: 7,
warningState: 30,
},
};

View file

@ -0,0 +1,69 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CertificateForm shallow renders expected elements for valid props 1`] = `
<ContextProvider
value={
Object {
"history": Object {
"action": "POP",
"block": [Function],
"canGo": [Function],
"createHref": [Function],
"entries": Array [
Object {
"hash": "",
"key": "TestKeyForTesting",
"pathname": "/",
"search": "",
"state": undefined,
},
],
"go": [Function],
"goBack": [Function],
"goForward": [Function],
"index": 0,
"length": 1,
"listen": [Function],
"location": Object {
"hash": "",
"key": "TestKeyForTesting",
"pathname": "/",
"search": "",
"state": undefined,
},
"push": [Function],
"replace": [Function],
},
"location": Object {
"hash": "",
"key": "TestKeyForTesting",
"pathname": "/",
"search": "",
"state": undefined,
},
"match": Object {
"isExact": true,
"params": Object {},
"path": "/",
"url": "/",
},
"staticContext": undefined,
}
}
>
<CertificateExpirationForm
fieldErrors={Object {}}
formFields={
Object {
"certificatesThresholds": Object {
"errorState": 7,
"warningState": 36,
},
"heartbeatIndices": "heartbeat-8*",
}
}
isDisabled={false}
onChange={[MockFunction]}
/>
</ContextProvider>
`;

View file

@ -0,0 +1,69 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CertificateForm shallow renders expected elements for valid props 1`] = `
<ContextProvider
value={
Object {
"history": Object {
"action": "POP",
"block": [Function],
"canGo": [Function],
"createHref": [Function],
"entries": Array [
Object {
"hash": "",
"key": "TestKeyForTesting",
"pathname": "/",
"search": "",
"state": undefined,
},
],
"go": [Function],
"goBack": [Function],
"goForward": [Function],
"index": 0,
"length": 1,
"listen": [Function],
"location": Object {
"hash": "",
"key": "TestKeyForTesting",
"pathname": "/",
"search": "",
"state": undefined,
},
"push": [Function],
"replace": [Function],
},
"location": Object {
"hash": "",
"key": "TestKeyForTesting",
"pathname": "/",
"search": "",
"state": undefined,
},
"match": Object {
"isExact": true,
"params": Object {},
"path": "/",
"url": "/",
},
"staticContext": undefined,
}
}
>
<IndicesForm
fieldErrors={Object {}}
formFields={
Object {
"certificatesThresholds": Object {
"errorState": 7,
"warningState": 36,
},
"heartbeatIndices": "heartbeat-8*",
}
}
isDisabled={false}
onChange={[MockFunction]}
/>
</ContextProvider>
`;

View file

@ -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(
<CertificateExpirationForm
onChange={jest.fn()}
formFields={{
heartbeatIndices: 'heartbeat-8*',
certificatesThresholds: { errorState: 7, warningState: 36 },
}}
fieldErrors={{}}
isDisabled={false}
/>
)
).toMatchSnapshot();
});
});

View file

@ -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(
<IndicesForm
onChange={jest.fn()}
formFields={{
heartbeatIndices: 'heartbeat-8*',
certificatesThresholds: { errorState: 7, warningState: 36 },
}}
fieldErrors={{}}
isDisabled={false}
/>
)
).toMatchSnapshot();
});
});

View file

@ -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<SettingsFormProps> = ({
onChange,
formFields,
fieldErrors,
isDisabled,
}) => {
const dss = useSelector(selectDynamicSettings);
return (
<>
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.uptime.sourceConfiguration.certificationSectionTitle"
defaultMessage="Certificate Expiration"
/>
</h3>
</EuiTitle>
<EuiSpacer size="m" />
<EuiDescribedFormGroup
title={
<h4>
<FormattedMessage
id="xpack.uptime.sourceConfiguration.stateThresholds"
defaultMessage="Expiration State Thresholds"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.uptime.sourceConfiguration.stateThresholdsDescription"
defaultMessage="Set certificate expiration warning/error thresholds"
/>
}
>
<EuiFormRow
describedByIds={['errorState']}
error={fieldErrors?.certificatesThresholds?.errorState}
fullWidth
helpText={
<FormattedMessage
id="xpack.uptime.sourceConfiguration.errorStateDefaultValue"
defaultMessage="The default value is {defaultValue}"
values={{
defaultValue: (
<EuiCode>{defaultDynamicSettings?.certificatesThresholds?.errorState}</EuiCode>
),
}}
/>
}
isInvalid={!!fieldErrors?.certificatesThresholds?.errorState}
label={
<FormattedMessage
id="xpack.uptime.sourceConfiguration.errorStateLabel"
defaultMessage="Error state"
/>
}
>
<EuiFlexGroup>
<EuiFlexItem grow={2}>
<EuiFieldNumber
data-test-subj={`error-state-threshold-input-${dss.loading ? 'loading' : 'loaded'}`}
fullWidth
disabled={isDisabled}
isLoading={dss.loading}
value={formFields?.certificatesThresholds?.errorState || ''}
onChange={({ currentTarget: { value } }: any) =>
onChange(
'certificatesThresholds.errorState',
value === '' ? undefined : Number(value)
)
}
/>
</EuiFlexItem>
<EuiFlexItem grow={1}>
<EuiSelect options={[{ value: 'day', text: 'Days' }]} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
<EuiFormRow
describedByIds={['warningState']}
error={fieldErrors?.certificatesThresholds?.warningState}
fullWidth
helpText={
<FormattedMessage
id="xpack.uptime.sourceConfiguration.warningStateDefaultValue"
defaultMessage="The default value is {defaultValue}"
values={{
defaultValue: (
<EuiCode>{defaultDynamicSettings?.certificatesThresholds?.warningState}</EuiCode>
),
}}
/>
}
isInvalid={!!fieldErrors?.certificatesThresholds?.warningState}
label={
<FormattedMessage
id="xpack.uptime.sourceConfiguration.warningStateLabel"
defaultMessage="Warning state"
/>
}
>
<EuiFlexGroup>
<EuiFlexItem grow={2}>
<EuiFieldNumber
data-test-subj={`warning-state-threshold-input-${
dss.loading ? 'loading' : 'loaded'
}`}
fullWidth
disabled={isDisabled}
isLoading={dss.loading}
value={formFields?.certificatesThresholds?.warningState || ''}
onChange={(event: any) =>
onChange('certificatesThresholds.warningState', Number(event.currentTarget.value))
}
/>
</EuiFlexItem>
<EuiFlexItem grow={1}>
<EuiSelect options={[{ value: 'day', text: 'Days' }]} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
</EuiDescribedFormGroup>
</>
);
};

View file

@ -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<SettingsFormProps> = ({
onChange,
formFields,
fieldErrors,
isDisabled,
}) => {
const dss = useSelector(selectDynamicSettings);
return (
<>
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.uptime.sourceConfiguration.indicesSectionTitle"
defaultMessage="Indices"
/>
</h3>
</EuiTitle>
<EuiSpacer size="m" />
<EuiDescribedFormGroup
title={
<h4>
<FormattedMessage
id="xpack.uptime.sourceConfiguration.heartbeatIndicesTitle"
defaultMessage="Uptime indices"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.uptime.sourceConfiguration.heartbeatIndicesDescription"
defaultMessage="Index pattern for matching indices that contain Heartbeat data"
/>
}
>
<EuiFormRow
describedByIds={['heartbeatIndices']}
error={fieldErrors?.heartbeatIndices}
fullWidth
helpText={
<FormattedMessage
id="xpack.uptime.sourceConfiguration.heartbeatIndicesDefaultValue"
defaultMessage="The default value is {defaultValue}"
values={{
defaultValue: <EuiCode>{defaultDynamicSettings.heartbeatIndices}</EuiCode>,
}}
/>
}
isInvalid={!!fieldErrors?.heartbeatIndices}
label={
<FormattedMessage
id="xpack.uptime.sourceConfiguration.heartbeatIndicesLabel"
defaultMessage="Heartbeat indices"
/>
}
>
<EuiFieldText
data-test-subj={`heartbeat-indices-input-${dss.loading ? 'loading' : 'loaded'}`}
fullWidth
disabled={isDisabled}
isLoading={dss.loading}
value={formFields?.heartbeatIndices || ''}
onChange={(event: any) => onChange('heartbeatIndices', event.currentTarget.value)}
/>
</EuiFormRow>
</EuiDescribedFormGroup>
</>
);
};

View file

@ -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';
interface Props {
dynamicSettingsState: DynamicSettingsState;
}
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 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<DynamicSettings | null>(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 = ({
<EuiFlexItem grow={false}>
<form onSubmit={onApply}>
<EuiForm>
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.uptime.sourceConfiguration.indicesSectionTitle"
defaultMessage="Indices"
/>
</h3>
</EuiTitle>
<EuiSpacer size="m" />
<EuiDescribedFormGroup
title={
<h4>
<FormattedMessage
id="xpack.uptime.sourceConfiguration.heartbeatIndicesTitle"
defaultMessage="Uptime indices"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.uptime.sourceConfiguration.heartbeatIndicesDescription"
defaultMessage="Index pattern for matching indices that contain Heartbeat data"
/>
}
>
<EuiFormRow
describedByIds={['heartbeatIndices']}
error={fieldErrors?.heartbeatIndices}
fullWidth
helpText={
<FormattedMessage
id="xpack.uptime.sourceConfiguration.heartbeatIndicesDefaultValue"
defaultMessage="The default value is {defaultValue}"
values={{
defaultValue: (
<EuiCode>{defaultDynamicSettings.heartbeatIndices}</EuiCode>
),
}}
/>
}
isInvalid={!!fieldErrors?.heartbeatIndices}
label={
<FormattedMessage
id="xpack.uptime.sourceConfiguration.heartbeatIndicesLabel"
defaultMessage="Heartbeat indices"
/>
}
>
<EuiFieldText
data-test-subj={`heartbeat-indices-input-${
dss.loading ? 'loading' : 'loaded'
}`}
fullWidth
disabled={isFormDisabled}
isLoading={dss.loading}
value={formFields?.heartbeatIndices || ''}
onChange={(event: any) =>
onChangeFormField('heartbeatIndices', event.currentTarget.value)
}
/>
</EuiFormRow>
</EuiDescribedFormGroup>
<IndicesForm
onChange={onChangeFormField}
formFields={formFields}
fieldErrors={fieldErrors}
isDisabled={isFormDisabled}
/>
<CertificateExpirationForm
onChange={onChangeFormField}
formFields={formFields}
fieldErrors={fieldErrors}
isDisabled={isFormDisabled}
/>
<EuiSpacer size="m" />
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
@ -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);

View file

@ -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 [],

View file

@ -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<DynamicSettings>;
setUptimeDynamicSettings: UMSavedObjectsQueryFn<void, DynamicSettings>;
@ -32,6 +28,16 @@ export const umDynamicSettings: SavedObjectsType = {
heartbeatIndices: {
type: 'keyword',
},
certificatesThresholds: {
properties: {
errorState: {
type: 'long',
},
warningState: {
type: 'long',
},
},
},
},
},
};

View file

@ -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')

View file

@ -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);
});
});
};

View file

@ -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'));