Allow registered alert types to be non-editable (#65606)

* Allow registered alert types to be non-editable

* Fixed isUiEditEnabled values

* Fixed due to comments

* fixed failing tests

* Enable alert type selection per alert consumer, only 'alerting' consumer can display other consumers alert types, but in case if it isEditable

* fixed tests

* Removed consumer property from the client side alert type registry and added server side property producer which purpose is to manage a feature logic

* fixed type check

* Fixed tests and type checks

* Removed error message for non registered plugins

* Fixed failing tests

* Fixed due to comments

* fixed test

* -

* revert logic for requiresAppContext

* Added close toast after saving alert
This commit is contained in:
Yuliia Naumenko 2020-05-12 13:38:22 -07:00 committed by GitHub
parent fd4074f2cd
commit 5ed5fda832
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 332 additions and 22 deletions

View file

@ -51,6 +51,7 @@ export function getAlertType(): AlertTypeModel {
}
return validationResult;
},
requiresAppContext: false,
};
}

View file

@ -99,6 +99,7 @@ export function getAlertType(): AlertTypeModel {
return validationResult;
},
requiresAppContext: false,
};
}

View file

@ -20,7 +20,7 @@
import uuid from 'uuid';
import { range } from 'lodash';
import { AlertType } from '../../../../x-pack/plugins/alerting/server';
import { DEFAULT_INSTANCES_TO_GENERATE } from '../../common/constants';
import { DEFAULT_INSTANCES_TO_GENERATE, ALERTING_EXAMPLE_APP_ID } from '../../common/constants';
export const alertType: AlertType = {
id: 'example.always-firing',
@ -43,4 +43,5 @@ export const alertType: AlertType = {
count,
};
},
producer: ALERTING_EXAMPLE_APP_ID,
};

View file

@ -19,7 +19,7 @@
import axios from 'axios';
import { AlertType } from '../../../../x-pack/plugins/alerting/server';
import { Operator, Craft } from '../../common/constants';
import { Operator, Craft, ALERTING_EXAMPLE_APP_ID } from '../../common/constants';
interface PeopleInSpace {
people: Array<{
@ -79,4 +79,5 @@ export const alertType: AlertType = {
peopleInSpace,
};
},
producer: ALERTING_EXAMPLE_APP_ID,
};

View file

@ -91,6 +91,7 @@ The following table describes the properties of the `options` object.
|actionVariables|An explicit list of action variables the alert type makes available via context and state in action parameter templates, and a short human readable description. Alert UI will use this to display prompts for the users for these variables, in action parameter editors. We highly encourage using `kbn-i18n` to translate the descriptions. |{ context: Array<{name:string, description:string}, state: Array<{name:string, description:string}>|
|validate.params|When developing an alert type, you can choose to accept a series of parameters. You may also have the parameters validated before they are passed to the `executor` function or created as an alert saved object. In order to do this, provide a `@kbn/config-schema` schema that we will use to validate the `params` attribute.|@kbn/config-schema|
|executor|This is where the code of the alert type lives. This is a function to be called when executing an alert on an interval basis. For full details, see executor section below.|Function|
|producer|The id of the application producing this alert type.|string|
### Executor
@ -212,6 +213,7 @@ server.newPlatform.setup.plugins.alerting.registerType({
lastChecked: new Date(),
};
},
producer: 'alerting',
});
```
@ -287,6 +289,7 @@ server.newPlatform.setup.plugins.alerting.registerType({
lastChecked: new Date(),
};
},
producer: 'alerting',
});
```

View file

@ -10,6 +10,7 @@ export interface AlertType {
actionGroups: ActionGroup[];
actionVariables: string[];
defaultActionGroupId: ActionGroup['id'];
producer: string;
}
export interface ActionGroup {

View file

@ -22,6 +22,7 @@ describe('loadAlertTypes', () => {
actionVariables: ['var1'],
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
producer: 'alerting',
},
];
http.get.mockResolvedValueOnce(resolvedValue);
@ -44,6 +45,7 @@ describe('loadAlertType', () => {
actionVariables: ['var1'],
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
producer: 'alerting',
};
http.get.mockResolvedValueOnce([alertType]);
@ -63,6 +65,7 @@ describe('loadAlertType', () => {
actionVariables: [],
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
producer: 'alerting',
};
http.get.mockResolvedValueOnce([alertType]);
@ -77,6 +80,7 @@ describe('loadAlertType', () => {
actionVariables: [],
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
producer: 'alerting',
},
]);

View file

@ -16,6 +16,7 @@ const mockAlertType = (id: string): AlertType => ({
actionGroups: [],
actionVariables: [],
defaultActionGroupId: 'default',
producer: 'alerting',
});
describe('AlertNavigationRegistry', () => {

View file

@ -36,6 +36,7 @@ describe('has()', () => {
],
defaultActionGroupId: 'default',
executor: jest.fn(),
producer: 'alerting',
});
expect(registry.has('foo')).toEqual(true);
});
@ -54,6 +55,7 @@ describe('register()', () => {
],
defaultActionGroupId: 'default',
executor: jest.fn(),
producer: 'alerting',
};
// eslint-disable-next-line @typescript-eslint/no-var-requires
const registry = new AlertTypeRegistry(alertTypeRegistryParams);
@ -84,6 +86,7 @@ describe('register()', () => {
],
defaultActionGroupId: 'default',
executor: jest.fn(),
producer: 'alerting',
};
const registry = new AlertTypeRegistry(alertTypeRegistryParams);
registry.register(alertType);
@ -104,6 +107,7 @@ describe('register()', () => {
],
defaultActionGroupId: 'default',
executor: jest.fn(),
producer: 'alerting',
});
expect(() =>
registry.register({
@ -117,6 +121,7 @@ describe('register()', () => {
],
defaultActionGroupId: 'default',
executor: jest.fn(),
producer: 'alerting',
})
).toThrowErrorMatchingInlineSnapshot(`"Alert type \\"test\\" is already registered."`);
});
@ -136,6 +141,7 @@ describe('get()', () => {
],
defaultActionGroupId: 'default',
executor: jest.fn(),
producer: 'alerting',
});
const alertType = registry.get('test');
expect(alertType).toMatchInlineSnapshot(`
@ -154,6 +160,7 @@ describe('get()', () => {
"executor": [MockFunction],
"id": "test",
"name": "Test",
"producer": "alerting",
}
`);
});
@ -186,6 +193,7 @@ describe('list()', () => {
],
defaultActionGroupId: 'testActionGroup',
executor: jest.fn(),
producer: 'alerting',
});
const result = registry.list();
expect(result).toMatchInlineSnapshot(`
@ -204,6 +212,7 @@ describe('list()', () => {
"defaultActionGroupId": "testActionGroup",
"id": "test",
"name": "Test",
"producer": "alerting",
},
]
`);
@ -251,6 +260,7 @@ function alertTypeWithVariables(id: string, context: string, state: string): Ale
actionGroups: [],
defaultActionGroupId: id,
async executor() {},
producer: 'alerting',
};
if (!context && !state) {

View file

@ -73,6 +73,7 @@ export class AlertTypeRegistry {
actionGroups: alertType.actionGroups,
defaultActionGroupId: alertType.defaultActionGroupId,
actionVariables: alertType.actionVariables,
producer: alertType.producer,
}));
}
}

View file

@ -91,6 +91,7 @@ describe('create()', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
async executor() {},
producer: 'alerting',
});
});
@ -539,6 +540,7 @@ describe('create()', () => {
}),
},
async executor() {},
producer: 'alerting',
});
await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot(
`"params invalid: [param1]: expected value of type [string] but got [undefined]"`
@ -1896,6 +1898,7 @@ describe('update()', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
async executor() {},
producer: 'alerting',
});
});
@ -2438,6 +2441,7 @@ describe('update()', () => {
}),
},
async executor() {},
producer: 'alerting',
});
await expect(
alertsClient.update({
@ -2669,6 +2673,7 @@ describe('update()', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
async executor() {},
producer: 'alerting',
});
savedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: [

View file

@ -20,6 +20,7 @@ test('should return passed in params when validation not defined', () => {
],
defaultActionGroupId: 'default',
async executor() {},
producer: 'alerting',
},
{
foo: true,
@ -47,6 +48,7 @@ test('should validate and apply defaults when params is valid', () => {
}),
},
async executor() {},
producer: 'alerting',
},
{ param1: 'value' }
);
@ -75,6 +77,7 @@ test('should validate and throw error when params is invalid', () => {
}),
},
async executor() {},
producer: 'alerting',
},
{}
)

View file

@ -48,6 +48,7 @@ describe('listAlertTypesRoute', () => {
],
defaultActionGroupId: 'default',
actionVariables: [],
producer: 'test',
},
];
@ -67,6 +68,7 @@ describe('listAlertTypesRoute', () => {
"defaultActionGroupId": "default",
"id": "1",
"name": "name",
"producer": "test",
},
],
}
@ -109,6 +111,7 @@ describe('listAlertTypesRoute', () => {
],
defaultActionGroupId: 'default',
actionVariables: [],
producer: 'alerting',
},
];
@ -158,6 +161,7 @@ describe('listAlertTypesRoute', () => {
],
defaultActionGroupId: 'default',
actionVariables: [],
producer: 'alerting',
},
];

View file

@ -19,6 +19,7 @@ const alertType: AlertType = {
],
defaultActionGroupId: 'default',
executor: jest.fn(),
producer: 'alerting',
};
const createExecutionHandlerParams = {

View file

@ -25,6 +25,7 @@ const alertType = {
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
executor: jest.fn(),
producer: 'alerting',
};
let fakeTimer: sinon.SinonFakeTimers;

View file

@ -19,6 +19,7 @@ const alertType = {
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
executor: jest.fn(),
producer: 'alerting',
};
let fakeTimer: sinon.SinonFakeTimers;

View file

@ -78,6 +78,7 @@ export interface AlertType {
actionGroups: ActionGroup[];
defaultActionGroupId: ActionGroup['id'];
executor: ({ services, params, state }: AlertExecutorOptions) => Promise<State | void>;
producer: string;
actionVariables?: {
context?: ActionVariable[];
state?: ActionVariable[];

View file

@ -85,6 +85,7 @@ export function getAlertType(service: Service): AlertType {
],
},
executor,
producer: 'alerting',
};
async function executor(options: AlertExecutorOptions) {

View file

@ -24,7 +24,8 @@ export const ALERT_TYPES_CONFIG = {
})
}
],
defaultActionGroupId: 'threshold_met'
defaultActionGroupId: 'threshold_met',
producer: 'apm'
},
[AlertType.TransactionDuration]: {
name: i18n.translate('xpack.apm.transactionDurationAlert.name', {
@ -41,7 +42,8 @@ export const ALERT_TYPES_CONFIG = {
)
}
],
defaultActionGroupId: 'threshold_met'
defaultActionGroupId: 'threshold_met',
producer: 'apm'
}
};

View file

@ -115,7 +115,8 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
alertParamsExpression: ErrorRateAlertTrigger,
validate: () => ({
errors: []
})
}),
requiresAppContext: true
});
plugins.triggers_actions_ui.alertTypeRegistry.register({
@ -127,7 +128,8 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
alertParamsExpression: TransactionDurationAlertTrigger,
validate: () => ({
errors: []
})
}),
requiresAppContext: true
});
}
}

View file

@ -60,6 +60,7 @@ export function registerErrorRateAlertType({
}
]
},
producer: 'apm',
executor: async ({ services, params }) => {
const config = await config$.pipe(take(1)).toPromise();

View file

@ -74,6 +74,7 @@ export function registerTransactionDurationAlertType({
}
]
},
producer: 'apm',
executor: async ({ services, params }) => {
const config = await config$.pipe(take(1)).toPromise();

View file

@ -30,5 +30,6 @@ Reason:
`,
}
),
requiresAppContext: false,
};
}

View file

@ -30,5 +30,6 @@ Current value is \\{\\{context.valueOf.condition0\\}\\}
`,
}
),
requiresAppContext: false,
};
}

View file

@ -25,5 +25,6 @@ export function getAlertType(): AlertTypeModel {
defaultMessage: `\\{\\{context.matchingDocuments\\}\\} log entries have matched the following conditions: \\{\\{context.conditions\\}\\}`,
}
),
requiresAppContext: false,
};
}

View file

@ -41,6 +41,7 @@ export const registerMetricInventoryThresholdAlertType = (libs: InfraBackendLibs
},
defaultActionGroupId: FIRED_ACTIONS.id,
actionGroups: [FIRED_ACTIONS],
producer: 'metrics',
executor: curry(createInventoryMetricThresholdExecutor)(libs, uuid.v4()),
actionVariables: {
context: [

View file

@ -86,5 +86,6 @@ export async function registerLogThresholdAlertType(
{ name: 'conditions', description: conditionsActionVariableDescription },
],
},
producer: 'logs',
});
}

View file

@ -84,5 +84,6 @@ export function registerMetricThresholdAlertType(libs: InfraBackendLibs) {
{ name: 'reason', description: reasonActionVariableDescription },
],
},
producer: 'metrics',
};
}

View file

@ -38,6 +38,7 @@ export const getClusterState = (
}),
},
],
producer: 'monitoring',
defaultActionGroupId: 'default',
async executor({
services,

View file

@ -41,6 +41,7 @@ export const getLicenseExpiration = (
},
],
defaultActionGroupId: 'default',
producer: 'monitoring',
async executor({ services, params, state }: AlertCommonExecutorOptions): Promise<any> {
logger.debug(
`Firing alert with params: ${JSON.stringify(params)} and state: ${JSON.stringify(state)}`

View file

@ -25,6 +25,7 @@ export const rulesNotificationAlertType = ({
name: 'SIEM notification',
actionGroups: siemRuleActionGroups,
defaultActionGroupId: 'default',
producer: 'siem',
validate: {
params: schema.object({
ruleAlertId: schema.string(),

View file

@ -50,6 +50,7 @@ export const signalRulesAlertType = ({
validate: {
params: signalParamsSchema(),
},
producer: 'siem',
async executor({ previousStartedAt, alertId, services, params }) {
const {
anomalyThreshold,

View file

@ -71,6 +71,7 @@ export function getAlertType(): AlertTypeModel {
iconClass: 'alert',
alertParamsExpression: lazy(() => import('./index_threshold_expression')),
validate: validateAlertType,
requiresAppContext: false,
};
}
```
@ -254,6 +255,7 @@ Each alert type should be defined as `AlertTypeModel` object with the these prop
|validate|Validation function for the alert params.|
|alertParamsExpression| A lazy loaded React component for building UI of the current alert type params.|
|defaultActionMessage|Optional property for providing default message for all added actions with `message` property.|
|requiresAppContext|Define if alert type is enabled for create and edit in the alerting management UI.|
IMPORTANT: The current UI supports a single action group only.
Action groups are mapped from the server API result for [GET /api/alert/types: List alert types](https://github.com/elastic/kibana/tree/master/x-pack/legacy/plugins/alerting#get-apialerttypes-list-alert-types).
@ -267,6 +269,7 @@ export interface AlertType {
};
actionGroups: string[];
executor: ({ services, params, state }: AlertExecutorOptions) => Promise<State | void>;
requiresAppContext: boolean;
}
```
Only the default (which means first item of the array) action group is displayed in the current UI.
@ -313,6 +316,7 @@ export function getAlertType(): AlertTypeModel {
alertParamsExpression: lazy(() => import('./expression')),
validate: validateExampleAlertType,
defaultActionMessage: 'Alert [{{ctx.metadata.name}}] has exceeded the threshold',
requiresAppContext: false,
};
}
```

View file

@ -17,5 +17,6 @@ export function getAlertType(): AlertTypeModel<IndexThresholdAlertParams, Alerts
iconClass: 'alert',
alertParamsExpression: lazy(() => import('./expression')),
validate: validateExpression,
requiresAppContext: false,
};
}

View file

@ -183,5 +183,6 @@ function getAlertType(actionVariables: ActionVariables): AlertType {
actionVariables,
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
producer: 'alerting',
};
}

View file

@ -42,6 +42,7 @@ describe('loadAlertTypes', () => {
context: [{ name: 'var1', description: 'val1' }],
state: [{ name: 'var2', description: 'val2' }],
},
producer: 'alerting',
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
},

View file

@ -26,6 +26,7 @@ describe('action_form', () => {
return { errors: {} };
},
alertParamsExpression: () => <Fragment />,
requiresAppContext: false,
};
const actionType = {

View file

@ -7,7 +7,7 @@ import * as React from 'react';
import uuid from 'uuid';
import { shallow } from 'enzyme';
import { AlertDetails } from './alert_details';
import { Alert, ActionType, AlertTypeRegistryContract } from '../../../../types';
import { Alert, ActionType, ValidationResult } from '../../../../types';
import {
EuiTitle,
EuiBadge,
@ -27,17 +27,27 @@ jest.mock('../../../app_context', () => ({
http: jest.fn(),
capabilities: {
get: jest.fn(() => ({})),
siem: {
'alerting:show': true,
'alerting:save': true,
'alerting:delete': true,
},
},
actionTypeRegistry: jest.fn(),
alertTypeRegistry: jest.fn(() => {
const mocked: jest.Mocked<AlertTypeRegistryContract> = {
has: jest.fn(),
register: jest.fn(),
get: jest.fn(),
list: jest.fn(),
};
return mocked;
}),
alertTypeRegistry: {
has: jest.fn().mockReturnValue(true),
register: jest.fn(),
get: jest.fn().mockReturnValue({
id: 'my-alert-type',
iconClass: 'test',
name: 'test-alert',
validate: (): ValidationResult => {
return { errors: {} };
},
requiresAppContext: false,
}),
list: jest.fn(),
},
toastNotifications: mockes.notifications.toasts,
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
uiSettings: mockes.uiSettings,
@ -79,6 +89,7 @@ describe('alert_details', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: 'alerting',
};
expect(
@ -116,6 +127,7 @@ describe('alert_details', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: 'alerting',
};
expect(
@ -144,6 +156,7 @@ describe('alert_details', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: 'alerting',
};
const actionTypes: ActionType[] = [
@ -196,6 +209,7 @@ describe('alert_details', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: 'alerting',
};
const actionTypes: ActionType[] = [
{
@ -253,6 +267,7 @@ describe('alert_details', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: 'alerting',
};
expect(
@ -271,6 +286,7 @@ describe('alert_details', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: 'alerting',
};
expect(
@ -298,6 +314,7 @@ describe('disable button', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: 'alerting',
};
const enableButton = shallow(
@ -324,6 +341,7 @@ describe('disable button', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: 'alerting',
};
const enableButton = shallow(
@ -350,6 +368,7 @@ describe('disable button', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: 'alerting',
};
const disableAlert = jest.fn();
@ -385,6 +404,7 @@ describe('disable button', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: 'alerting',
};
const enableAlert = jest.fn();
@ -423,6 +443,7 @@ describe('mute button', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: 'alerting',
};
const enableButton = shallow(
@ -450,6 +471,7 @@ describe('mute button', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: 'alerting',
};
const enableButton = shallow(
@ -477,6 +499,7 @@ describe('mute button', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: 'alerting',
};
const muteAlert = jest.fn();
@ -513,6 +536,7 @@ describe('mute button', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: 'alerting',
};
const unmuteAlert = jest.fn();
@ -549,6 +573,7 @@ describe('mute button', () => {
actionGroups: [{ id: 'default', name: 'Default' }],
actionVariables: { context: [], state: [] },
defaultActionGroupId: 'default',
producer: 'alerting',
};
const enableButton = shallow(

View file

@ -72,8 +72,11 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
} = useAppDependencies();
const canSave = hasSaveAlertsCapability(capabilities);
const actionTypesByTypeId = indexBy(actionTypes, 'id');
const hasEditButton =
canSave && alertTypeRegistry.has(alert.alertTypeId)
? !alertTypeRegistry.get(alert.alertTypeId).requiresAppContext
: false;
const alertActions = alert.actions;
const uniqueActions = Array.from(new Set(alertActions.map((item: any) => item.actionTypeId)));
@ -113,7 +116,7 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
</EuiPageContentHeaderSection>
<EuiPageContentHeaderSection>
<EuiFlexGroup responsive={false} gutterSize="xs">
{canSave ? (
{hasEditButton ? (
<EuiFlexItem grow={false}>
<Fragment>
{' '}

View file

@ -71,6 +71,7 @@ describe('alert_add', () => {
return { errors: {} };
},
alertParamsExpression: TestExpression,
requiresAppContext: false,
};
const actionTypeModel = {

View file

@ -55,6 +55,7 @@ describe('alert_edit', () => {
return { errors: {} };
},
alertParamsExpression: () => <React.Fragment />,
requiresAppContext: false,
};
const actionTypeModel = {

View file

@ -15,6 +15,10 @@ import { AlertsContextProvider } from '../../context/alerts_context';
import { coreMock } from 'src/core/public/mocks';
const actionTypeRegistry = actionTypeRegistryMock.create();
const alertTypeRegistry = alertTypeRegistryMock.create();
jest.mock('../../lib/alert_api', () => ({
loadAlertTypes: jest.fn(),
}));
describe('alert_form', () => {
let deps: any;
const alertType = {
@ -25,6 +29,7 @@ describe('alert_form', () => {
return { errors: {} };
},
alertParamsExpression: () => <Fragment />,
requiresAppContext: false,
};
const actionType = {
@ -42,6 +47,17 @@ describe('alert_form', () => {
actionParamsFields: null,
};
const alertTypeNonEditable = {
id: 'non-edit-alert-type',
iconClass: 'test',
name: 'non edit alert',
validate: (): ValidationResult => {
return { errors: {} };
},
alertParamsExpression: () => <Fragment />,
requiresAppContext: true,
};
describe('alert_form create alert', () => {
let wrapper: ReactWrapper<any>;
@ -61,7 +77,7 @@ describe('alert_form', () => {
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
capabilities,
};
alertTypeRegistry.list.mockReturnValue([alertType]);
alertTypeRegistry.list.mockReturnValue([alertType, alertTypeNonEditable]);
alertTypeRegistry.has.mockReturnValue(true);
actionTypeRegistry.list.mockReturnValue([actionType]);
actionTypeRegistry.has.mockReturnValue(true);
@ -118,6 +134,14 @@ describe('alert_form', () => {
expect(alertTypeSelectOptions.exists()).toBeTruthy();
});
it('does not render registered alert type which non editable', async () => {
await setup();
const alertTypeSelectOptions = wrapper.find(
'[data-test-subj="non-edit-alert-type-SelectOption"]'
);
expect(alertTypeSelectOptions.exists()).toBeFalsy();
});
it('renders registered action types', async () => {
await setup();
const alertTypeSelectOptions = wrapper.find(
@ -127,6 +151,134 @@ describe('alert_form', () => {
});
});
describe('alert_form create alert non alerting consumer and producer', () => {
let wrapper: ReactWrapper<any>;
async function setup() {
const { loadAlertTypes } = jest.requireMock('../../lib/alert_api');
loadAlertTypes.mockResolvedValue([
{
id: 'other-consumer-producer-alert-type',
name: 'Test',
actionGroups: [
{
id: 'testActionGroup',
name: 'Test Action Group',
},
],
defaultActionGroupId: 'testActionGroup',
producer: 'alerting',
},
{
id: 'same-consumer-producer-alert-type',
name: 'Test',
actionGroups: [
{
id: 'testActionGroup',
name: 'Test Action Group',
},
],
defaultActionGroupId: 'testActionGroup',
producer: 'test',
},
]);
const mocks = coreMock.createSetup();
const [
{
application: { capabilities },
},
] = await mocks.getStartServices();
deps = {
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([
{
id: 'same-consumer-producer-alert-type',
iconClass: 'test',
name: 'test-alert',
validate: (): ValidationResult => {
return { errors: {} };
},
alertParamsExpression: () => <Fragment />,
requiresAppContext: true,
},
{
id: 'other-consumer-producer-alert-type',
iconClass: 'test',
name: 'test-alert',
validate: (): ValidationResult => {
return { errors: {} };
},
alertParamsExpression: () => <Fragment />,
requiresAppContext: false,
},
]);
alertTypeRegistry.has.mockReturnValue(true);
const initialAlert = ({
name: 'non alerting consumer test',
params: {},
consumer: 'test',
schedule: {
interval: '1m',
},
actions: [],
tags: [],
muteAll: false,
enabled: false,
mutedInstanceIds: [],
} as unknown) as Alert;
wrapper = mountWithIntl(
<AlertsContextProvider
value={{
reloadAlerts: () => {
return new Promise<void>(() => {});
},
http: deps!.http,
docLinks: deps.docLinks,
actionTypeRegistry: deps!.actionTypeRegistry,
alertTypeRegistry: deps!.alertTypeRegistry,
toastNotifications: deps!.toastNotifications,
uiSettings: deps!.uiSettings,
capabilities: deps!.capabilities,
}}
>
<AlertForm alert={initialAlert} dispatch={() => {}} errors={{ name: [], interval: [] }} />
</AlertsContextProvider>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(loadAlertTypes).toHaveBeenCalled();
}
it('renders alert type options which producer correspond to the alert consumer', async () => {
await setup();
const alertTypeSelectOptions = wrapper.find(
'[data-test-subj="same-consumer-producer-alert-type-SelectOption"]'
);
expect(alertTypeSelectOptions.exists()).toBeTruthy();
});
it('does not render alert type options which producer does not correspond to the alert consumer', async () => {
await setup();
const alertTypeSelectOptions = wrapper.find(
'[data-test-subj="other-consumer-producer-alert-type-SelectOption"]'
);
expect(alertTypeSelectOptions.exists()).toBeFalsy();
});
});
describe('alert_form edit alert', () => {
let wrapper: ReactWrapper<any>;

View file

@ -167,7 +167,21 @@ export const AlertForm = ({
? alertTypeModel.alertParamsExpression
: null;
const alertTypeNodes = alertTypeRegistry.list().map(function(item, index) {
const alertTypeRegistryList =
alert.consumer === 'alerting'
? alertTypeRegistry
.list()
.filter(
(alertTypeRegistryItem: AlertTypeModel) => !alertTypeRegistryItem.requiresAppContext
)
: alertTypeRegistry
.list()
.filter(
(alertTypeRegistryItem: AlertTypeModel) =>
alertTypesIndex &&
alertTypesIndex[alertTypeRegistryItem.id].producer === alert.consumer
);
const alertTypeNodes = alertTypeRegistryList.map(function(item, index) {
return (
<EuiKeyPadMenuItem
key={index}

View file

@ -44,6 +44,7 @@ const alertType = {
return { errors: {} };
},
alertParamsExpression: () => null,
requiresAppContext: false,
};
alertTypeRegistry.list.mockReturnValue([alertType]);
actionTypeRegistry.list.mockReturnValue([]);

View file

@ -20,6 +20,7 @@ const getTestAlertType = (id?: string, name?: string, iconClass?: string) => {
return { errors: {} };
},
alertParamsExpression: ExpressionComponent,
requiresAppContext: false,
};
};

View file

@ -99,6 +99,7 @@ export interface AlertType {
actionGroups: ActionGroup[];
actionVariables: ActionVariables;
defaultActionGroupId: ActionGroup['id'];
producer: string;
}
export type SanitizedAlertType = Omit<AlertType, 'apiKey'>;
@ -132,6 +133,7 @@ export interface AlertTypeModel<AlertParamsType = any, AlertsContextValue = any>
| React.LazyExoticComponent<
ComponentType<AlertTypeParamsExpressionProps<AlertParamsType, AlertsContextValue>>
>;
requiresAppContext: boolean;
defaultActionMessage?: string;
}

View file

@ -175,6 +175,7 @@ describe('monitor status alert type', () => {
"iconClass": "uptimeApp",
"id": "xpack.uptime.alerts.monitorStatus",
"name": <MonitorStatusTitle />,
"requiresAppContext": true,
"validate": [Function],
}
`);

View file

@ -68,4 +68,5 @@ export const initMonitorStatusAlertType: AlertTypeInitializer = ({
),
validate,
defaultActionMessage,
requiresAppContext: true,
});

View file

@ -20,4 +20,5 @@ export const initTlsAlertType: AlertTypeInitializer = (): AlertTypeModel => ({
name,
validate: () => ({ errors: {} }),
defaultActionMessage,
requiresAppContext: true,
});

View file

@ -171,6 +171,7 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) =
],
state: [...commonStateTranslations],
},
producer: 'uptime',
async executor(options: AlertExecutorOptions) {
const { params: rawParams } = options;
const decoded = StatusCheckExecutorParamsType.decode(rawParams);

View file

@ -100,6 +100,7 @@ export const tlsAlertFactory: UptimeAlertTypeFactory = (_server, libs) => ({
context: [],
state: [...tlsTranslations.actionVariables, ...commonStateTranslations],
},
producer: 'uptime',
async executor(options) {
const {
services: { alertInstanceFactory, callCluster, savedObjectsClient },

View file

@ -245,6 +245,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
{ id: 'default', name: 'Default' },
{ id: 'other', name: 'Other' },
],
producer: 'alerting',
defaultActionGroupId: 'default',
actionVariables: {
state: [{ name: 'instanceStateValue', description: 'the instance state value' }],
@ -304,6 +305,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
{ id: 'default', name: 'Default' },
{ id: 'other', name: 'Other' },
],
producer: 'alerting',
defaultActionGroupId: 'default',
async executor(alertExecutorOptions: AlertExecutorOptions) {
const { services, state } = alertExecutorOptions;
@ -332,6 +334,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
name: 'Default',
},
],
producer: 'alerting',
defaultActionGroupId: 'default',
async executor({ services, params, state }: AlertExecutorOptions) {
await services.callCluster('index', {
@ -358,6 +361,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
name: 'Default',
},
],
producer: 'alerting',
defaultActionGroupId: 'default',
async executor({ services, params, state }: AlertExecutorOptions) {
await services.callCluster('index', {
@ -383,6 +387,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
},
],
defaultActionGroupId: 'default',
producer: 'alerting',
validate: {
params: schema.object({
callClusterAuthorizationIndex: schema.string(),
@ -465,6 +470,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
name: 'Default',
},
],
producer: 'alerting',
defaultActionGroupId: 'default',
validate: {
params: schema.object({
@ -477,6 +483,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
id: 'test.noop',
name: 'Test: Noop',
actionGroups: [{ id: 'default', name: 'Default' }],
producer: 'alerting',
defaultActionGroupId: 'default',
async executor({ services, params, state }: AlertExecutorOptions) {},
};
@ -484,6 +491,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
id: 'test.onlyContextVariables',
name: 'Test: Only Context Variables',
actionGroups: [{ id: 'default', name: 'Default' }],
producer: 'alerting',
defaultActionGroupId: 'default',
actionVariables: {
context: [{ name: 'aContextVariable', description: 'this is a context variable' }],
@ -494,6 +502,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
id: 'test.onlyStateVariables',
name: 'Test: Only State Variables',
actionGroups: [{ id: 'default', name: 'Default' }],
producer: 'alerting',
defaultActionGroupId: 'default',
actionVariables: {
state: [{ name: 'aStateVariable', description: 'this is a state variable' }],

View file

@ -48,6 +48,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) {
state: [],
context: [],
},
producer: 'alerting',
});
break;
default:

View file

@ -27,6 +27,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) {
state: [],
context: [],
},
producer: 'alerting',
});
});

View file

@ -79,6 +79,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
it('can save alert', async () => {
await alerts.clickSaveAlertButton();
await pageObjects.common.closeToast();
});
it('posts an alert, verifies its presence, and deletes the alert', async () => {
@ -171,6 +172,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
it('can save alert', async () => {
await alerts.clickSaveAlertButton();
await pageObjects.common.closeToast();
});
it('has created a valid alert with expected parameters', async () => {

View file

@ -3,7 +3,7 @@
"version": "1.0.0",
"kibanaVersion": "kibana",
"configPath": ["xpack"],
"requiredPlugins": ["alerting"],
"requiredPlugins": ["alerting", "triggers_actions_ui"],
"server": true,
"ui": true
}

View file

@ -4,25 +4,50 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public';
import { PluginSetupContract as AlertingSetup } from '../../../../../../plugins/alerting/public';
import { AlertType, SanitizedAlert } from '../../../../../../plugins/alerting/common';
import { TriggersAndActionsUIPublicPluginSetup } from '../../../../../../plugins/triggers_actions_ui/public';
export type Setup = void;
export type Start = void;
export interface AlertingExamplePublicSetupDeps {
alerting: AlertingSetup;
triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup;
}
export class AlertingFixturePlugin implements Plugin<Setup, Start, AlertingExamplePublicSetupDeps> {
public setup(core: CoreSetup, { alerting }: AlertingExamplePublicSetupDeps) {
public setup(core: CoreSetup, { alerting, triggers_actions_ui }: AlertingExamplePublicSetupDeps) {
alerting.registerNavigation(
'consumer-noop',
'test.noop',
(alert: SanitizedAlert, alertType: AlertType) => `/alert/${alert.id}`
);
triggers_actions_ui.alertTypeRegistry.register({
id: 'test.always-firing',
name: 'Test Always Firing',
iconClass: 'alert',
alertParamsExpression: () => React.createElement('div', null, 'Test Always Firing'),
validate: () => {
return { errors: {} };
},
requiresAppContext: false,
});
triggers_actions_ui.alertTypeRegistry.register({
id: 'test.noop',
name: 'Test Noop',
iconClass: 'alert',
alertParamsExpression: () => React.createElement('div', null, 'Test Noop'),
validate: () => {
return { errors: {} };
},
requiresAppContext: false,
});
core.application.register({
id: 'consumer-noop',
title: 'No Op App',

View file

@ -32,6 +32,7 @@ function createNoopAlertType(alerting: AlertingSetup) {
actionGroups: [{ id: 'default', name: 'Default' }],
defaultActionGroupId: 'default',
async executor() {},
producer: 'alerting',
};
alerting.registerType(noopAlertType);
}
@ -45,6 +46,7 @@ function createAlwaysFiringAlertType(alerting: AlertingSetup) {
{ id: 'default', name: 'Default' },
{ id: 'other', name: 'Other' },
],
producer: 'alerting',
async executor(alertExecutorOptions: any) {
const { services, state, params } = alertExecutorOptions;