mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Synthetics] Add synthetics settings alerting default (#147339)
Co-authored-by: Alejandro Fernández Gómez <antarticonorte@gmail.com> Fixes https://github.com/elastic/kibana/issues/145402
This commit is contained in:
parent
ac6f2fb782
commit
edc8624651
25 changed files with 1040 additions and 12 deletions
|
@ -16,6 +16,8 @@ import {
|
|||
} from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { EmailActionParams } from '../../types';
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
export const EmailParamsFields = ({
|
||||
actionParams,
|
||||
editAction,
|
||||
|
@ -25,6 +27,7 @@ export const EmailParamsFields = ({
|
|||
defaultMessage,
|
||||
isLoading,
|
||||
isDisabled,
|
||||
onBlur = noop,
|
||||
showEmailSubjectAndMessage = true,
|
||||
}: ActionParamsProps<EmailActionParams>) => {
|
||||
const { to, cc, bcc, subject, message } = actionParams;
|
||||
|
@ -114,6 +117,7 @@ export const EmailParamsFields = ({
|
|||
if (!to) {
|
||||
editAction('to', [], index);
|
||||
}
|
||||
onBlur('to');
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
@ -156,6 +160,7 @@ export const EmailParamsFields = ({
|
|||
if (!cc) {
|
||||
editAction('cc', [], index);
|
||||
}
|
||||
onBlur('cc');
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
@ -199,6 +204,7 @@ export const EmailParamsFields = ({
|
|||
if (!bcc) {
|
||||
editAction('bcc', [], index);
|
||||
}
|
||||
onBlur('bcc');
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
|
|
@ -12,4 +12,9 @@ export const DYNAMIC_SETTINGS_DEFAULTS: DynamicSettings = {
|
|||
certAgeThreshold: 730,
|
||||
certExpirationThreshold: 30,
|
||||
defaultConnectors: [],
|
||||
defaultEmail: {
|
||||
to: [],
|
||||
cc: [],
|
||||
bcc: [],
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { journey, step, expect, before, after } from '@elastic/synthetics';
|
||||
import { byTestId } from '@kbn/observability-plugin/e2e/utils';
|
||||
import { syntheticsAppPageProvider } from '../../page_objects/synthetics_app';
|
||||
import { cleanSettings } from './services/settings';
|
||||
|
||||
journey('AlertingDefaults', async ({ page, params }) => {
|
||||
const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl });
|
||||
|
||||
page.setDefaultTimeout(60 * 1000);
|
||||
|
||||
before(async () => {
|
||||
await cleanSettings(params);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await cleanSettings(params);
|
||||
});
|
||||
|
||||
step('Login to kibana', async () => {
|
||||
await page.goto('http://localhost:5620/login?next=%2F');
|
||||
await syntheticsApp.loginToKibana();
|
||||
});
|
||||
|
||||
step('Go to Settings page', async () => {
|
||||
await page.click('[aria-label="Toggle primary navigation"]');
|
||||
await page.click('text=Synthetics');
|
||||
await page.click('text=Settings');
|
||||
});
|
||||
|
||||
step('Click text=Synthetics', async () => {
|
||||
await page.click('text=Synthetics');
|
||||
await page.click('text=Settings');
|
||||
expect(page.url()).toBe('http://localhost:5620/app/synthetics/settings/alerting');
|
||||
await page.click('.euiComboBox__inputWrap');
|
||||
await page.click("text=There aren't any options available");
|
||||
await page.click('button:has-text("Add connector")');
|
||||
await page.click('p:has-text("Slack")');
|
||||
await page.click('input[type="text"]');
|
||||
await page.fill('input[type="text"]', 'Test slack');
|
||||
await page.press('input[type="text"]', 'Tab');
|
||||
});
|
||||
step(
|
||||
'Fill text=Webhook URLCreate a Slack Webhook URL(opens in a new tab or window) >> input[type="text"]',
|
||||
async () => {
|
||||
await page.fill(
|
||||
'text=Webhook URLCreate a Slack Webhook URL(opens in a new tab or window) >> input[type="text"]',
|
||||
'https://www.slack.com'
|
||||
);
|
||||
await page.click('button:has-text("Save")');
|
||||
await page.click('.euiComboBox__inputWrap');
|
||||
await page.click('button[role="option"]:has-text("Test slack")');
|
||||
await page.click("text=You've selected all available options");
|
||||
await page.click('button:has-text("Apply changes")');
|
||||
await page.click('[aria-label="Remove Test slack from selection in this group"]');
|
||||
await page.isDisabled('button:has-text("Discard changes")');
|
||||
await page.click('button:has-text("Add connector")');
|
||||
}
|
||||
);
|
||||
step('Click text=Email', async () => {
|
||||
await page.click('text=Email');
|
||||
await page.click('input[type="text"]');
|
||||
await page.fill('input[type="text"]', 'Test email');
|
||||
await page.press('input[type="text"]', 'Tab');
|
||||
await page.selectOption('select', 'gmail');
|
||||
await page.click('text=UsernamePassword >> input[type="text"]');
|
||||
await page.fill('text=UsernamePassword >> input[type="text"]', 'elastic');
|
||||
await page.press('text=UsernamePassword >> input[type="text"]', 'Tab');
|
||||
await page.fill('input[type="password"]', 'changeme');
|
||||
await page.click('button:has-text("Save")');
|
||||
await page.click(
|
||||
'text=Sender is required.Configure email accounts(opens in a new tab or window) >> input[type="text"]'
|
||||
);
|
||||
await page.fill(
|
||||
'text=Sender is required.Configure email accounts(opens in a new tab or window) >> input[type="text"]',
|
||||
'test@gmail.com'
|
||||
);
|
||||
await page.click('button:has-text("Save")');
|
||||
});
|
||||
step('Click .euiComboBox__inputWrap', async () => {
|
||||
await page.click('.euiComboBox__inputWrap');
|
||||
await page.click('button[role="option"]:has-text("Test email")');
|
||||
await page.click(byTestId('toEmailAddressInput'));
|
||||
await page.fill(
|
||||
'text=To CcBccCombo box. Selected. Combo box input. Type some text or, to display a li >> input[role="combobox"]',
|
||||
'test@gmail.com'
|
||||
);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.fill(
|
||||
'text=test@gmail.comCombo box. Selected. test@gmail.com. Press Backspace to delete tes >> input[role="combobox"]',
|
||||
'tesyt'
|
||||
);
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await page.click('[aria-label="Remove tesyt from selection in this group"]');
|
||||
await page.click('button:has-text("Cc")');
|
||||
await page.click(byTestId('ccEmailAddressInput'));
|
||||
|
||||
await page.fill(`${byTestId('ccEmailAddressInput')} >> input[role="combobox"]`, 'wow');
|
||||
await page.keyboard.press('Enter');
|
||||
});
|
||||
step('Click text=wow is not a valid email.', async () => {
|
||||
await page.click('text=wow is not a valid email.');
|
||||
await page.click('text=wowwow is not a valid email. >> [aria-label="Clear input"]');
|
||||
await page.fill(`${byTestId('ccEmailAddressInput')} >> input[role="combobox"]`, 'list');
|
||||
await page.click(
|
||||
'text=Default emailEmail settings required for selected email alert connectors.To Bcct'
|
||||
);
|
||||
await page.click('[aria-label="Remove list from selection in this group"]');
|
||||
await page.click(
|
||||
'text=Default emailEmail settings required for selected email alert connectors.To Bcct'
|
||||
);
|
||||
await page.click('text=To Bcctest@gmail.com >> [aria-label="Clear input"]');
|
||||
await page.click('.euiForm');
|
||||
await page.click('text=To: Email is required for selected email connector');
|
||||
});
|
||||
step(
|
||||
'Click .euiComboBox.euiComboBox--fullWidth.euiComboBox-isInvalid .euiFormControlLayout .euiFormControlLayout__childrenWrapper .euiComboBox__inputWrap',
|
||||
async () => {
|
||||
await page.click(
|
||||
'.euiComboBox.euiComboBox--fullWidth.euiComboBox-isInvalid .euiFormControlLayout .euiFormControlLayout__childrenWrapper .euiComboBox__inputWrap'
|
||||
);
|
||||
await page.fill(
|
||||
'text=To BccCombo box. Selected. Combo box input. Type some text or, to display a list >> input[role="combobox"]',
|
||||
'test@gmail.com'
|
||||
);
|
||||
await page.isDisabled('button:has-text("Apply changes")');
|
||||
await page.click('[aria-label="Account menu"]');
|
||||
await page.click('text=Log out');
|
||||
}
|
||||
);
|
||||
|
||||
step('Login to kibana with readonly', async () => {
|
||||
await syntheticsApp.loginToKibana('viewer', 'changeme');
|
||||
});
|
||||
|
||||
step('Go to http://localhost:5620/app/synthetics/settings/alerting', async () => {
|
||||
await page.goto('http://localhost:5620/app/synthetics/settings/alerting', {
|
||||
waitUntil: 'networkidle',
|
||||
});
|
||||
await page.isDisabled('.euiComboBox__inputWrap');
|
||||
await page.isDisabled('button:has-text("Apply changes")');
|
||||
await page.isDisabled('button:has-text("Add connector")');
|
||||
});
|
||||
});
|
|
@ -14,3 +14,4 @@ export * from './overview_sorting.journey';
|
|||
export * from './overview_scrolling.journey';
|
||||
export * from './overview_search.journey';
|
||||
export * from './private_locations.journey';
|
||||
export * from './alerting_default.journey';
|
||||
|
|
|
@ -77,9 +77,8 @@ export const cleanPrivateLocations = async (params: Record<string, any>) => {
|
|||
const server = getService('kibanaServer');
|
||||
|
||||
try {
|
||||
await server.savedObjects.clean({ types: [privateLocationsSavedObjectName] });
|
||||
await server.savedObjects.clean({
|
||||
types: ['ingest-agent-policies', 'ingest-package-policies'],
|
||||
types: [privateLocationsSavedObjectName, 'ingest-agent-policies', 'ingest-package-policies'],
|
||||
});
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const cleanSettings = async (params: Record<string, any>) => {
|
||||
const getService = params.getService;
|
||||
const server = getService('kibanaServer');
|
||||
|
||||
try {
|
||||
await server.savedObjects.clean({ types: ['uptime-dynamic-settings'] });
|
||||
await cleanConnectors(params);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const cleanConnectors = async (params: Record<string, any>) => {
|
||||
const getService = params.getService;
|
||||
const server = getService('kibanaServer');
|
||||
|
||||
try {
|
||||
const { data } = await server.requester.request({
|
||||
path: '/api/actions/connectors',
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
for (const connector of data) {
|
||||
await server.requester.request({
|
||||
path: `/api/actions/connector/${connector.id}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
import { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { getConnectorsAction } from '../../../state/settings/actions';
|
||||
|
||||
interface Props {
|
||||
focusInput: () => void;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
|
||||
interface KibanaDeps {
|
||||
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
|
||||
}
|
||||
|
||||
export const AddConnectorFlyout = ({ focusInput, isDisabled }: Props) => {
|
||||
const [addFlyoutVisible, setAddFlyoutVisibility] = useState<boolean>(false);
|
||||
const {
|
||||
services: {
|
||||
application,
|
||||
triggersActionsUi: { getAddConnectorFlyout },
|
||||
},
|
||||
} = useKibana<KibanaDeps>();
|
||||
|
||||
const canEdit: boolean = !!application?.capabilities.actions.save;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const ConnectorAddFlyout = useMemo(
|
||||
() =>
|
||||
getAddConnectorFlyout({
|
||||
onClose: () => {
|
||||
dispatch(getConnectorsAction.get());
|
||||
setAddFlyoutVisibility(false);
|
||||
focusInput();
|
||||
},
|
||||
featureId: 'uptime',
|
||||
}),
|
||||
[dispatch, focusInput, getAddConnectorFlyout]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{addFlyoutVisible ? ConnectorAddFlyout : null}
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="createConnectorButton"
|
||||
onClick={() => setAddFlyoutVisibility(true)}
|
||||
size="s"
|
||||
isDisabled={isDisabled || !canEdit}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.alerts.settings.addConnector"
|
||||
defaultMessage="Add connector"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiDescribedFormGroup,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { isEmpty, isEqual } from 'lodash';
|
||||
import { hasInvalidEmail } from './validation';
|
||||
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../common/constants';
|
||||
import { DefaultEmail } from './default_email';
|
||||
import { selectDynamicSettings } from '../../../state/settings/selectors';
|
||||
import {
|
||||
getDynamicSettingsAction,
|
||||
setDynamicSettingsAction,
|
||||
} from '../../../state/settings/actions';
|
||||
import { DefaultConnectorField } from './connector_field';
|
||||
import { DynamicSettings } from '../../../../../../common/runtime_types';
|
||||
import { useAlertingDefaults } from './hooks/use_alerting_defaults';
|
||||
|
||||
interface FormFields extends Omit<DynamicSettings, 'defaultEmail'> {
|
||||
defaultEmail: Partial<DynamicSettings['defaultEmail']>;
|
||||
}
|
||||
|
||||
export const AlertDefaultsForm = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { settings, loading } = useSelector(selectDynamicSettings);
|
||||
|
||||
const [formFields, setFormFields] = useState<FormFields>(DYNAMIC_SETTINGS_DEFAULTS as FormFields);
|
||||
|
||||
const canEdit: boolean =
|
||||
!!useKibana().services?.application?.capabilities.uptime.configureSettings || false;
|
||||
|
||||
const isDisabled = !canEdit;
|
||||
|
||||
useEffect(() => {
|
||||
if (settings) {
|
||||
setFormFields(settings as FormFields);
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getDynamicSettingsAction.get());
|
||||
}, [dispatch]);
|
||||
|
||||
const { connectors } = useAlertingDefaults();
|
||||
|
||||
const hasEmailConnector = connectors?.find(
|
||||
(connector) =>
|
||||
formFields.defaultConnectors?.includes(connector.id) && connector.actionTypeId === '.email'
|
||||
);
|
||||
|
||||
const onApply = () => {
|
||||
dispatch(setDynamicSettingsAction.get(formFields as DynamicSettings));
|
||||
};
|
||||
|
||||
const isFormDirty = !isEqual(formFields, settings);
|
||||
|
||||
const isFormValid = () => {
|
||||
if (hasEmailConnector) {
|
||||
return isEmpty(hasInvalidEmail(formFields?.defaultConnectors, formFields?.defaultEmail));
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiForm>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.settings.defaultConnectors"
|
||||
defaultMessage="Default Connectors"
|
||||
/>
|
||||
</h4>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.settings.defaultConnectors.description"
|
||||
defaultMessage="Selector one or more connectors to be used for alerts. These settings will be applied to all synthetics based alerts."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<DefaultConnectorField
|
||||
isDisabled={isDisabled}
|
||||
isLoading={loading}
|
||||
selectedConnectors={formFields.defaultConnectors}
|
||||
onChange={(value) => setFormFields({ ...formFields, defaultConnectors: value })}
|
||||
/>
|
||||
</EuiDescribedFormGroup>
|
||||
{hasEmailConnector && (
|
||||
<DefaultEmail
|
||||
loading={loading}
|
||||
isDisabled={isDisabled}
|
||||
value={formFields.defaultEmail}
|
||||
selectedConnectors={formFields.defaultConnectors}
|
||||
onChange={(value) => setFormFields((prevStat) => ({ ...prevStat, defaultEmail: value }))}
|
||||
/>
|
||||
)}
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="cross"
|
||||
onClick={() => {
|
||||
setFormFields((settings ?? DYNAMIC_SETTINGS_DEFAULTS) as FormFields);
|
||||
}}
|
||||
flush="left"
|
||||
isDisabled={!isFormDirty}
|
||||
isLoading={loading}
|
||||
>
|
||||
{DISCARD_CHANGES}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
onClick={(evt: React.FormEvent) => {
|
||||
evt.preventDefault();
|
||||
onApply();
|
||||
}}
|
||||
fill
|
||||
isLoading={loading}
|
||||
isDisabled={!isFormDirty || isDisabled || !isFormValid()}
|
||||
>
|
||||
{APPLY_CHANGES}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiForm>
|
||||
);
|
||||
};
|
||||
|
||||
const DISCARD_CHANGES = i18n.translate('xpack.synthetics.settings.discardChanges', {
|
||||
defaultMessage: 'Discard changes',
|
||||
});
|
||||
|
||||
const APPLY_CHANGES = i18n.translate('xpack.synthetics.settings.applyChanges', {
|
||||
defaultMessage: 'Apply changes',
|
||||
});
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow, EuiIcon } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import styled from 'styled-components';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useGetUrlParams, useUrlParams } from '../../../hooks';
|
||||
import { useAlertingDefaults } from './hooks/use_alerting_defaults';
|
||||
import { alertFormI18n } from './translations';
|
||||
import { ClientPluginsStart } from '../../../../../plugin';
|
||||
import { AddConnectorFlyout } from './add_connector_flyout';
|
||||
|
||||
type ConnectorOption = EuiComboBoxOptionOption<string>;
|
||||
|
||||
export function DefaultConnectorField({
|
||||
isLoading,
|
||||
isDisabled,
|
||||
onChange,
|
||||
selectedConnectors,
|
||||
}: {
|
||||
isLoading: boolean;
|
||||
isDisabled: boolean;
|
||||
selectedConnectors: string[];
|
||||
onChange: (connectors: string[]) => void;
|
||||
}) {
|
||||
const { actionTypeRegistry } = useKibana<ClientPluginsStart>().services.triggersActionsUi;
|
||||
|
||||
const { options, connectors } = useAlertingDefaults();
|
||||
|
||||
const renderOption = (option: ConnectorOption) => {
|
||||
const { label, value } = option;
|
||||
|
||||
const { actionTypeId: type } = connectors?.find((dt) => dt.id === value) ?? {};
|
||||
return (
|
||||
<ConnectorSpan>
|
||||
<EuiIcon type={actionTypeRegistry.get(type as string).iconClass} />
|
||||
<span>{label}</span>
|
||||
</ConnectorSpan>
|
||||
);
|
||||
};
|
||||
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const { focusConnectorField } = useGetUrlParams();
|
||||
|
||||
const updateUrlParams = useUrlParams()[1];
|
||||
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (focusConnectorField && inputRef.current && !isLoading) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [focusConnectorField, inputRef, isLoading]);
|
||||
|
||||
const onBlur = () => {
|
||||
if (inputRef.current) {
|
||||
const { value } = inputRef.current;
|
||||
setError(value.length === 0 ? undefined : `"${value}" is not a valid option`);
|
||||
}
|
||||
if (inputRef.current && !isLoading && focusConnectorField) {
|
||||
updateUrlParams({ focusConnectorField: undefined });
|
||||
}
|
||||
};
|
||||
|
||||
const onSearchChange = (value: string, hasMatchingOptions?: boolean) => {
|
||||
setError(
|
||||
value.length === 0 || hasMatchingOptions ? undefined : `"${value}" is not a valid option`
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<RowWrapper
|
||||
describedByIds={['defaultConnectors']}
|
||||
error={error}
|
||||
fullWidth
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.sourceConfiguration.defaultConnectors"
|
||||
defaultMessage="Default connectors"
|
||||
/>
|
||||
}
|
||||
labelAppend={
|
||||
<AddConnectorFlyout
|
||||
isDisabled={isDisabled}
|
||||
focusInput={useCallback(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [])}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiComboBox
|
||||
inputRef={(input) => {
|
||||
inputRef.current = input;
|
||||
}}
|
||||
placeholder={alertFormI18n.inputPlaceHolder}
|
||||
options={options}
|
||||
selectedOptions={options.filter((opt) => selectedConnectors?.includes(opt.value))}
|
||||
onBlur={onBlur}
|
||||
isDisabled={isDisabled}
|
||||
data-test-subj={`default-connectors-input-${isLoading ? 'loading' : 'loaded'}`}
|
||||
renderOption={renderOption}
|
||||
fullWidth
|
||||
aria-label={TAGS_LABEL}
|
||||
isLoading={isLoading}
|
||||
onChange={(newSelectedConnectors) => {
|
||||
onChange(newSelectedConnectors.map((tag) => tag.value as string));
|
||||
}}
|
||||
onSearchChange={onSearchChange}
|
||||
/>
|
||||
</RowWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const RowWrapper = styled(EuiFormRow)`
|
||||
&&& > .euiFormRow__labelWrapper {
|
||||
align-items: baseline;
|
||||
}
|
||||
`;
|
||||
|
||||
const ConnectorSpan = styled.span`
|
||||
.euiIcon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
> img {
|
||||
width: 16px;
|
||||
height: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const TAGS_LABEL = i18n.translate('xpack.synthetics.monitorManagement.paramForm.tagsLabel', {
|
||||
defaultMessage: 'Tags',
|
||||
});
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiDescribedFormGroup } from '@elastic/eui';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { UptimePluginServices } from '../../../../../plugin';
|
||||
import { DefaultEmail as DefaultEmailType } from '../../../../../../common/runtime_types';
|
||||
import { hasInvalidEmail } from './validation';
|
||||
|
||||
export function DefaultEmail({
|
||||
loading,
|
||||
onChange,
|
||||
value,
|
||||
isDisabled,
|
||||
selectedConnectors,
|
||||
}: {
|
||||
onChange: (value: Partial<DefaultEmailType>) => void;
|
||||
value?: Partial<DefaultEmailType>;
|
||||
isDisabled: boolean;
|
||||
loading: boolean;
|
||||
selectedConnectors: string[];
|
||||
}) {
|
||||
const { actionTypeRegistry } = useKibana<UptimePluginServices>().services.triggersActionsUi;
|
||||
|
||||
const emailActionType = actionTypeRegistry.get('.email');
|
||||
const ActionParams = emailActionType.actionParamsFields;
|
||||
|
||||
const [isTouched, setIsTouched] = useState(false);
|
||||
|
||||
const errors = hasInvalidEmail(selectedConnectors, value, isTouched);
|
||||
|
||||
return (
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.sourceConfiguration.alertConnectors.defaultEmail"
|
||||
defaultMessage="Default email"
|
||||
/>
|
||||
</h4>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.sourceConfiguration.defaultConnectors.description.defaultEmail"
|
||||
defaultMessage="Email settings required for selected email alert connectors."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ActionParams
|
||||
isLoading={loading}
|
||||
actionParams={value ?? {}}
|
||||
errors={errors}
|
||||
editAction={(key, val, index) => {
|
||||
if (key !== 'message') {
|
||||
onChange({ ...(value ?? {}), [key]: val });
|
||||
}
|
||||
}}
|
||||
showEmailSubjectAndMessage={false}
|
||||
index={1}
|
||||
isDisabled={isDisabled}
|
||||
onBlur={() => setIsTouched(true)}
|
||||
/>
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useFetcher } from '@kbn/observability-plugin/public';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useEffect } from 'react';
|
||||
import { selectDynamicSettings } from '../../../../state/settings/selectors';
|
||||
import { fetchActionTypes } from '../../../../state/settings/api';
|
||||
import { getConnectorsAction } from '../../../../state/settings/actions';
|
||||
|
||||
export const useAlertingDefaults = () => {
|
||||
const { data: actionTypes } = useFetcher(() => fetchActionTypes(), []);
|
||||
const { connectors } = useSelector(selectDynamicSettings);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getConnectorsAction.get());
|
||||
}, [dispatch]);
|
||||
|
||||
const options = (connectors ?? [])
|
||||
.filter((action) => (actionTypes ?? []).find((type) => type.id === action.actionTypeId))
|
||||
.map((connectorAction) => ({
|
||||
value: connectorAction.id,
|
||||
label: connectorAction.name,
|
||||
'data-test-subj': connectorAction.name,
|
||||
}));
|
||||
|
||||
return {
|
||||
options,
|
||||
actionTypes,
|
||||
connectors,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const alertFormI18n = {
|
||||
inputPlaceHolder: i18n.translate(
|
||||
'xpack.synthetics.sourceConfiguration.alertDefaultForm.selectConnector',
|
||||
{
|
||||
defaultMessage: 'Please select one or more connectors',
|
||||
}
|
||||
),
|
||||
emailPlaceHolder: i18n.translate(
|
||||
'xpack.synthetics.sourceConfiguration.alertDefaultForm.emailConnectorPlaceHolder',
|
||||
{
|
||||
defaultMessage: 'To: Email for email connector',
|
||||
}
|
||||
),
|
||||
};
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DefaultEmail as DefaultEmailType } from '../../../../../../common/runtime_types';
|
||||
|
||||
export const validateEmail = (email: string) => {
|
||||
return email
|
||||
.toLowerCase()
|
||||
.match(
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
);
|
||||
};
|
||||
|
||||
const REQUIRED_EMAIL = i18n.translate('xpack.synthetics.settings.alertDefaultForm.requiredEmail', {
|
||||
defaultMessage: 'To: Email is required for selected email connector',
|
||||
});
|
||||
|
||||
const getInvalidEmailError = (value?: string[]) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const inValidEmail = value.find((val) => !validateEmail(val));
|
||||
|
||||
if (!inValidEmail) {
|
||||
return;
|
||||
}
|
||||
|
||||
return i18n.translate('xpack.synthetics.sourceConfiguration.alertDefaultForm.invalidEmail', {
|
||||
defaultMessage: '{val} is not a valid email.',
|
||||
values: { val: inValidEmail },
|
||||
});
|
||||
};
|
||||
|
||||
export const hasInvalidEmail = (
|
||||
defaultConnectors?: string[],
|
||||
value?: Partial<DefaultEmailType>,
|
||||
isTouched?: boolean
|
||||
): {
|
||||
to?: string;
|
||||
cc?: string;
|
||||
bcc?: string;
|
||||
} => {
|
||||
if (!defaultConnectors || defaultConnectors.length === 0 || isTouched === false) {
|
||||
return {};
|
||||
}
|
||||
if (!value || !value.to) {
|
||||
return { to: REQUIRED_EMAIL };
|
||||
}
|
||||
|
||||
const toError = value.to.length === 0 ? REQUIRED_EMAIL : getInvalidEmailError(value.to);
|
||||
const ccError = getInvalidEmailError(value.cc);
|
||||
const bccError = getInvalidEmailError(value.bcc);
|
||||
|
||||
if (toError || ccError || bccError) {
|
||||
return {
|
||||
to: toError ?? '',
|
||||
cc: ccError ?? '',
|
||||
bcc: bccError ?? '',
|
||||
};
|
||||
}
|
||||
return {};
|
||||
};
|
|
@ -7,8 +7,10 @@
|
|||
|
||||
import React from 'react';
|
||||
import { Redirect, useParams } from 'react-router-dom';
|
||||
import { SettingsTabId } from './page_header';
|
||||
import { EuiPanel } from '@elastic/eui';
|
||||
import { AlertDefaultsForm } from './alerting_defaults/alert_defaults_form';
|
||||
import { ProjectAPIKeys } from './project_api_keys/project_api_keys';
|
||||
import { SettingsTabId } from './page_header';
|
||||
import { DataRetentionTab } from './data_retention';
|
||||
import { useSettingsBreadcrumbs } from './use_settings_breadcrumbs';
|
||||
import { ManagePrivateLocations } from './private_locations/manage_private_locations';
|
||||
|
@ -27,7 +29,11 @@ export const SettingsPage = () => {
|
|||
case 'data-retention':
|
||||
return <DataRetentionTab />;
|
||||
case 'alerting':
|
||||
return <div>TODO: Alerting</div>;
|
||||
return (
|
||||
<EuiPanel hasShadow={false} hasBorder={true}>
|
||||
<AlertDefaultsForm />
|
||||
</EuiPanel>
|
||||
);
|
||||
default:
|
||||
return <Redirect to="/settings/alerting" />;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,11 @@
|
|||
*/
|
||||
|
||||
import { all, fork } from 'redux-saga/effects';
|
||||
import {
|
||||
fetchAlertConnectorsEffect,
|
||||
fetchDynamicSettingsEffect,
|
||||
setDynamicSettingsEffect,
|
||||
} from './settings/effects';
|
||||
import { fetchAgentPoliciesEffect } from './private_locations';
|
||||
import { fetchNetworkEventsEffect } from './network_events/effects';
|
||||
import { fetchSyntheticsMonitorEffect } from './monitor_details';
|
||||
|
@ -31,5 +36,9 @@ export const rootEffect = function* root(): Generator {
|
|||
fork(fetchNetworkEventsEffect),
|
||||
fork(fetchPingStatusesEffect),
|
||||
fork(fetchAgentPoliciesEffect),
|
||||
fork(fetchDynamicSettingsEffect),
|
||||
fork(setDynamicSettingsEffect),
|
||||
fork(fetchAgentPoliciesEffect),
|
||||
fork(fetchAlertConnectorsEffect),
|
||||
]);
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { combineReducers } from '@reduxjs/toolkit';
|
||||
|
||||
import { dynamicSettingsReducer, DynamicSettingsState } from './settings';
|
||||
import { agentPoliciesReducer, AgentPoliciesState } from './private_locations';
|
||||
import { networkEventsReducer, NetworkEventsState } from './network_events';
|
||||
import { monitorDetailsReducer, MonitorDetailsState } from './monitor_details';
|
||||
|
@ -32,6 +33,7 @@ export interface SyntheticsAppState {
|
|||
networkEvents: NetworkEventsState;
|
||||
pingStatus: PingStatusState;
|
||||
agentPolicies: AgentPoliciesState;
|
||||
dynamicSettings: DynamicSettingsState;
|
||||
}
|
||||
|
||||
export const rootReducer = combineReducers<SyntheticsAppState>({
|
||||
|
@ -46,4 +48,5 @@ export const rootReducer = combineReducers<SyntheticsAppState>({
|
|||
networkEvents: networkEventsReducer,
|
||||
pingStatus: pingStatusReducer,
|
||||
agentPolicies: agentPoliciesReducer,
|
||||
dynamicSettings: dynamicSettingsReducer,
|
||||
});
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ActionConnector } from './api';
|
||||
import { DynamicSettings } from '../../../../../common/runtime_types';
|
||||
import { createAsyncAction } from '../utils/actions';
|
||||
|
||||
export const getDynamicSettingsAction = createAsyncAction<void, DynamicSettings>(
|
||||
'GET_DYNAMIC_SETTINGS'
|
||||
);
|
||||
export const setDynamicSettingsAction = createAsyncAction<DynamicSettings, DynamicSettings>(
|
||||
'SET_DYNAMIC_SETTINGS'
|
||||
);
|
||||
export const getConnectorsAction = createAsyncAction<void, ActionConnector[]>('GET CONNECTORS');
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
ActionConnector as RawActionConnector,
|
||||
ActionType,
|
||||
AsApiContract,
|
||||
} from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { apiService } from '../../../../utils/api_service';
|
||||
import {
|
||||
DynamicSettings,
|
||||
DynamicSettingsSaveResponse,
|
||||
DynamicSettingsSaveType,
|
||||
DynamicSettingsType,
|
||||
} from '../../../../../common/runtime_types';
|
||||
import { API_URLS } from '../../../../../common/constants';
|
||||
|
||||
const apiPath = API_URLS.DYNAMIC_SETTINGS;
|
||||
|
||||
interface SaveApiRequest {
|
||||
settings: DynamicSettings;
|
||||
}
|
||||
|
||||
export const getDynamicSettings = async (): Promise<DynamicSettings> => {
|
||||
return await apiService.get(apiPath, undefined, DynamicSettingsType);
|
||||
};
|
||||
|
||||
export const setDynamicSettings = async ({
|
||||
settings,
|
||||
}: SaveApiRequest): Promise<DynamicSettingsSaveResponse> => {
|
||||
return await apiService.post(apiPath, settings, DynamicSettingsSaveType);
|
||||
};
|
||||
|
||||
export type ActionConnector = Omit<RawActionConnector, 'secrets'>;
|
||||
|
||||
export const fetchConnectors = async (): Promise<ActionConnector[]> => {
|
||||
const response = (await apiService.get(API_URLS.RULE_CONNECTORS)) as Array<
|
||||
AsApiContract<ActionConnector>
|
||||
>;
|
||||
return response.map(
|
||||
({
|
||||
connector_type_id: actionTypeId,
|
||||
referenced_by_count: referencedByCount,
|
||||
is_preconfigured: isPreconfigured,
|
||||
is_deprecated: isDeprecated,
|
||||
is_missing_secrets: isMissingSecrets,
|
||||
...res
|
||||
}) => ({
|
||||
...res,
|
||||
actionTypeId,
|
||||
referencedByCount,
|
||||
isDeprecated,
|
||||
isPreconfigured,
|
||||
isMissingSecrets,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const fetchActionTypes = async (): Promise<ActionType[]> => {
|
||||
const response = (await apiService.get(API_URLS.CONNECTOR_TYPES, {
|
||||
feature_id: 'uptime',
|
||||
})) as Array<AsApiContract<ActionType>>;
|
||||
return response.map<ActionType>(
|
||||
({
|
||||
enabled_in_config: enabledInConfig,
|
||||
enabled_in_license: enabledInLicense,
|
||||
minimum_license_required: minimumLicenseRequired,
|
||||
supported_feature_ids: supportedFeatureIds,
|
||||
...res
|
||||
}: AsApiContract<ActionType>) => ({
|
||||
...res,
|
||||
enabledInConfig,
|
||||
enabledInLicense,
|
||||
minimumLicenseRequired,
|
||||
supportedFeatureIds,
|
||||
})
|
||||
);
|
||||
};
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { takeLeading, put, call, takeLatest } from 'redux-saga/effects';
|
||||
import { Action } from 'redux-actions';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DynamicSettings } from '../../../../../common/runtime_types';
|
||||
import { kibanaService } from '../../../../utils/kibana_service';
|
||||
import { getConnectorsAction, getDynamicSettingsAction, setDynamicSettingsAction } from './actions';
|
||||
import { fetchEffectFactory } from '../utils/fetch_effect';
|
||||
import { fetchConnectors, getDynamicSettings, setDynamicSettings } from './api';
|
||||
|
||||
export function* fetchDynamicSettingsEffect() {
|
||||
yield takeLeading(
|
||||
String(getDynamicSettingsAction.get),
|
||||
fetchEffectFactory(
|
||||
getDynamicSettings,
|
||||
getDynamicSettingsAction.success,
|
||||
getDynamicSettingsAction.fail
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function* setDynamicSettingsEffect() {
|
||||
const couldNotSaveSettingsText = i18n.translate('xpack.synthetics.settings.error.couldNotSave', {
|
||||
defaultMessage: 'Could not save settings!',
|
||||
});
|
||||
yield takeLatest(
|
||||
String(setDynamicSettingsAction.get),
|
||||
function* (action: Action<DynamicSettings>) {
|
||||
try {
|
||||
yield call(setDynamicSettings, { settings: action.payload });
|
||||
yield put(setDynamicSettingsAction.success(action.payload));
|
||||
kibanaService.core.notifications.toasts.addSuccess(
|
||||
i18n.translate('xpack.synthetics.settings.saveSuccess', {
|
||||
defaultMessage: 'Settings saved!',
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
kibanaService.core.notifications.toasts.addError(err, {
|
||||
title: couldNotSaveSettingsText,
|
||||
});
|
||||
yield put(setDynamicSettingsAction.fail(err));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function* fetchAlertConnectorsEffect() {
|
||||
yield takeLeading(
|
||||
String(getConnectorsAction.get),
|
||||
fetchEffectFactory(fetchConnectors, getConnectorsAction.success, getConnectorsAction.fail)
|
||||
);
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
import { DynamicSettings } from '../../../../../common/runtime_types';
|
||||
import { IHttpSerializedFetchError } from '..';
|
||||
import { getConnectorsAction, getDynamicSettingsAction, setDynamicSettingsAction } from './actions';
|
||||
import { ActionConnector } from './api';
|
||||
|
||||
export interface DynamicSettingsState {
|
||||
settings?: DynamicSettings;
|
||||
loadError?: IHttpSerializedFetchError;
|
||||
saveError?: IHttpSerializedFetchError;
|
||||
loading: boolean;
|
||||
connectors?: ActionConnector[];
|
||||
connectorsLoading?: boolean;
|
||||
}
|
||||
|
||||
const initialState: DynamicSettingsState = {
|
||||
loading: true,
|
||||
connectors: [],
|
||||
};
|
||||
|
||||
export const dynamicSettingsReducer = createReducer(initialState, (builder) => {
|
||||
builder
|
||||
.addCase(getDynamicSettingsAction.get, (state) => {
|
||||
state.loading = true;
|
||||
})
|
||||
.addCase(getDynamicSettingsAction.success, (state, action) => {
|
||||
state.settings = action.payload;
|
||||
state.loading = false;
|
||||
})
|
||||
.addCase(getDynamicSettingsAction.fail, (state, action) => {
|
||||
state.loadError = action.payload;
|
||||
state.loading = false;
|
||||
})
|
||||
.addCase(setDynamicSettingsAction.get, (state) => {
|
||||
state.loading = true;
|
||||
})
|
||||
.addCase(setDynamicSettingsAction.success, (state, action) => {
|
||||
state.settings = action.payload;
|
||||
state.loading = false;
|
||||
})
|
||||
.addCase(setDynamicSettingsAction.fail, (state, action) => {
|
||||
state.loadError = action.payload;
|
||||
state.loading = false;
|
||||
})
|
||||
.addCase(getConnectorsAction.get, (state) => {
|
||||
state.connectorsLoading = true;
|
||||
})
|
||||
.addCase(getConnectorsAction.success, (state, action) => {
|
||||
state.connectors = action.payload;
|
||||
state.connectorsLoading = false;
|
||||
})
|
||||
.addCase(getConnectorsAction.fail, (state, action) => {
|
||||
state.connectorsLoading = false;
|
||||
});
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SyntheticsAppState } from '../root_reducer';
|
||||
|
||||
export const selectDynamicSettings = (state: SyntheticsAppState) => state.dynamicSettings;
|
|
@ -116,6 +116,9 @@ export const mockState: SyntheticsAppState = {
|
|||
error: null,
|
||||
data: null,
|
||||
},
|
||||
dynamicSettings: {
|
||||
loading: false,
|
||||
},
|
||||
};
|
||||
|
||||
function getBrowserJourneyMockSlice() {
|
||||
|
|
|
@ -5,17 +5,17 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { FieldValues, useForm, UseFormProps } from 'react-hook-form';
|
||||
|
||||
export function useFormWrapped<TFieldValues extends FieldValues = FieldValues, TContext = any>(
|
||||
props?: UseFormProps<TFieldValues, TContext>
|
||||
) {
|
||||
const { register, ...restOfForm } = useForm<TFieldValues>(props);
|
||||
const form = useForm<TFieldValues>(props);
|
||||
|
||||
const euiRegister = useCallback(
|
||||
(name, ...registerArgs) => {
|
||||
const { ref, ...restOfRegister } = register(name, ...registerArgs);
|
||||
const { ref, ...restOfRegister } = form.register(name, ...registerArgs);
|
||||
|
||||
return {
|
||||
inputRef: ref,
|
||||
|
@ -23,11 +23,12 @@ export function useFormWrapped<TFieldValues extends FieldValues = FieldValues, T
|
|||
...restOfRegister,
|
||||
};
|
||||
},
|
||||
[register]
|
||||
[form]
|
||||
);
|
||||
const formState = form.formState;
|
||||
|
||||
return {
|
||||
register: euiRegister,
|
||||
...restOfForm,
|
||||
};
|
||||
return useMemo(
|
||||
() => ({ ...form, register: euiRegister, formState }),
|
||||
[euiRegister, form, formState]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -210,6 +210,7 @@ export interface ActionParamsProps<TParams> {
|
|||
isDisabled?: boolean;
|
||||
showEmailSubjectAndMessage?: boolean;
|
||||
executionMode?: ActionConnectorMode;
|
||||
onBlur?: (field?: string) => void;
|
||||
}
|
||||
|
||||
export interface Pagination {
|
||||
|
|
|
@ -45,6 +45,7 @@ export function UptimeSettingsProvider({ getService }: FtrProviderContext) {
|
|||
certAgeThreshold: parseInt(age, 10),
|
||||
certExpirationThreshold: parseInt(expiration, 10),
|
||||
defaultConnectors: [],
|
||||
defaultEmail: { to: [], cc: [], bcc: [] },
|
||||
};
|
||||
},
|
||||
applyButtonIsDisabled: async () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue