Add indicator to rule details page if broken connector (#112017)

* Add indicator to rule details page if broken connector

* Adding unit test

* Updating disabled state and adding warning callout

* Adding back the tooltip

* eslint

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
ymao1 2021-09-21 12:48:05 -04:00 committed by GitHub
parent c76082e006
commit 9574b4b86b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 413 additions and 386 deletions

View file

@ -39,6 +39,10 @@ jest.mock('react-router-dom', () => ({
}),
}));
jest.mock('../../../lib/action_connector_api', () => ({
loadAllActions: jest.fn().mockResolvedValue([]),
}));
jest.mock('../../../lib/capabilities', () => ({
hasAllPrivilege: jest.fn(() => true),
hasSaveAlertsCapability: jest.fn(() => true),
@ -60,24 +64,22 @@ const authorizedConsumers = {
};
const recoveryActionGroup: ActionGroup<'recovered'> = { id: 'recovered', name: 'Recovered' };
describe('alert_details', () => {
// mock Api handlers
const alertType: AlertType = {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
producer: ALERTS_FEATURE_ID,
authorizedConsumers,
enabledInLicense: true,
};
describe('alert_details', () => {
it('renders the alert name as a title', () => {
const alert = mockAlert();
const alertType: AlertType = {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
producer: ALERTS_FEATURE_ID,
authorizedConsumers,
enabledInLicense: true,
};
expect(
shallow(
<AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} />
@ -87,19 +89,6 @@ describe('alert_details', () => {
it('renders the alert type badge', () => {
const alert = mockAlert();
const alertType: AlertType = {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
producer: ALERTS_FEATURE_ID,
authorizedConsumers,
enabledInLicense: true,
};
expect(
shallow(
<AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} />
@ -118,19 +107,6 @@ describe('alert_details', () => {
},
},
});
const alertType: AlertType = {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
producer: ALERTS_FEATURE_ID,
authorizedConsumers,
enabledInLicense: true,
};
expect(
shallow(
<AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} />
@ -155,19 +131,6 @@ describe('alert_details', () => {
],
});
const alertType: AlertType = {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
producer: ALERTS_FEATURE_ID,
authorizedConsumers,
enabledInLicense: true,
};
const actionTypes: ActionType[] = [
{
id: '.server-log',
@ -212,18 +175,6 @@ describe('alert_details', () => {
},
],
});
const alertType: AlertType = {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
minimumLicenseRequired: 'basic',
authorizedConsumers,
enabledInLicense: true,
};
const actionTypes: ActionType[] = [
{
id: '.server-log',
@ -273,20 +224,6 @@ describe('alert_details', () => {
describe('links', () => {
it('links to the app that created the alert', () => {
const alert = mockAlert();
const alertType: AlertType = {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
authorizedConsumers,
minimumLicenseRequired: 'basic',
enabledInLicense: true,
};
expect(
shallow(
<AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} />
@ -296,19 +233,6 @@ describe('alert_details', () => {
it('links to the Edit flyout', () => {
const alert = mockAlert();
const alertType: AlertType = {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
authorizedConsumers,
minimumLicenseRequired: 'basic',
enabledInLicense: true,
};
const pageHeaderProps = shallow(
<AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} />
)
@ -316,22 +240,22 @@ describe('alert_details', () => {
.props() as EuiPageHeaderProps;
const rightSideItems = pageHeaderProps.rightSideItems;
expect(!!rightSideItems && rightSideItems[2]!).toMatchInlineSnapshot(`
<React.Fragment>
<EuiButtonEmpty
data-test-subj="openEditAlertFlyoutButton"
disabled={false}
iconType="pencil"
name="edit"
onClick={[Function]}
>
<FormattedMessage
defaultMessage="Edit"
id="xpack.triggersActionsUI.sections.alertDetails.editAlertButtonLabel"
values={Object {}}
/>
</EuiButtonEmpty>
</React.Fragment>
`);
<React.Fragment>
<EuiButtonEmpty
data-test-subj="openEditAlertFlyoutButton"
disabled={false}
iconType="pencil"
name="edit"
onClick={[Function]}
>
<FormattedMessage
defaultMessage="Edit"
id="xpack.triggersActionsUI.sections.alertDetails.editAlertButtonLabel"
values={Object {}}
/>
</EuiButtonEmpty>
</React.Fragment>
`);
});
});
});
@ -341,20 +265,6 @@ describe('disable button', () => {
const alert = mockAlert({
enabled: true,
});
const alertType: AlertType = {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
authorizedConsumers,
minimumLicenseRequired: 'basic',
enabledInLicense: true,
};
const enableButton = shallow(
<AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} />
)
@ -368,55 +278,43 @@ describe('disable button', () => {
});
});
it('should render a enable button when alert is disabled', () => {
it('should render a enable button and empty state when alert is disabled', async () => {
const alert = mockAlert({
enabled: false,
});
const alertType: AlertType = {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
authorizedConsumers,
minimumLicenseRequired: 'basic',
enabledInLicense: true,
};
const enableButton = shallow(
const wrapper = mountWithIntl(
<AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} />
)
.find(EuiSwitch)
.find('[name="enable"]')
.first();
);
await act(async () => {
await nextTick();
wrapper.update();
});
const enableButton = wrapper.find(EuiSwitch).find('[name="enable"]').first();
const disabledEmptyPrompt = wrapper.find('[data-test-subj="disabledEmptyPrompt"]');
const disabledEmptyPromptAction = wrapper.find('[data-test-subj="disabledEmptyPromptAction"]');
expect(enableButton.props()).toMatchObject({
checked: false,
disabled: false,
});
expect(disabledEmptyPrompt.exists()).toBeTruthy();
expect(disabledEmptyPromptAction.exists()).toBeTruthy();
disabledEmptyPromptAction.first().simulate('click');
await act(async () => {
await nextTick();
wrapper.update();
});
expect(mockAlertApis.enableAlert).toHaveBeenCalledTimes(1);
});
it('should enable the alert when alert is disabled and button is clicked', () => {
it('should disable the alert when alert is enabled and button is clicked', () => {
const alert = mockAlert({
enabled: true,
});
const alertType: AlertType = {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
authorizedConsumers,
minimumLicenseRequired: 'basic',
enabledInLicense: true,
};
const disableAlert = jest.fn();
const enableButton = shallow(
<AlertDetails
@ -439,24 +337,10 @@ describe('disable button', () => {
expect(disableAlert).toHaveBeenCalledTimes(1);
});
it('should disable the alert when alert is enabled and button is clicked', () => {
it('should enable the alert when alert is disabled and button is clicked', () => {
const alert = mockAlert({
enabled: false,
});
const alertType: AlertType = {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
authorizedConsumers,
minimumLicenseRequired: 'basic',
enabledInLicense: true,
};
const enableAlert = jest.fn();
const enableButton = shallow(
<AlertDetails
@ -492,19 +376,6 @@ describe('disable button', () => {
},
});
const alertType: AlertType = {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
authorizedConsumers,
minimumLicenseRequired: 'basic',
enabledInLicense: true,
};
const disableAlert = jest.fn();
const enableAlert = jest.fn();
const wrapper = mountWithIntl(
@ -565,19 +436,6 @@ describe('disable button', () => {
},
});
const alertType: AlertType = {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
authorizedConsumers,
minimumLicenseRequired: 'basic',
enabledInLicense: true,
};
const disableAlert = jest.fn(async () => {
await new Promise((resolve) => setTimeout(resolve, 6000));
});
@ -630,27 +488,12 @@ describe('mute button', () => {
enabled: true,
muteAll: false,
});
const alertType: AlertType = {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
authorizedConsumers,
minimumLicenseRequired: 'basic',
enabledInLicense: true,
};
const enableButton = shallow(
<AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} />
)
.find(EuiSwitch)
.find('[name="mute"]')
.first();
expect(enableButton.props()).toMatchObject({
checked: false,
disabled: false,
@ -662,27 +505,12 @@ describe('mute button', () => {
enabled: true,
muteAll: true,
});
const alertType: AlertType = {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
authorizedConsumers,
minimumLicenseRequired: 'basic',
enabledInLicense: true,
};
const enableButton = shallow(
<AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} />
)
.find(EuiSwitch)
.find('[name="mute"]')
.first();
expect(enableButton.props()).toMatchObject({
checked: true,
disabled: false,
@ -694,20 +522,6 @@ describe('mute button', () => {
enabled: true,
muteAll: false,
});
const alertType: AlertType = {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
authorizedConsumers,
minimumLicenseRequired: 'basic',
enabledInLicense: true,
};
const muteAlert = jest.fn();
const enableButton = shallow(
<AlertDetails
@ -721,7 +535,6 @@ describe('mute button', () => {
.find(EuiSwitch)
.find('[name="mute"]')
.first();
enableButton.simulate('click');
const handler = enableButton.prop('onChange');
expect(typeof handler).toEqual('function');
@ -735,20 +548,6 @@ describe('mute button', () => {
enabled: true,
muteAll: true,
});
const alertType: AlertType = {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
authorizedConsumers,
minimumLicenseRequired: 'basic',
enabledInLicense: true,
};
const unmuteAlert = jest.fn();
const enableButton = shallow(
<AlertDetails
@ -762,7 +561,6 @@ describe('mute button', () => {
.find(EuiSwitch)
.find('[name="mute"]')
.first();
enableButton.simulate('click');
const handler = enableButton.prop('onChange');
expect(typeof handler).toEqual('function');
@ -776,27 +574,12 @@ describe('mute button', () => {
enabled: false,
muteAll: false,
});
const alertType: AlertType = {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: ALERTS_FEATURE_ID,
authorizedConsumers,
minimumLicenseRequired: 'basic',
enabledInLicense: true,
};
const enableButton = shallow(
<AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} />
)
.find(EuiSwitch)
.find('[name="mute"]')
.first();
expect(enableButton.props()).toMatchObject({
checked: false,
disabled: true,
@ -843,20 +626,6 @@ describe('edit button', () => {
},
],
});
const alertType: AlertType = {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: 'alerting',
authorizedConsumers,
minimumLicenseRequired: 'basic',
enabledInLicense: true,
};
const pageHeaderProps = shallow(
<AlertDetails
alert={alert}
@ -869,27 +638,27 @@ describe('edit button', () => {
.props() as EuiPageHeaderProps;
const rightSideItems = pageHeaderProps.rightSideItems;
expect(!!rightSideItems && rightSideItems[2]!).toMatchInlineSnapshot(`
<React.Fragment>
<EuiButtonEmpty
data-test-subj="openEditAlertFlyoutButton"
disabled={false}
iconType="pencil"
name="edit"
onClick={[Function]}
>
<FormattedMessage
defaultMessage="Edit"
id="xpack.triggersActionsUI.sections.alertDetails.editAlertButtonLabel"
values={Object {}}
/>
</EuiButtonEmpty>
</React.Fragment>
`);
<React.Fragment>
<EuiButtonEmpty
data-test-subj="openEditAlertFlyoutButton"
disabled={false}
iconType="pencil"
name="edit"
onClick={[Function]}
>
<FormattedMessage
defaultMessage="Edit"
id="xpack.triggersActionsUI.sections.alertDetails.editAlertButtonLabel"
values={Object {}}
/>
</EuiButtonEmpty>
</React.Fragment>
`);
});
it('should not render an edit button when alert editable but actions arent', () => {
const { hasExecuteActionsCapability } = jest.requireMock('../../../lib/capabilities');
hasExecuteActionsCapability.mockReturnValue(false);
hasExecuteActionsCapability.mockReturnValueOnce(false);
const alert = mockAlert({
enabled: true,
muteAll: false,
@ -902,20 +671,6 @@ describe('edit button', () => {
},
],
});
const alertType: AlertType = {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: 'alerting',
authorizedConsumers,
minimumLicenseRequired: 'basic',
enabledInLicense: true,
};
expect(
shallow(
<AlertDetails
@ -934,26 +689,12 @@ describe('edit button', () => {
it('should render an edit button when alert editable but actions arent when there are no actions on the alert', async () => {
const { hasExecuteActionsCapability } = jest.requireMock('../../../lib/capabilities');
hasExecuteActionsCapability.mockReturnValue(false);
hasExecuteActionsCapability.mockReturnValueOnce(false);
const alert = mockAlert({
enabled: true,
muteAll: false,
actions: [],
});
const alertType: AlertType = {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
producer: 'alerting',
authorizedConsumers,
minimumLicenseRequired: 'basic',
enabledInLicense: true,
};
const pageHeaderProps = shallow(
<AlertDetails
alert={alert}
@ -966,41 +707,221 @@ describe('edit button', () => {
.props() as EuiPageHeaderProps;
const rightSideItems = pageHeaderProps.rightSideItems;
expect(!!rightSideItems && rightSideItems[2]!).toMatchInlineSnapshot(`
<React.Fragment>
<EuiButtonEmpty
data-test-subj="openEditAlertFlyoutButton"
disabled={false}
iconType="pencil"
name="edit"
onClick={[Function]}
>
<FormattedMessage
defaultMessage="Edit"
id="xpack.triggersActionsUI.sections.alertDetails.editAlertButtonLabel"
values={Object {}}
/>
</EuiButtonEmpty>
</React.Fragment>
`);
<React.Fragment>
<EuiButtonEmpty
data-test-subj="openEditAlertFlyoutButton"
disabled={false}
iconType="pencil"
name="edit"
onClick={[Function]}
>
<FormattedMessage
defaultMessage="Edit"
id="xpack.triggersActionsUI.sections.alertDetails.editAlertButtonLabel"
values={Object {}}
/>
</EuiButtonEmpty>
</React.Fragment>
`);
});
});
describe('broken connector indicator', () => {
const actionTypes: ActionType[] = [
{
id: '.server-log',
name: 'Server log',
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
},
];
ruleTypeRegistry.has.mockReturnValue(true);
const alertTypeR: AlertTypeModel = {
id: 'my-alert-type',
iconClass: 'test',
description: 'Alert when testing',
documentationUrl: 'https://localhost.local/docs',
validate: () => {
return { errors: {} };
},
alertParamsExpression: jest.fn(),
requiresAppContext: false,
};
ruleTypeRegistry.get.mockReturnValue(alertTypeR);
useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry;
const { loadAllActions } = jest.requireMock('../../../lib/action_connector_api');
loadAllActions.mockResolvedValue([
{
secrets: {},
isMissingSecrets: false,
id: 'connector-id-1',
actionTypeId: '.server-log',
name: 'Test connector',
config: {},
isPreconfigured: false,
},
{
secrets: {},
isMissingSecrets: false,
id: 'connector-id-2',
actionTypeId: '.server-log',
name: 'Test connector 2',
config: {},
isPreconfigured: false,
},
]);
it('should not render broken connector indicator or warning if all rule actions connectors exist', async () => {
const alert = mockAlert({
enabled: true,
muteAll: false,
actions: [
{
group: 'default',
id: 'connector-id-1',
params: {},
actionTypeId: '.server-log',
},
{
group: 'default',
id: 'connector-id-2',
params: {},
actionTypeId: '.server-log',
},
],
});
const wrapper = mountWithIntl(
<AlertDetails
alert={alert}
alertType={alertType}
actionTypes={actionTypes}
{...mockAlertApis}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
const brokenConnectorIndicator = wrapper
.find('[data-test-subj="actionWithBrokenConnector"]')
.first();
const brokenConnectorWarningBanner = wrapper
.find('[data-test-subj="actionWithBrokenConnectorWarningBanner"]')
.first();
expect(brokenConnectorIndicator.exists()).toBeFalsy();
expect(brokenConnectorWarningBanner.exists()).toBeFalsy();
});
it('should render broken connector indicator and warning if any rule actions connector does not exist', async () => {
const alert = mockAlert({
enabled: true,
muteAll: false,
actions: [
{
group: 'default',
id: 'connector-id-1',
params: {},
actionTypeId: '.server-log',
},
{
group: 'default',
id: 'connector-id-2',
params: {},
actionTypeId: '.server-log',
},
{
group: 'default',
id: 'connector-id-doesnt-exist',
params: {},
actionTypeId: '.server-log',
},
],
});
const wrapper = mountWithIntl(
<AlertDetails
alert={alert}
alertType={alertType}
actionTypes={actionTypes}
{...mockAlertApis}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
const brokenConnectorIndicator = wrapper
.find('[data-test-subj="actionWithBrokenConnector"]')
.first();
const brokenConnectorWarningBanner = wrapper
.find('[data-test-subj="actionWithBrokenConnectorWarningBanner"]')
.first();
const brokenConnectorWarningBannerAction = wrapper
.find('[data-test-subj="actionWithBrokenConnectorWarningBannerEdit"]')
.first();
expect(brokenConnectorIndicator.exists()).toBeTruthy();
expect(brokenConnectorWarningBanner.exists()).toBeTruthy();
expect(brokenConnectorWarningBannerAction.exists()).toBeTruthy();
});
it('should render broken connector indicator and warning with no edit button if any rule actions connector does not exist and user has no edit access', async () => {
const alert = mockAlert({
enabled: true,
muteAll: false,
actions: [
{
group: 'default',
id: 'connector-id-1',
params: {},
actionTypeId: '.server-log',
},
{
group: 'default',
id: 'connector-id-2',
params: {},
actionTypeId: '.server-log',
},
{
group: 'default',
id: 'connector-id-doesnt-exist',
params: {},
actionTypeId: '.server-log',
},
],
});
const { hasExecuteActionsCapability } = jest.requireMock('../../../lib/capabilities');
hasExecuteActionsCapability.mockReturnValue(false);
const wrapper = mountWithIntl(
<AlertDetails
alert={alert}
alertType={alertType}
actionTypes={actionTypes}
{...mockAlertApis}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
const brokenConnectorIndicator = wrapper
.find('[data-test-subj="actionWithBrokenConnector"]')
.first();
const brokenConnectorWarningBanner = wrapper
.find('[data-test-subj="actionWithBrokenConnectorWarningBanner"]')
.first();
const brokenConnectorWarningBannerAction = wrapper
.find('[data-test-subj="actionWithBrokenConnectorWarningBannerEdit"]')
.first();
expect(brokenConnectorIndicator.exists()).toBeTruthy();
expect(brokenConnectorWarningBanner.exists()).toBeTruthy();
expect(brokenConnectorWarningBannerAction.exists()).toBeFalsy();
});
});
describe('refresh button', () => {
it('should call requestRefresh when clicked', () => {
it('should call requestRefresh when clicked', async () => {
const alert = mockAlert();
const alertType: AlertType = {
id: '.noop',
name: 'No Op',
actionGroups: [{ id: 'default', name: 'Default' }],
recoveryActionGroup,
actionVariables: { context: [], state: [], params: [] },
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
producer: ALERTS_FEATURE_ID,
authorizedConsumers,
enabledInLicense: true,
};
const requestRefresh = jest.fn();
const wrapper = mountWithIntl(
<AlertDetails
@ -1012,6 +933,10 @@ describe('refresh button', () => {
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
const refreshButton = wrapper.find('[data-test-subj="refreshAlertsButton"]').first();
expect(refreshButton.exists()).toBeTruthy();

View file

@ -22,13 +22,16 @@ import {
EuiButtonEmpty,
EuiButton,
EuiLoadingSpinner,
EuiIconTip,
EuiEmptyPrompt,
EuiPageTemplate,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { AlertExecutionStatusErrorReasons } from '../../../../../../alerting/common';
import { hasAllPrivilege, hasExecuteActionsCapability } from '../../../lib/capabilities';
import { getAlertingSectionBreadcrumb, getAlertDetailsBreadcrumb } from '../../../lib/breadcrumb';
import { getCurrentDocTitle } from '../../../lib/doc_title';
import { Alert, AlertType, ActionType } from '../../../../types';
import { Alert, AlertType, ActionType, ActionConnector } from '../../../../types';
import {
ComponentOpts as BulkOperationsComponentOpts,
withBulkAlertOperations,
@ -40,6 +43,7 @@ import { routeToRuleDetails } from '../../../constants';
import { alertsErrorReasonTranslationsMapping } from '../../alerts_list/translations';
import { useKibana } from '../../../../common/lib/kibana';
import { alertReducer } from '../../alert_form/alert_reducer';
import { loadAllActions as loadConnectors } from '../../../lib/action_connector_api';
export type AlertDetailsProps = {
alert: Alert;
@ -72,6 +76,9 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
dispatch({ command: { type: 'setAlert' }, payload: { key: 'alert', value } });
};
const [hasActionsWithBrokenConnector, setHasActionsWithBrokenConnector] =
useState<boolean>(false);
// Set breadcrumb and page title
useEffect(() => {
setBreadcrumbs([
@ -82,6 +89,28 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Determine if any attached action has an issue with its connector
useEffect(() => {
(async () => {
let loadedConnectors: ActionConnector[] = [];
try {
loadedConnectors = await loadConnectors({ http });
} catch (err) {
loadedConnectors = [];
}
if (loadedConnectors.length > 0) {
const hasActionWithBrokenConnector = alert.actions.some(
(action) => !loadedConnectors.find((connector) => connector.id === action.id)
);
if (setHasActionsWithBrokenConnector) {
setHasActionsWithBrokenConnector(hasActionWithBrokenConnector);
}
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const canExecuteActions = hasExecuteActionsCapability(capabilities);
const canSaveAlert =
hasAllPrivilege(alert, alertType) &&
@ -197,13 +226,27 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
{uniqueActions && uniqueActions.length ? (
<>
<EuiText size="s">
<p>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsTex"
defaultMessage="Actions"
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsTex"
defaultMessage="Actions"
/>{' '}
{hasActionsWithBrokenConnector && (
<EuiIconTip
data-test-subj="actionWithBrokenConnector"
type="alert"
color="danger"
content={i18n.translate(
'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsWarningTooltip',
{
defaultMessage:
'Unable to load one of the connectors associated with this rule. Edit the rule to select a new connector.',
}
)}
position="right"
/>
</p>
)}
</EuiText>
<EuiSpacer size="xs" />
<EuiFlexGroup wrap gutterSize="s">
{uniqueActions.map((action, index) => (
@ -358,6 +401,42 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
</EuiFlexItem>
</EuiFlexGroup>
) : null}
{hasActionsWithBrokenConnector && (
<EuiFlexGroup>
<EuiFlexItem>
<EuiSpacer size="s" />
<EuiCallOut
color="warning"
data-test-subj="actionWithBrokenConnectorWarningBanner"
size="s"
title={i18n.translate(
'xpack.triggersActionsUI.sections.alertDetails.actionWithBrokenConnectorWarningBannerTitle',
{
defaultMessage:
'There is an issue with one of the connectors associated with this rule.',
}
)}
>
{hasEditButton && (
<EuiFlexGroup gutterSize="s" wrap={true}>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="actionWithBrokenConnectorWarningBannerEdit"
color="warning"
onClick={() => setEditFlyoutVisibility(true)}
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertDetails.actionWithBrokenConnectorWarningBannerEditText"
defaultMessage="Edit rule"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
)}
</EuiCallOut>
</EuiFlexItem>
</EuiFlexGroup>
)}
<EuiFlexGroup>
<EuiFlexItem>
{alert.enabled ? (
@ -370,23 +449,46 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
) : (
<>
<EuiSpacer />
<EuiCallOut
title={i18n.translate(
'xpack.triggersActionsUI.sections.alertDetails.alerts.disabledRuleTitle',
{
defaultMessage: 'Disabled Rule',
<EuiPageTemplate template="empty">
<EuiEmptyPrompt
data-test-subj="disabledEmptyPrompt"
title={
<h2>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertDetails.alerts.disabledRuleTitle"
defaultMessage="Disabled Rule"
/>
</h2>
}
)}
color="warning"
iconType="help"
>
<p>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertDetails.alertInstances.disabledRule"
defaultMessage="This rule is disabled and cannot be displayed. Toggle Disable ↑ to activate it."
/>
</p>
</EuiCallOut>
body={
<>
<p>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertDetails.alertInstances.disabledRule"
defaultMessage="This rule is disabled and cannot be displayed."
/>
</p>
</>
}
actions={[
<EuiButton
data-test-subj="disabledEmptyPromptAction"
color="primary"
fill
disabled={isEnabledUpdating}
onClick={async () => {
setIsEnabledUpdating(true);
setIsEnabled(true);
await enableAlert(alert);
requestRefresh();
setIsEnabledUpdating(false);
}}
>
Enable
</EuiButton>,
]}
/>
</EuiPageTemplate>
</>
)}
</EuiFlexItem>