[Alerting] Enabling import of rules and connectors (#99857) (#100128)

* [Alerting] Enabling import of rules and connectors

* changed export to set pending executionStatus for rule

* fixed tests

* added docs

* Apply suggestions from code review

Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com>

* fixed docs

* fixed docs

* Update x-pack/plugins/alerting/server/saved_objects/get_import_warnings.ts

Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com>

* fixed test

* fixed test

Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com>

Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com>
This commit is contained in:
Yuliia Naumenko 2021-05-14 09:57:59 -07:00 committed by GitHub
parent b51c48c33e
commit 8df5efa83f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 182 additions and 17 deletions

View file

@ -111,6 +111,13 @@ image::images/connector-select-type.png[Connector select type]
=== Importing and exporting connectors
To import and export rules, use the <<managing-saved-objects, Saved Objects Management UI>>.
After a successful import, the proper banner is displayed:
[role="screenshot"]
image::images/coonectors-import-banner.png[Connectors import banner, width=50%]
If a connector is missing user sensitive information because of the import, a **Fix** button appears in the list view.
[role="screenshot"]
image::images/connectors-with-missing-secrets.png[Connectors with missing secrets]
[float]
[[create-connectors]]

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View file

@ -62,6 +62,9 @@ image:images/bulk-mute-disable.png[The Manage rules button lets you mute/unmute,
=== Importing and exporting rules
To import and export rules, use the <<managing-saved-objects, Saved Objects Management UI>>.
After the succesful import the proper banner will be displayed:
[role="screenshot"]
image::images/rules-imported-banner.png[Rules import banner, width=50%]
[float]
=== Required permissions

View file

@ -48,7 +48,7 @@ describe('getImportWarnings', () => {
const warnings = getImportWarnings(
(savedObjectConnectors as unknown) as Array<SavedObject<RawAction>>
);
expect(warnings[0].message).toBe('1 connector has secrets that require updates.');
expect(warnings[0].message).toBe('1 connector has sensitive information that require updates.');
});
it('does not return the warning message if all of the imported connectors do not have secrets to update', () => {

View file

@ -20,7 +20,7 @@ export function getImportWarnings(
}
const message = i18n.translate('xpack.actions.savedObjects.onImportText', {
defaultMessage:
'{connectorsWithSecretsLength} {connectorsWithSecretsLength, plural, one {connector has} other {connectors have}} secrets that require updates.',
'{connectorsWithSecretsLength} {connectorsWithSecretsLength, plural, one {connector has} other {connectors have}} sensitive information that require updates.',
values: {
connectorsWithSecretsLength: connectorsWithSecrets.length,
},
@ -35,4 +35,7 @@ export function getImportWarnings(
];
}
export const GO_TO_CONNECTORS_BUTTON_LABLE = 'Go to connectors';
export const GO_TO_CONNECTORS_BUTTON_LABLE = i18n.translate(
'xpack.actions.savedObjects.goToConnectorsButtonText',
{ defaultMessage: 'Go to connectors' }
);

View file

@ -36,8 +36,8 @@ export function setupSavedObjects(
management: {
defaultSearchField: 'name',
importableAndExportable: true,
getTitle(obj) {
return `Connector: [${obj.attributes.name}]`;
getTitle(savedObject: SavedObject<RawAction>) {
return `Connector: [${savedObject.attributes.name}]`;
},
onExport<RawAction>(
context: SavedObjectsExportTransformContext,

View file

@ -59,6 +59,7 @@ import { markApiKeyForInvalidation } from '../invalidate_pending_api_keys/mark_a
import { alertAuditEvent, AlertAuditAction } from './audit_events';
import { nodeBuilder } from '../../../../../src/plugins/data/common';
import { mapSortField } from './lib';
import { getAlertExecutionStatusPending } from '../lib/alert_execution_status';
export interface RegistryAlertTypeWithAuth extends RegistryAlertType {
authorizedConsumers: string[];
@ -288,11 +289,7 @@ export class AlertsClient {
muteAll: false,
mutedInstanceIds: [],
notifyWhen,
executionStatus: {
status: 'pending',
lastExecutionDate: new Date().toISOString(),
error: null,
},
executionStatus: getAlertExecutionStatusPending(new Date().toISOString()),
};
this.auditLogger?.log(

View file

@ -9,6 +9,7 @@ import { Logger } from 'src/core/server';
import { AlertTaskState, AlertExecutionStatus, RawAlertExecutionStatus } from '../types';
import { getReasonFromError } from './error_with_reason';
import { getEsErrorMessage } from './errors';
import { AlertExecutionStatuses } from '../../common';
export function executionStatusFromState(state: AlertTaskState): AlertExecutionStatus {
const instanceIds = Object.keys(state.alertInstances ?? {});
@ -66,3 +67,9 @@ export function alertExecutionStatusFromRaw(
return { lastExecutionDate: parsedDate, status };
}
}
export const getAlertExecutionStatusPending = (lastExecutionDate: string) => ({
status: 'pending' as AlertExecutionStatuses,
lastExecutionDate,
error: null,
});

View file

@ -0,0 +1,87 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { SavedObject } from 'kibana/server';
import { RawAlert } from '../types';
import { getImportWarnings } from './get_import_warnings';
describe('getImportWarnings', () => {
it('return warning message with total imported rules that have to be enabled', () => {
const savedObjectRules = [
{
id: '1',
type: 'alert',
attributes: {
enabled: true,
name: 'rule-name1',
tags: ['tag-1', 'tag-2'],
alertTypeId: '123',
consumer: 'alert-consumer',
schedule: { interval: '1m' },
actions: [],
params: {},
createdBy: 'me',
updatedBy: 'me',
apiKey: '4tndskbuhewotw4klrhgjewrt9u',
apiKeyOwner: 'me',
throttle: null,
notifyWhen: 'onActionGroupChange',
muteAll: false,
mutedInstanceIds: [],
executionStatus: {
status: 'active',
lastExecutionDate: '2020-08-20T19:23:38Z',
error: null,
},
scheduledTaskId: '2q5tjbf3q45twer',
},
references: [],
},
{
id: '2',
type: 'alert',
attributes: {
enabled: true,
name: 'rule-name2',
tags: [],
alertTypeId: '123',
consumer: 'alert-consumer',
schedule: { interval: '1m' },
actions: [],
params: {},
createdBy: 'me',
updatedBy: 'me',
apiKey: '4tndskbuhewotw4klrhgjewrt9u',
apiKeyOwner: 'me',
throttle: null,
notifyWhen: 'onActionGroupChange',
muteAll: false,
mutedInstanceIds: [],
executionStatus: {
status: 'pending',
lastExecutionDate: '2020-08-20T19:23:38Z',
error: null,
},
scheduledTaskId: '123',
},
references: [],
},
];
const warnings = getImportWarnings(
(savedObjectRules as unknown) as Array<SavedObject<RawAlert>>
);
expect(warnings[0].message).toBe('2 rules must be enabled after the import.');
});
it('return no warning messages if no rules were imported', () => {
const savedObjectRules = [] as Array<SavedObject<RawAlert>>;
const warnings = getImportWarnings(
(savedObjectRules as unknown) as Array<SavedObject<RawAlert>>
);
expect(warnings.length).toBe(0);
});
});

View file

@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { SavedObject, SavedObjectsImportWarning } from 'kibana/server';
export function getImportWarnings(
rulesSavedObjects: Array<SavedObject<unknown>>
): SavedObjectsImportWarning[] {
if (rulesSavedObjects.length === 0) {
return [];
}
const message = i18n.translate('xpack.alerting.savedObjects.onImportText', {
defaultMessage:
'{rulesSavedObjectsLength} {rulesSavedObjectsLength, plural, one {rule} other {rules}} must be enabled after the import.',
values: {
rulesSavedObjectsLength: rulesSavedObjects.length,
},
});
return [
{
type: 'action_required',
message,
actionPath: '/app/management/insightsAndAlerting/triggersActions/rules',
buttonLabel: GO_TO_RULES_BUTTON_LABLE,
} as SavedObjectsImportWarning,
];
}
export const GO_TO_RULES_BUTTON_LABLE = i18n.translate(
'xpack.alerting.savedObjects.goToRulesButtonText',
{ defaultMessage: 'Go to rules' }
);

View file

@ -14,6 +14,8 @@ import mappings from './mappings.json';
import { getMigrations } from './migrations';
import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server';
import { transformRulesForExport } from './transform_rule_for_export';
import { RawAlert } from '../types';
import { getImportWarnings } from './get_import_warnings';
export { partiallyUpdateAlert } from './partially_update_alert';
export const AlertAttributesExcludedFromAAD = [
@ -49,8 +51,13 @@ export function setupSavedObjects(
mappings: mappings.alert,
management: {
importableAndExportable: true,
getTitle(obj) {
return `Rule: [${obj.attributes.name}]`;
getTitle(ruleSavedObject: SavedObject<RawAlert>) {
return `Rule: [${ruleSavedObject.attributes.name}]`;
},
onImport(ruleSavedObjects) {
return {
warnings: getImportWarnings(ruleSavedObjects),
};
},
onExport<RawAlert>(
context: SavedObjectsExportTransformContext,

View file

@ -6,7 +6,13 @@
*/
import { transformRulesForExport } from './transform_rule_for_export';
jest.mock('../lib/alert_execution_status', () => ({
getAlertExecutionStatusPending: () => ({
status: 'pending',
lastExecutionDate: '2020-08-20T19:23:38Z',
error: null,
}),
}));
describe('transform rule for export', () => {
const date = new Date().toISOString();
const mockRules = [
@ -84,6 +90,11 @@ describe('transform rule for export', () => {
apiKey: null,
apiKeyOwner: null,
scheduledTaskId: null,
executionStatus: {
status: 'pending',
lastExecutionDate: '2020-08-20T19:23:38Z',
error: null,
},
},
}))
);

View file

@ -6,13 +6,18 @@
*/
import { SavedObject } from 'kibana/server';
import { getAlertExecutionStatusPending } from '../lib/alert_execution_status';
import { RawAlert } from '../types';
export function transformRulesForExport(rules: SavedObject[]): Array<SavedObject<RawAlert>> {
return rules.map((rule) => transformRuleForExport(rule as SavedObject<RawAlert>));
const exportDate = new Date().toISOString();
return rules.map((rule) => transformRuleForExport(rule as SavedObject<RawAlert>, exportDate));
}
function transformRuleForExport(rule: SavedObject<RawAlert>): SavedObject<RawAlert> {
function transformRuleForExport(
rule: SavedObject<RawAlert>,
exportDate: string
): SavedObject<RawAlert> {
return {
...rule,
attributes: {
@ -21,6 +26,7 @@ function transformRuleForExport(rule: SavedObject<RawAlert>): SavedObject<RawAle
apiKey: null,
apiKeyOwner: null,
scheduledTaskId: null,
executionStatus: getAlertExecutionStatusPending(exportDate),
},
};
}

View file

@ -80,7 +80,7 @@ export const AddConnectorInline = ({
<EuiText color="danger">
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertForm.unableToLoadConnectorTitle"
defaultMessage="Unable to load connector."
defaultMessage="Unable to load connector"
/>
</EuiText>
);
@ -196,7 +196,7 @@ export const AddConnectorInline = ({
data-test-subj={`alertActionAccordionErrorTooltip`}
content={
<FormattedMessage
defaultMessage="Unable to load connector."
defaultMessage="Unable to load connector"
id="xpack.triggersActionsUI.sections.alertForm.unableToLoadConnectorTitle'"
/>
}