Added support for docLinks plugin in Connectors forms and missing save capabilities for modal dialog (#64986) (#65289)

* Added support for docLinks plugin in Connectors forms and missing save capabilities for modal dialog

* Fixed tests

* Extended alert context with application capabilities

* Fixed due to comments

* Fixed typecheck

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
# Conflicts:
#	x-pack/plugins/infra/public/components/alerting/inventory/alert_flyout.tsx
#	x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx
This commit is contained in:
Yuliia Naumenko 2020-05-05 11:20:26 -07:00 committed by GitHub
parent 7cb65c5548
commit 55de3186f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 397 additions and 141 deletions

View file

@ -27,6 +27,7 @@ import {
IUiSettingsClient,
DocLinksStart,
ToastsSetup,
ApplicationStart,
} from '../../../src/core/public';
import { DataPublicPluginStart } from '../../../src/plugins/data/public';
import { ChartsPluginStart } from '../../../src/plugins/charts/public';
@ -48,6 +49,7 @@ export interface AlertingExampleComponentParams {
uiSettings: IUiSettingsClient;
docLinks: DocLinksStart;
toastNotifications: ToastsSetup;
capabilities: ApplicationStart['capabilities'];
}
const AlertingExampleApp = (deps: AlertingExampleComponentParams) => {
@ -102,6 +104,7 @@ export const renderApp = (
http={http}
uiSettings={uiSettings}
docLinks={docLinks}
capabilities={application.capabilities}
{...deps}
/>,
element

View file

@ -36,6 +36,7 @@ export const CreateAlert = ({
docLinks,
data,
toastNotifications,
capabilities,
}: AlertingExampleComponentParams) => {
const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState<boolean>(false);
@ -60,6 +61,7 @@ export const CreateAlert = ({
docLinks,
charts,
dataFieldsFormats: data.fieldFormats,
capabilities,
}}
>
<AlertAdd

View file

@ -74,6 +74,7 @@ const ApmAppRoot = ({
value={{
http: core.http,
docLinks: core.docLinks,
capabilities: core.application.capabilities,
toastNotifications: core.notifications.toasts,
actionTypeRegistry: plugins.triggers_actions_ui.actionTypeRegistry,
alertTypeRegistry: plugins.triggers_actions_ui.alertTypeRegistry

View file

@ -36,6 +36,7 @@ export const AlertFlyout = (props: Props) => {
toastNotifications: services.notifications?.toasts,
http: services.http,
docLinks: services.docLinks,
capabilities: services.application.capabilities,
actionTypeRegistry: triggersActionsUI.actionTypeRegistry,
alertTypeRegistry: triggersActionsUI.alertTypeRegistry,
}}

View file

@ -30,6 +30,7 @@ export const AlertFlyout = (props: Props) => {
toastNotifications: services.notifications?.toasts,
http: services.http,
docLinks: services.docLinks,
capabilities: services.application.capabilities,
actionTypeRegistry: triggersActionsUI.actionTypeRegistry,
alertTypeRegistry: triggersActionsUI.alertTypeRegistry,
}}

View file

@ -64,7 +64,7 @@ interface ConfigureCasesComponentProps {
const ConfigureCasesComponent: React.FC<ConfigureCasesComponentProps> = ({ userCanCrud }) => {
const search = useGetUrlSearch(navTabs.case);
const { http, triggers_actions_ui, notifications, application } = useKibana().services;
const { http, triggers_actions_ui, notifications, application, docLinks } = useKibana().services;
const [connectorIsValid, setConnectorIsValid] = useState(true);
const [addFlyoutVisible, setAddFlyoutVisibility] = useState<boolean>(false);
@ -291,6 +291,7 @@ const ConfigureCasesComponent: React.FC<ConfigureCasesComponentProps> = ({ userC
toastNotifications: notifications.toasts,
capabilities: application.capabilities,
reloadConnectors,
docLinks,
}}
>
<ConnectorAddFlyout

View file

@ -19,6 +19,15 @@ describe('RuleActionsField', () => {
triggers_actions_ui: {
actionTypeRegistry: {},
},
application: {
capabilities: {
actions: {
delete: true,
save: true,
show: true,
},
},
},
},
});
const Component = () => {

View file

@ -27,6 +27,8 @@ export const RuleActionsField: ThrottleSelectField = ({ field, messageVariables
http,
triggers_actions_ui: { actionTypeRegistry },
notifications,
docLinks,
application: { capabilities },
} = useKibana().services;
const setActionIdByIndex = useCallback(
@ -78,6 +80,8 @@ export const RuleActionsField: ThrottleSelectField = ({ field, messageVariables
actionTypes={supportedActionTypes}
defaultActionMessage={DEFAULT_ACTION_MESSAGE}
toastNotifications={notifications.toasts}
docLinks={docLinks}
capabilities={capabilities}
/>
);
};

View file

@ -1374,7 +1374,7 @@ import { ActionsConnectorsContextProvider, ConnectorAddFlyout } from '../../../.
const [addFlyoutVisible, setAddFlyoutVisibility] = useState<boolean>(false);
// load required dependancied
const { http, triggers_actions_ui, toastNotifications, capabilities } = useKibana().services;
const { http, triggers_actions_ui, toastNotifications, capabilities, docLinks } = useKibana().services;
const connector = {
secrets: {},
@ -1406,6 +1406,7 @@ const connector = {
toastNotifications: toastNotifications,
actionTypeRegistry: triggers_actions_ui.actionTypeRegistry,
capabilities: capabilities,
docLinks,
}}
>
<ConnectorAddFlyout
@ -1447,6 +1448,7 @@ export interface ActionsConnectorsContextValue {
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
capabilities: ApplicationStart['capabilities'];
docLinks: DocLinksStart;
reloadConnectors?: () => Promise<void>;
}
```

View file

@ -11,6 +11,7 @@ import { registerBuiltInActionTypes } from './index';
import { ActionTypeModel, ActionParamsProps } from '../../../types';
import { IndexActionParams, EsIndexActionConnector } from './types';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context';
jest.mock('../../../common/index_controls', () => ({
firstFieldOption: jest.fn(),
getFields: jest.fn(),
@ -20,14 +21,35 @@ jest.mock('../../../common/index_controls', () => ({
const ACTION_TYPE_ID = '.index';
let actionTypeModel: ActionTypeModel;
let deps: any;
beforeAll(() => {
beforeAll(async () => {
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
registerBuiltInActionTypes({ actionTypeRegistry });
const getResult = actionTypeRegistry.get(ACTION_TYPE_ID);
if (getResult !== null) {
actionTypeModel = getResult;
}
const mocks = coreMock.createSetup();
const [
{
application: { capabilities },
},
] = await mocks.getStartServices();
deps = {
toastNotifications: mocks.notifications.toasts,
http: mocks.http,
capabilities: {
...capabilities,
actions: {
delete: true,
save: true,
show: true,
},
},
actionTypeRegistry: actionTypeRegistry as any,
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
};
});
describe('actionTypeRegistry.get() works', () => {
@ -99,8 +121,6 @@ describe('action params validation', () => {
describe('IndexActionConnectorFields renders', () => {
test('all connector fields is rendered', async () => {
const mocks = coreMock.createSetup();
expect(actionTypeModel.actionConnectorFields).not.toBeNull();
if (!actionTypeModel.actionConnectorFields) {
return;
@ -145,13 +165,25 @@ describe('IndexActionConnectorFields renders', () => {
},
} as EsIndexActionConnector;
const wrapper = mountWithIntl(
<ConnectorFields
action={actionConnector}
errors={{ index: [] }}
editActionConfig={() => {}}
editActionSecrets={() => {}}
http={mocks.http}
/>
<ActionsConnectorsContextProvider
value={{
http: deps!.http,
actionTypeRegistry: deps!.actionTypeRegistry,
capabilities: deps!.capabilities,
toastNotifications: deps!.toastNotifications,
reloadConnectors: () => {
return new Promise<void>(() => {});
},
docLinks: deps!.docLinks,
}}
>
<ConnectorFields
action={actionConnector}
errors={{ index: [] }}
editActionConfig={() => {}}
editActionSecrets={() => {}}
/>
</ActionsConnectorsContextProvider>
);
await act(async () => {

View file

@ -33,6 +33,7 @@ import {
getIndexPatterns,
} from '../../../common/index_controls';
import { AddMessageVariables } from '../add_message_variables';
import { useActionsConnectorsContext } from '../../context/actions_connectors_context';
export function getActionType(): ActionTypeModel {
return {
@ -78,7 +79,8 @@ export function getActionType(): ActionTypeModel {
const IndexActionConnectorFields: React.FunctionComponent<ActionConnectorFieldsProps<
EsIndexActionConnector
>> = ({ action, editActionConfig, errors, http }) => {
>> = ({ action, editActionConfig, errors }) => {
const { http } = useActionsConnectorsContext();
const { index, refresh, executionTimeField } = action.config;
const [hasTimeFieldCheckbox, setTimeFieldCheckboxState] = useState<boolean>(
executionTimeField != null

View file

@ -4,7 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FunctionComponent } from 'react';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
import { act } from 'react-dom/test-utils';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { TypeRegistry } from '../../type_registry';
import { registerBuiltInActionTypes } from './index';
import { ActionTypeModel, ActionParamsProps } from '../../../types';
@ -14,17 +16,39 @@ import {
SeverityActionOptions,
PagerDutyActionConnector,
} from './types';
import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context';
const ACTION_TYPE_ID = '.pagerduty';
let actionTypeModel: ActionTypeModel;
let deps: any;
beforeAll(() => {
beforeAll(async () => {
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
registerBuiltInActionTypes({ actionTypeRegistry });
const getResult = actionTypeRegistry.get(ACTION_TYPE_ID);
if (getResult !== null) {
actionTypeModel = getResult;
}
const mocks = coreMock.createSetup();
const [
{
application: { capabilities },
},
] = await mocks.getStartServices();
deps = {
toastNotifications: mocks.notifications.toasts,
http: mocks.http,
capabilities: {
...capabilities,
actions: {
delete: true,
save: true,
show: true,
},
},
actionTypeRegistry: actionTypeRegistry as any,
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
};
});
describe('actionTypeRegistry.get() works', () => {
@ -106,7 +130,7 @@ describe('pagerduty action params validation', () => {
});
describe('PagerDutyActionConnectorFields renders', () => {
test('all connector fields is rendered', () => {
test('all connector fields is rendered', async () => {
expect(actionTypeModel.actionConnectorFields).not.toBeNull();
if (!actionTypeModel.actionConnectorFields) {
return;
@ -124,13 +148,31 @@ describe('PagerDutyActionConnectorFields renders', () => {
},
} as PagerDutyActionConnector;
const wrapper = mountWithIntl(
<ConnectorFields
action={actionConnector}
errors={{ routingKey: [] }}
editActionConfig={() => {}}
editActionSecrets={() => {}}
/>
<ActionsConnectorsContextProvider
value={{
http: deps!.http,
actionTypeRegistry: deps!.actionTypeRegistry,
capabilities: deps!.capabilities,
toastNotifications: deps!.toastNotifications,
reloadConnectors: () => {
return new Promise<void>(() => {});
},
docLinks: deps!.docLinks,
}}
>
<ConnectorFields
action={actionConnector}
errors={{ index: [], routingKey: [] }}
editActionConfig={() => {}}
editActionSecrets={() => {}}
/>
</ActionsConnectorsContextProvider>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('[data-test-subj="pagerdutyApiUrlInput"]').length > 0).toBeTruthy();
expect(
wrapper

View file

@ -25,6 +25,7 @@ import { PagerDutyActionParams, PagerDutyActionConnector } from './types';
import pagerDutySvg from './pagerduty.svg';
import { AddMessageVariables } from '../add_message_variables';
import { hasMustacheTokens } from '../../lib/has_mustache_tokens';
import { useActionsConnectorsContext } from '../../context/actions_connectors_context';
export function getActionType(): ActionTypeModel {
return {
@ -105,6 +106,7 @@ export function getActionType(): ActionTypeModel {
const PagerDutyActionConnectorFields: React.FunctionComponent<ActionConnectorFieldsProps<
PagerDutyActionConnector
>> = ({ errors, action, editActionConfig, editActionSecrets }) => {
const { docLinks } = useActionsConnectorsContext();
const { apiUrl } = action.config;
const { routingKey } = action.secrets;
return (
@ -139,7 +141,7 @@ const PagerDutyActionConnectorFields: React.FunctionComponent<ActionConnectorFie
fullWidth
helpText={
<EuiLink
href="https://www.elastic.co/guide/en/kibana/current/pagerduty-action-type.html"
href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/pagerduty-action-type.html`}
target="_blank"
>
<FormattedMessage

View file

@ -4,22 +4,47 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FunctionComponent } from 'react';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
import { act } from 'react-dom/test-utils';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { TypeRegistry } from '../../type_registry';
import { registerBuiltInActionTypes } from './index';
import { ActionTypeModel, ActionParamsProps } from '../../../types';
import { SlackActionParams, SlackActionConnector } from './types';
import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context';
const ACTION_TYPE_ID = '.slack';
let actionTypeModel: ActionTypeModel;
beforeAll(() => {
let deps: any;
beforeAll(async () => {
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
registerBuiltInActionTypes({ actionTypeRegistry });
const getResult = actionTypeRegistry.get(ACTION_TYPE_ID);
if (getResult !== null) {
actionTypeModel = getResult;
}
const mocks = coreMock.createSetup();
const [
{
application: { capabilities },
},
] = await mocks.getStartServices();
deps = {
toastNotifications: mocks.notifications.toasts,
http: mocks.http,
capabilities: {
...capabilities,
actions: {
delete: true,
save: true,
show: true,
},
},
actionTypeRegistry: actionTypeRegistry as any,
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
};
});
describe('actionTypeRegistry.get() works', () => {
@ -78,7 +103,7 @@ describe('slack action params validation', () => {
});
describe('SlackActionFields renders', () => {
test('all connector fields is rendered', () => {
test('all connector fields is rendered', async () => {
expect(actionTypeModel.actionConnectorFields).not.toBeNull();
if (!actionTypeModel.actionConnectorFields) {
return;
@ -94,13 +119,31 @@ describe('SlackActionFields renders', () => {
config: {},
} as SlackActionConnector;
const wrapper = mountWithIntl(
<ConnectorFields
action={actionConnector}
errors={{ webhookUrl: [] }}
editActionConfig={() => {}}
editActionSecrets={() => {}}
/>
<ActionsConnectorsContextProvider
value={{
http: deps!.http,
actionTypeRegistry: deps!.actionTypeRegistry,
capabilities: deps!.capabilities,
toastNotifications: deps!.toastNotifications,
reloadConnectors: () => {
return new Promise<void>(() => {});
},
docLinks: deps!.docLinks,
}}
>
<ConnectorFields
action={actionConnector}
errors={{ index: [], webhookUrl: [] }}
editActionConfig={() => {}}
editActionSecrets={() => {}}
/>
</ActionsConnectorsContextProvider>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('[data-test-subj="slackWebhookUrlInput"]').length > 0).toBeTruthy();
expect(
wrapper

View file

@ -15,6 +15,7 @@ import {
} from '../../../types';
import { SlackActionParams, SlackActionConnector } from './types';
import { AddMessageVariables } from '../add_message_variables';
import { useActionsConnectorsContext } from '../../context/actions_connectors_context';
export function getActionType(): ActionTypeModel {
return {
@ -76,6 +77,7 @@ export function getActionType(): ActionTypeModel {
const SlackActionFields: React.FunctionComponent<ActionConnectorFieldsProps<
SlackActionConnector
>> = ({ action, editActionSecrets, errors }) => {
const { docLinks } = useActionsConnectorsContext();
const { webhookUrl } = action.secrets;
return (
@ -85,7 +87,7 @@ const SlackActionFields: React.FunctionComponent<ActionConnectorFieldsProps<
fullWidth
helpText={
<EuiLink
href="https://www.elastic.co/guide/en/elasticsearch/reference/current/actions-slack.html#configuring-slack"
href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/slack-action-type.html`}
target="_blank"
>
<FormattedMessage

View file

@ -5,7 +5,7 @@
*/
import React, { createContext, useContext } from 'react';
import { HttpSetup, ToastsApi, ApplicationStart } from 'kibana/public';
import { HttpSetup, ToastsApi, ApplicationStart, DocLinksStart } from 'kibana/public';
import { ActionTypeModel } from '../../types';
import { TypeRegistry } from '../type_registry';
@ -18,6 +18,7 @@ export interface ActionsConnectorsContextValue {
>;
capabilities: ApplicationStart['capabilities'];
reloadConnectors?: () => Promise<void>;
docLinks: DocLinksStart;
}
const ActionsConnectorsContext = createContext<ActionsConnectorsContextValue>(null as any);

View file

@ -5,7 +5,13 @@
*/
import React, { useContext, createContext } from 'react';
import { HttpSetup, IUiSettingsClient, ToastsApi, DocLinksStart } from 'kibana/public';
import {
HttpSetup,
IUiSettingsClient,
ToastsApi,
DocLinksStart,
ApplicationStart,
} from 'kibana/public';
import { ChartsPluginSetup } from 'src/plugins/charts/public';
import { FieldFormatsRegistry } from 'src/plugins/data/common/field_formats';
import { TypeRegistry } from '../type_registry';
@ -24,6 +30,7 @@ export interface AlertsContextValue<MetaData = Record<string, any>> {
charts?: ChartsPluginSetup;
dataFieldsFormats?: Pick<FieldFormatsRegistry, 'register'>;
docLinks: DocLinksStart;
capabilities: ApplicationStart['capabilities'];
metadata?: MetaData;
}

View file

@ -9,11 +9,11 @@ import { coreMock } from '../../../../../../../src/core/public/mocks';
import { actionTypeRegistryMock } from '../../action_type_registry.mock';
import { ValidationResult, ActionConnector } from '../../../types';
import { ActionConnectorForm } from './action_connector_form';
import { ActionsConnectorsContextValue } from '../../context/actions_connectors_context';
import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context';
const actionTypeRegistry = actionTypeRegistryMock.create();
describe('action_connector_form', () => {
let deps: ActionsConnectorsContextValue;
let deps: any;
beforeAll(async () => {
const mocks = coreMock.createSetup();
const [
@ -33,6 +33,7 @@ describe('action_connector_form', () => {
},
},
actionTypeRegistry: actionTypeRegistry as any,
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
};
});
@ -62,14 +63,25 @@ describe('action_connector_form', () => {
let wrapper;
if (deps) {
wrapper = mountWithIntl(
<ActionConnectorForm
actionTypeName={'my-action-type-name'}
connector={initialConnector}
dispatch={() => {}}
errors={{ name: [] }}
actionTypeRegistry={deps.actionTypeRegistry}
http={deps.http}
/>
<ActionsConnectorsContextProvider
value={{
http: deps!.http,
actionTypeRegistry: deps!.actionTypeRegistry,
capabilities: deps!.capabilities,
toastNotifications: deps!.toastNotifications,
reloadConnectors: () => {
return new Promise<void>(() => {});
},
docLinks: deps!.docLinks,
}}
>
<ActionConnectorForm
actionTypeName={'my-action-type-name'}
connector={initialConnector}
dispatch={() => {}}
errors={{ name: [] }}
/>
</ActionsConnectorsContextProvider>
);
}
const connectorNameField = wrapper?.find('[data-test-subj="nameInput"]');

View file

@ -15,10 +15,9 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { HttpSetup } from 'kibana/public';
import { ReducerAction } from './connector_reducer';
import { ActionConnector, IErrorObject, ActionTypeModel } from '../../../types';
import { TypeRegistry } from '../../type_registry';
import { ActionConnector, IErrorObject } from '../../../types';
import { useActionsConnectorsContext } from '../../context/actions_connectors_context';
export function validateBaseProperties(actionObject: ActionConnector) {
const validationResult = { errors: {} };
@ -47,8 +46,6 @@ interface ActionConnectorProps {
body: { message: string; error: string };
};
errors: IErrorObject;
actionTypeRegistry: TypeRegistry<ActionTypeModel>;
http: HttpSetup;
}
export const ActionConnectorForm = ({
@ -57,9 +54,8 @@ export const ActionConnectorForm = ({
actionTypeName,
serverError,
errors,
actionTypeRegistry,
http,
}: ActionConnectorProps) => {
const { actionTypeRegistry, docLinks } = useActionsConnectorsContext();
const setActionProperty = (key: string, value: any) => {
dispatch({ command: { type: 'setProperty' }, payload: { key, value } });
};
@ -94,7 +90,10 @@ export const ActionConnectorForm = ({
values={{
actionType: actionTypeName,
docLink: (
<EuiLink target="_blank">
<EuiLink
href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/action-types.html`}
target="_blank"
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.actionConnectorForm.actions.actionConfigurationWarningHelpLinkText"
defaultMessage="Learn more."
@ -151,7 +150,6 @@ export const ActionConnectorForm = ({
errors={errors}
editActionConfig={setActionConfigProperty}
editActionSecrets={setActionSecretsProperty}
http={http}
/>
) : null}
</EuiForm>

View file

@ -127,11 +127,25 @@ describe('action_form', () => {
isPreconfigured: false,
},
]);
const mockes = coreMock.createSetup();
const mocks = coreMock.createSetup();
const [
{
application: { capabilities },
},
] = await mocks.getStartServices();
deps = {
toastNotifications: mockes.notifications.toasts,
http: mockes.http,
toastNotifications: mocks.notifications.toasts,
http: mocks.http,
capabilities: {
...capabilities,
actions: {
delete: true,
save: true,
show: true,
},
},
actionTypeRegistry: actionTypeRegistry as any,
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
};
actionTypeRegistry.list.mockReturnValue([
actionType,
@ -224,6 +238,8 @@ describe('action_form', () => {
},
]}
toastNotifications={deps!.toastNotifications}
docLinks={deps.docLinks}
capabilities={deps.capabilities}
/>
);

View file

@ -28,7 +28,7 @@ import {
EuiHorizontalRule,
EuiText,
} from '@elastic/eui';
import { HttpSetup, ToastsApi } from 'kibana/public';
import { HttpSetup, ToastsApi, ApplicationStart, DocLinksStart } from 'kibana/public';
import { loadActionTypes, loadAllActions as loadConnectors } from '../../lib/action_connector_api';
import {
IErrorObject,
@ -57,10 +57,12 @@ interface ActionAccordionFormProps {
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
docLinks: DocLinksStart;
actionTypes?: ActionType[];
messageVariables?: string[];
defaultActionMessage?: string;
setHasActionsDisabled?: (value: boolean) => void;
capabilities: ApplicationStart['capabilities'];
}
interface ActiveActionConnectorState {
@ -81,6 +83,8 @@ export const ActionForm = ({
defaultActionMessage,
toastNotifications,
setHasActionsDisabled,
capabilities,
docLinks,
}: ActionAccordionFormProps) => {
const [addModalVisible, setAddModalVisibility] = useState<boolean>(false);
const [activeActionItem, setActiveActionItem] = useState<ActiveActionConnectorState | undefined>(
@ -685,6 +689,8 @@ export const ActionForm = ({
actionTypeRegistry={actionTypeRegistry}
http={http}
toastNotifications={toastNotifications}
docLinks={docLinks}
capabilities={capabilities}
/>
) : null}
</Fragment>

View file

@ -6,17 +6,14 @@
import * as React from 'react';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import {
ActionsConnectorsContextProvider,
ActionsConnectorsContextValue,
} from '../../context/actions_connectors_context';
import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context';
import { actionTypeRegistryMock } from '../../action_type_registry.mock';
import { ActionTypeMenu } from './action_type_menu';
import { ValidationResult } from '../../../types';
const actionTypeRegistry = actionTypeRegistryMock.create();
describe('connector_add_flyout', () => {
let deps: ActionsConnectorsContextValue;
let deps: any;
beforeAll(async () => {
const mockes = coreMock.createSetup();
@ -37,6 +34,7 @@ describe('connector_add_flyout', () => {
},
},
actionTypeRegistry: actionTypeRegistry as any,
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
};
});
@ -68,6 +66,7 @@ describe('connector_add_flyout', () => {
reloadConnectors: () => {
return new Promise<void>(() => {});
},
docLinks: deps!.docLinks,
}}
>
<ActionTypeMenu
@ -117,6 +116,7 @@ describe('connector_add_flyout', () => {
reloadConnectors: () => {
return new Promise<void>(() => {});
},
docLinks: deps!.docLinks,
}}
>
<ActionTypeMenu
@ -166,6 +166,7 @@ describe('connector_add_flyout', () => {
reloadConnectors: () => {
return new Promise<void>(() => {});
},
docLinks: deps!.docLinks,
}}
>
<ActionTypeMenu

View file

@ -7,17 +7,14 @@ import * as React from 'react';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { ConnectorAddFlyout } from './connector_add_flyout';
import {
ActionsConnectorsContextProvider,
ActionsConnectorsContextValue,
} from '../../context/actions_connectors_context';
import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context';
import { actionTypeRegistryMock } from '../../action_type_registry.mock';
import { ValidationResult } from '../../../types';
const actionTypeRegistry = actionTypeRegistryMock.create();
describe('connector_add_flyout', () => {
let deps: ActionsConnectorsContextValue;
let deps: any;
beforeAll(async () => {
const mocks = coreMock.createSetup();
@ -38,6 +35,7 @@ describe('connector_add_flyout', () => {
},
},
actionTypeRegistry: actionTypeRegistry as any,
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
};
});
@ -56,6 +54,7 @@ describe('connector_add_flyout', () => {
reloadConnectors: () => {
return new Promise<void>(() => {});
},
docLinks: deps!.docLinks,
}}
>
<ConnectorAddFlyout
@ -95,6 +94,7 @@ describe('connector_add_flyout', () => {
reloadConnectors: () => {
return new Promise<void>(() => {});
},
docLinks: deps!.docLinks,
}}
>
<ConnectorAddFlyout
@ -154,6 +154,7 @@ describe('connector_add_flyout', () => {
reloadConnectors: () => {
return new Promise<void>(() => {});
},
docLinks: deps!.docLinks,
}}
>
<ConnectorAddFlyout
@ -201,6 +202,7 @@ describe('connector_add_flyout', () => {
reloadConnectors: () => {
return new Promise<void>(() => {});
},
docLinks: deps!.docLinks,
}}
>
<ConnectorAddFlyout

View file

@ -114,8 +114,6 @@ export const ConnectorAddFlyout = ({
connector={connector}
dispatch={dispatch}
errors={errors}
actionTypeRegistry={actionTypeRegistry}
http={http}
/>
);
}

View file

@ -9,11 +9,10 @@ import { coreMock } from '../../../../../../../src/core/public/mocks';
import { ConnectorAddModal } from './connector_add_modal';
import { actionTypeRegistryMock } from '../../action_type_registry.mock';
import { ValidationResult, ActionType } from '../../../types';
import { ActionsConnectorsContextValue } from '../../context/actions_connectors_context';
const actionTypeRegistry = actionTypeRegistryMock.create();
describe('connector_add_modal', () => {
let deps: ActionsConnectorsContextValue;
let deps: any;
beforeAll(async () => {
const mocks = coreMock.createSetup();
@ -27,13 +26,14 @@ describe('connector_add_modal', () => {
http: mocks.http,
capabilities: {
...capabilities,
actions: {
delete: true,
save: true,
show: true,
siem: {
'actions:show': true,
'actions:save': true,
'actions:delete': true,
},
},
actionTypeRegistry: actionTypeRegistry as any,
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
};
});
it('renders connector modal form if addModalVisible is true', () => {
@ -63,19 +63,19 @@ describe('connector_add_modal', () => {
minimumLicenseRequired: 'basic',
};
const wrapper = deps
? mountWithIntl(
<ConnectorAddModal
addModalVisible={true}
setAddModalVisibility={() => {}}
actionType={actionType}
http={deps.http}
actionTypeRegistry={deps.actionTypeRegistry}
toastNotifications={deps.toastNotifications}
/>
)
: undefined;
expect(wrapper?.find('EuiModalHeader')).toHaveLength(1);
expect(wrapper?.find('[data-test-subj="saveActionButtonModal"]').exists()).toBeTruthy();
const wrapper = mountWithIntl(
<ConnectorAddModal
addModalVisible={true}
setAddModalVisibility={() => {}}
actionType={actionType}
http={deps!.http}
actionTypeRegistry={deps!.actionTypeRegistry}
toastNotifications={deps!.toastNotifications}
docLinks={deps!.docLinks}
capabilities={deps!.capabilities}
/>
);
expect(wrapper.exists('.euiModalHeader')).toBeTruthy();
expect(wrapper.exists('[data-test-subj="saveActionButtonModal"]')).toBeTruthy();
});
});

View file

@ -17,7 +17,7 @@ import {
import { EuiButtonEmpty } from '@elastic/eui';
import { EuiOverlayMask } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { HttpSetup, ToastsApi } from 'kibana/public';
import { HttpSetup, ToastsApi, ApplicationStart, DocLinksStart } from 'kibana/public';
import { ActionConnectorForm, validateBaseProperties } from './action_connector_form';
import { ActionType, ActionConnector, IErrorObject, ActionTypeModel } from '../../../types';
import { connectorReducer } from './connector_reducer';
@ -25,6 +25,8 @@ import { createActionConnector } from '../../lib/action_connector_api';
import { TypeRegistry } from '../../type_registry';
import './connector_add_modal.scss';
import { PLUGIN } from '../../constants/plugin';
import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context';
import { hasSaveActionsCapability } from '../../lib/capabilities';
interface ConnectorAddModalProps {
actionType: ActionType;
@ -33,10 +35,12 @@ interface ConnectorAddModalProps {
postSaveEventHandler?: (savedAction: ActionConnector) => void;
http: HttpSetup;
actionTypeRegistry: TypeRegistry<ActionTypeModel>;
toastNotifications?: Pick<
toastNotifications: Pick<
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
capabilities: ApplicationStart['capabilities'];
docLinks: DocLinksStart;
}
export const ConnectorAddModal = ({
@ -47,6 +51,8 @@ export const ConnectorAddModal = ({
http,
toastNotifications,
actionTypeRegistry,
capabilities,
docLinks,
}: ConnectorAddModalProps) => {
let hasErrors = false;
const initialConnector = {
@ -55,6 +61,7 @@ export const ConnectorAddModal = ({
secrets: {},
} as ActionConnector;
const [isSaving, setIsSaving] = useState<boolean>(false);
const canSave = hasSaveActionsCapability(capabilities);
const [{ connector }, dispatch] = useReducer(connectorReducer, { connector: initialConnector });
const setConnector = (value: any) => {
@ -149,17 +156,24 @@ export const ConnectorAddModal = ({
</EuiModalHeader>
<EuiModalBody>
<ActionConnectorForm
connector={connector}
actionTypeName={actionType.name}
dispatch={dispatch}
serverError={serverError}
errors={errors}
actionTypeRegistry={actionTypeRegistry}
http={http}
/>
<ActionsConnectorsContextProvider
value={{
actionTypeRegistry,
http,
capabilities,
toastNotifications,
docLinks,
}}
>
<ActionConnectorForm
connector={connector}
actionTypeName={actionType.name}
dispatch={dispatch}
serverError={serverError}
errors={errors}
/>
</ActionsConnectorsContextProvider>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty onClick={closeModal}>
{i18n.translate(
@ -169,32 +183,33 @@ export const ConnectorAddModal = ({
}
)}
</EuiButtonEmpty>
<EuiButton
fill
color="secondary"
data-test-subj="saveActionButtonModal"
type="submit"
iconType="check"
isDisabled={hasErrors}
isLoading={isSaving}
onClick={async () => {
setIsSaving(true);
const savedAction = await onActionConnectorSave();
setIsSaving(false);
if (savedAction) {
if (postSaveEventHandler) {
postSaveEventHandler(savedAction);
{canSave ? (
<EuiButton
fill
color="secondary"
data-test-subj="saveActionButtonModal"
type="submit"
iconType="check"
isDisabled={hasErrors}
isLoading={isSaving}
onClick={async () => {
setIsSaving(true);
const savedAction = await onActionConnectorSave();
setIsSaving(false);
if (savedAction) {
if (postSaveEventHandler) {
postSaveEventHandler(savedAction);
}
closeModal();
}
closeModal();
}
}}
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.addModalConnectorForm.saveButtonLabel"
defaultMessage="Save"
/>
</EuiButton>
}}
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.addModalConnectorForm.saveButtonLabel"
defaultMessage="Save"
/>
</EuiButton>
) : null}
</EuiModalFooter>
</EuiModal>
</EuiOverlayMask>

View file

@ -37,6 +37,7 @@ describe('connector_edit_flyout', () => {
},
actionTypeRegistry: actionTypeRegistry as any,
alertTypeRegistry: {} as any,
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
};
});
@ -80,6 +81,7 @@ describe('connector_edit_flyout', () => {
reloadConnectors: () => {
return new Promise<void>(() => {});
},
docLinks: deps.docLinks,
}}
>
<ConnectorEditFlyout
@ -136,6 +138,7 @@ describe('connector_edit_flyout', () => {
reloadConnectors: () => {
return new Promise<void>(() => {});
},
docLinks: deps.docLinks,
}}
>
<ConnectorEditFlyout

View file

@ -47,6 +47,7 @@ export const ConnectorEditFlyout = ({
capabilities,
actionTypeRegistry,
reloadConnectors,
docLinks,
} = useActionsConnectorsContext();
const canSave = hasSaveActionsCapability(capabilities);
const closeFlyout = useCallback(() => setEditFlyoutVisibility(false), [setEditFlyoutVisibility]);
@ -181,8 +182,6 @@ export const ConnectorEditFlyout = ({
errors={errors}
actionTypeName={connector.actionType}
dispatch={dispatch}
actionTypeRegistry={actionTypeRegistry}
http={http}
/>
) : (
<Fragment>
@ -194,7 +193,10 @@ export const ConnectorEditFlyout = ({
}
)}
</EuiText>
<EuiLink href="https://www.elastic.co/guide" target="_blank">
<EuiLink
href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/pre-configured-connectors.html`}
target="_blank"
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.editConnectorForm.preconfiguredHelpLabel"
defaultMessage="Learn more about preconfigured connectors."

View file

@ -32,7 +32,13 @@ import { ActionConnector, ActionConnectorTableItem, ActionTypeIndex } from '../.
import { EmptyConnectorsPrompt } from '../../../components/prompts/empty_connectors_prompt';
export const ActionsConnectorsList: React.FunctionComponent = () => {
const { http, toastNotifications, capabilities, actionTypeRegistry } = useAppDependencies();
const {
http,
toastNotifications,
capabilities,
actionTypeRegistry,
docLinks,
} = useAppDependencies();
const canDelete = hasDeleteActionsCapability(capabilities);
const canSave = hasSaveActionsCapability(capabilities);
@ -394,6 +400,7 @@ export const ActionsConnectorsList: React.FunctionComponent = () => {
capabilities,
toastNotifications,
reloadConnectors: loadActions,
docLinks,
}}
>
<ConnectorAddFlyout

View file

@ -138,6 +138,7 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
charts,
dataFieldsFormats: dataPlugin.fieldFormats,
reloadAlerts: setAlert,
capabilities,
}}
>
<AlertEdit

View file

@ -41,11 +41,16 @@ describe('alert_add', () => {
let wrapper: ReactWrapper<any>;
async function setup() {
const mockes = coreMock.createSetup();
const mocks = coreMock.createSetup();
const [
{
application: { capabilities },
},
] = await mocks.getStartServices();
deps = {
toastNotifications: mockes.notifications.toasts,
http: mockes.http,
uiSettings: mockes.uiSettings,
toastNotifications: mocks.notifications.toasts,
http: mocks.http,
uiSettings: mocks.uiSettings,
dataPlugin: dataPluginMock.createStartContract(),
charts: chartPluginMock.createStartContract(),
actionTypeRegistry: actionTypeRegistry as any,
@ -53,7 +58,7 @@ describe('alert_add', () => {
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
};
mockes.http.get.mockResolvedValue({
mocks.http.get.mockResolvedValue({
isSufficientlySecure: true,
hasPermanentEncryptionKey: true,
});
@ -104,6 +109,14 @@ describe('alert_add', () => {
uiSettings: deps.uiSettings,
docLinks: deps.docLinks,
metadata: { test: 'some value', fields: ['test'] },
capabilities: {
...capabilities,
actions: {
delete: true,
save: true,
show: true,
},
},
}}
>
<AlertAdd

View file

@ -27,6 +27,11 @@ describe('alert_edit', () => {
});
async function setup() {
const [
{
application: { capabilities },
},
] = await mockedCoreSetup.getStartServices();
deps = {
toastNotifications: mockedCoreSetup.notifications.toasts,
http: mockedCoreSetup.http,
@ -34,6 +39,7 @@ describe('alert_edit', () => {
actionTypeRegistry: actionTypeRegistry as any,
alertTypeRegistry: alertTypeRegistry as any,
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
capabilities,
};
mockedCoreSetup.http.get.mockResolvedValue({
@ -122,6 +128,7 @@ describe('alert_edit', () => {
toastNotifications: deps!.toastNotifications,
uiSettings: deps!.uiSettings,
docLinks: deps.docLinks,
capabilities: deps!.capabilities,
}}
>
<AlertEdit

View file

@ -46,14 +46,20 @@ describe('alert_form', () => {
let wrapper: ReactWrapper<any>;
async function setup() {
const mockes = coreMock.createSetup();
const mocks = coreMock.createSetup();
const [
{
application: { capabilities },
},
] = await mocks.getStartServices();
deps = {
toastNotifications: mockes.notifications.toasts,
http: mockes.http,
uiSettings: mockes.uiSettings,
toastNotifications: mocks.notifications.toasts,
http: mocks.http,
uiSettings: mocks.uiSettings,
actionTypeRegistry: actionTypeRegistry as any,
alertTypeRegistry: alertTypeRegistry as any,
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
capabilities,
};
alertTypeRegistry.list.mockReturnValue([alertType]);
alertTypeRegistry.has.mockReturnValue(true);
@ -86,6 +92,7 @@ describe('alert_form', () => {
alertTypeRegistry: deps!.alertTypeRegistry,
toastNotifications: deps!.toastNotifications,
uiSettings: deps!.uiSettings,
capabilities: deps!.capabilities,
}}
>
<AlertForm alert={initialAlert} dispatch={() => {}} errors={{ name: [], interval: [] }} />
@ -166,6 +173,7 @@ describe('alert_form', () => {
alertTypeRegistry: deps!.alertTypeRegistry,
toastNotifications: deps!.toastNotifications,
uiSettings: deps!.uiSettings,
capabilities: deps!.capabilities,
}}
>
<AlertForm alert={initialAlert} dispatch={() => {}} errors={{ name: [], interval: [] }} />

View file

@ -87,7 +87,14 @@ export const AlertForm = ({
setHasActionsDisabled,
}: AlertFormProps) => {
const alertsContext = useAlertsContext();
const { http, toastNotifications, alertTypeRegistry, actionTypeRegistry } = alertsContext;
const {
http,
toastNotifications,
alertTypeRegistry,
actionTypeRegistry,
docLinks,
capabilities,
} = alertsContext;
const [alertTypeModel, setAlertTypeModel] = useState<AlertTypeModel | null>(
alert.alertTypeId ? alertTypeRegistry.get(alert.alertTypeId) : null
@ -245,6 +252,8 @@ export const AlertForm = ({
actionTypeRegistry={actionTypeRegistry}
defaultActionMessage={alertTypeModel?.defaultActionMessage}
toastNotifications={toastNotifications}
docLinks={docLinks}
capabilities={capabilities}
/>
) : null}
</Fragment>

View file

@ -435,6 +435,7 @@ export const AlertsList: React.FunctionComponent = () => {
docLinks,
charts,
dataFieldsFormats: dataPlugin.fieldFormats,
capabilities,
}}
>
<AlertAdd

View file

@ -18,6 +18,7 @@ export const UptimeAlertsContextProvider: React.FC = ({ children }) => {
triggers_actions_ui: { actionTypeRegistry, alertTypeRegistry },
uiSettings,
docLinks,
application: { capabilities },
},
} = useKibana();
@ -32,6 +33,7 @@ export const UptimeAlertsContextProvider: React.FC = ({ children }) => {
http,
toastNotifications: notifications?.toasts,
uiSettings,
capabilities,
}}
>
{children}