mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Alerting] Export rules and connectors (#98802)
* Adding importableAndExportable but hidden saved object types to saved object feature privilege * Adding helper function for transforming rule for export. Added audit logging * Adding helper function for transforming rule for export. Added audit logging * Adding unit test for transforming rules for export * Exporting connectors * Removing auditing during export * Adding import/export to docs * PR fixes * Using action type validation onExport * Fixing logic for connectors with optional secrets * Fixing logic for connectors with optional secrets Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
0d7a5826a4
commit
4ab86c77d4
11 changed files with 495 additions and 7 deletions
|
@ -106,6 +106,12 @@ New connectors can be created by clicking the *Create connector* button, which w
|
|||
[role="screenshot"]
|
||||
image::images/connector-select-type.png[Connector select type]
|
||||
|
||||
[float]
|
||||
[[importing-and-exporting-connectors]]
|
||||
=== Importing and exporting connectors
|
||||
|
||||
To import and export rules, use the <<managing-saved-objects, Saved Objects Management UI>>.
|
||||
|
||||
[float]
|
||||
[[create-connectors]]
|
||||
=== Preconfigured connectors
|
||||
|
|
|
@ -57,6 +57,12 @@ These operations can also be performed in bulk by multi-selecting rules and clic
|
|||
[role="screenshot"]
|
||||
image:images/bulk-mute-disable.png[The Manage rules button lets you mute/unmute, enable/disable, and delete in bulk]
|
||||
|
||||
[float]
|
||||
[[importing-and-exporting-rules]]
|
||||
=== Importing and exporting rules
|
||||
|
||||
To import and export rules, use the <<managing-saved-objects, Saved Objects Management UI>>.
|
||||
|
||||
[float]
|
||||
=== Required permissions
|
||||
|
||||
|
|
|
@ -181,7 +181,6 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
|
|||
}
|
||||
|
||||
plugins.features.registerKibanaFeature(ACTIONS_FEATURE);
|
||||
setupSavedObjects(core.savedObjects, plugins.encryptedSavedObjects);
|
||||
|
||||
this.eventLogService = plugins.eventLog;
|
||||
plugins.eventLog.registerProviderActions(EVENT_LOG_PROVIDER, Object.values(EVENT_LOG_ACTIONS));
|
||||
|
@ -228,6 +227,8 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
|
|||
this.actionExecutor = actionExecutor;
|
||||
this.security = plugins.security;
|
||||
|
||||
setupSavedObjects(core.savedObjects, plugins.encryptedSavedObjects, this.actionTypeRegistry!);
|
||||
|
||||
registerBuiltInActionTypes({
|
||||
logger: this.logger,
|
||||
actionTypeRegistry,
|
||||
|
|
|
@ -5,12 +5,18 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SavedObject, SavedObjectsServiceSetup } from 'kibana/server';
|
||||
import {
|
||||
SavedObject,
|
||||
SavedObjectsExportTransformContext,
|
||||
SavedObjectsServiceSetup,
|
||||
} from 'kibana/server';
|
||||
import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server';
|
||||
import mappings from './mappings.json';
|
||||
import { getMigrations } from './migrations';
|
||||
import { RawAction } from '../types';
|
||||
import { getImportResultMessage, GO_TO_CONNECTORS_BUTTON_LABLE } from './get_import_result_message';
|
||||
import { transformConnectorsForExport } from './transform_connectors_for_export';
|
||||
import { ActionTypeRegistry } from '../action_type_registry';
|
||||
|
||||
export const ACTION_SAVED_OBJECT_TYPE = 'action';
|
||||
export const ALERT_SAVED_OBJECT_TYPE = 'alert';
|
||||
|
@ -18,7 +24,8 @@ export const ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE = 'action_task_params';
|
|||
|
||||
export function setupSavedObjects(
|
||||
savedObjects: SavedObjectsServiceSetup,
|
||||
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup
|
||||
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup,
|
||||
actionTypeRegistry: ActionTypeRegistry
|
||||
) {
|
||||
savedObjects.registerType({
|
||||
name: ACTION_SAVED_OBJECT_TYPE,
|
||||
|
@ -32,6 +39,12 @@ export function setupSavedObjects(
|
|||
getTitle(obj) {
|
||||
return `Connector: [${obj.attributes.name}]`;
|
||||
},
|
||||
onExport<RawAction>(
|
||||
context: SavedObjectsExportTransformContext,
|
||||
objects: Array<SavedObject<RawAction>>
|
||||
) {
|
||||
return transformConnectorsForExport(objects, actionTypeRegistry);
|
||||
},
|
||||
onImport(connectors) {
|
||||
return {
|
||||
warnings: [
|
||||
|
|
|
@ -0,0 +1,253 @@
|
|||
/*
|
||||
* 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 { transformConnectorsForExport } from './transform_connectors_for_export';
|
||||
import { ActionTypeRegistry, ActionTypeRegistryOpts } from '../action_type_registry';
|
||||
import { loggingSystemMock } from '../../../../../src/core/server/mocks';
|
||||
import { actionsConfigMock } from '../actions_config.mock';
|
||||
import { licensingMock } from '../../../licensing/server/mocks';
|
||||
import { licenseStateMock } from '../lib/license_state.mock';
|
||||
import { taskManagerMock } from '../../../task_manager/server/mocks';
|
||||
import { ActionExecutor, TaskRunnerFactory } from '../lib';
|
||||
import { registerBuiltInActionTypes } from '../builtin_action_types';
|
||||
|
||||
describe('transform connector for export', () => {
|
||||
const actionTypeRegistryParams: ActionTypeRegistryOpts = {
|
||||
licensing: licensingMock.createSetup(),
|
||||
taskManager: taskManagerMock.createSetup(),
|
||||
taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor({ isESOCanEncrypt: true })),
|
||||
actionsConfigUtils: actionsConfigMock.create(),
|
||||
licenseState: licenseStateMock.create(),
|
||||
preconfiguredActions: [],
|
||||
};
|
||||
const actionTypeRegistry: ActionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
|
||||
|
||||
registerBuiltInActionTypes({
|
||||
logger: loggingSystemMock.create().get(),
|
||||
actionTypeRegistry,
|
||||
actionsConfigUtils: actionsConfigMock.create(),
|
||||
});
|
||||
|
||||
const connectorsWithNoSecrets = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: '.email',
|
||||
name: 'email connector without auth',
|
||||
isMissingSecrets: false,
|
||||
config: {
|
||||
hasAuth: false,
|
||||
from: 'me@me.com',
|
||||
host: 'host',
|
||||
port: 22,
|
||||
service: null,
|
||||
secure: null,
|
||||
},
|
||||
secrets: 'asbqw4tqbef',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: '.index',
|
||||
name: 'index connector',
|
||||
isMissingSecrets: false,
|
||||
config: {
|
||||
index: 'test-index',
|
||||
refresh: false,
|
||||
executionTimeField: null,
|
||||
},
|
||||
secrets: 'asbqw4tqbef',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: '.server-log',
|
||||
name: 'server log connector',
|
||||
isMissingSecrets: false,
|
||||
config: {},
|
||||
secrets: 'asbqw4tqbef',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: '.webhook',
|
||||
name: 'webhook connector without auth',
|
||||
isMissingSecrets: false,
|
||||
config: {
|
||||
method: 'post',
|
||||
hasAuth: false,
|
||||
url: 'https://webhook',
|
||||
headers: {},
|
||||
},
|
||||
secrets: 'asbqw4tqbef',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
];
|
||||
const connectorsWithSecrets = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: '.email',
|
||||
name: 'email connector with auth',
|
||||
isMissingSecrets: false,
|
||||
config: {
|
||||
hasAuth: true,
|
||||
from: 'me@me.com',
|
||||
host: 'host',
|
||||
port: 22,
|
||||
service: null,
|
||||
secure: null,
|
||||
},
|
||||
secrets: 'asbqw4tqbef',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: '.resilient',
|
||||
name: 'resilient connector',
|
||||
isMissingSecrets: false,
|
||||
config: {
|
||||
apiUrl: 'https://resilient',
|
||||
orgId: 'origId',
|
||||
},
|
||||
secrets: 'asbqw4tqbef',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: '.servicenow',
|
||||
name: 'servicenow itsm connector',
|
||||
isMissingSecrets: false,
|
||||
config: {
|
||||
apiUrl: 'https://servicenow',
|
||||
},
|
||||
secrets: 'asbqw4tqbef',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: '.pagerduty',
|
||||
name: 'pagerduty connector',
|
||||
isMissingSecrets: false,
|
||||
config: {
|
||||
apiUrl: 'https://pagerduty',
|
||||
},
|
||||
secrets: 'asbqw4tqbef',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: '.jira',
|
||||
name: 'jira connector',
|
||||
isMissingSecrets: false,
|
||||
config: {
|
||||
apiUrl: 'https://jira',
|
||||
projectKey: 'foo',
|
||||
},
|
||||
secrets: 'asbqw4tqbef',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: '.teams',
|
||||
name: 'teams connector',
|
||||
isMissingSecrets: false,
|
||||
config: {},
|
||||
secrets: 'asbqw4tqbef',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: '.slack',
|
||||
name: 'slack connector',
|
||||
isMissingSecrets: false,
|
||||
config: {},
|
||||
secrets: 'asbqw4tqbef',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: '.servicenow-sir',
|
||||
name: 'servicenow sir connector',
|
||||
isMissingSecrets: false,
|
||||
config: {
|
||||
apiUrl: 'https://servicenow-sir',
|
||||
},
|
||||
secrets: 'asbqw4tqbef',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: '.webhook',
|
||||
name: 'webhook connector with auth',
|
||||
isMissingSecrets: false,
|
||||
config: {
|
||||
method: 'post',
|
||||
hasAuth: true,
|
||||
url: 'https://webhook',
|
||||
headers: {},
|
||||
},
|
||||
secrets: 'asbqw4tqbef',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
];
|
||||
|
||||
it('should not change connectors without secrets', () => {
|
||||
expect(transformConnectorsForExport(connectorsWithNoSecrets, actionTypeRegistry)).toEqual(
|
||||
connectorsWithNoSecrets
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove secrets for connectors with secrets', () => {
|
||||
expect(transformConnectorsForExport(connectorsWithSecrets, actionTypeRegistry)).toEqual(
|
||||
connectorsWithSecrets.map((connector) => ({
|
||||
...connector,
|
||||
attributes: {
|
||||
...connector.attributes,
|
||||
isMissingSecrets: true,
|
||||
},
|
||||
}))
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 { ActionTypeRegistry } from '../action_type_registry';
|
||||
import { validateSecrets } from '../lib';
|
||||
import { RawAction, ActionType } from '../types';
|
||||
|
||||
export function transformConnectorsForExport(
|
||||
connectors: SavedObject[],
|
||||
actionTypeRegistry: ActionTypeRegistry
|
||||
): Array<SavedObject<RawAction>> {
|
||||
return connectors.map((c) => {
|
||||
const connector = c as SavedObject<RawAction>;
|
||||
return transformConnectorForExport(
|
||||
connector,
|
||||
actionTypeRegistry.get(connector.attributes.actionTypeId)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function transformConnectorForExport(
|
||||
connector: SavedObject<RawAction>,
|
||||
actionType: ActionType
|
||||
): SavedObject<RawAction> {
|
||||
let isMissingSecrets = false;
|
||||
try {
|
||||
// If connector requires secrets, this will throw an error
|
||||
validateSecrets(actionType, {});
|
||||
|
||||
// If connector has optional (or no) secrets, set isMissingSecrets value to value of hasAuth
|
||||
// If connector doesn't have hasAuth value, default to isMissingSecrets: false
|
||||
isMissingSecrets = (connector?.attributes?.config?.hasAuth as boolean) ?? false;
|
||||
} catch (err) {
|
||||
isMissingSecrets = true;
|
||||
}
|
||||
|
||||
// Skip connectors
|
||||
return {
|
||||
...connector,
|
||||
attributes: {
|
||||
...connector.attributes,
|
||||
isMissingSecrets,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -5,11 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SavedObjectsServiceSetup } from 'kibana/server';
|
||||
import {
|
||||
SavedObject,
|
||||
SavedObjectsExportTransformContext,
|
||||
SavedObjectsServiceSetup,
|
||||
} from 'kibana/server';
|
||||
import mappings from './mappings.json';
|
||||
import { getMigrations } from './migrations';
|
||||
import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server';
|
||||
|
||||
import { transformRulesForExport } from './transform_rule_for_export';
|
||||
export { partiallyUpdateAlert } from './partially_update_alert';
|
||||
|
||||
export const AlertAttributesExcludedFromAAD = [
|
||||
|
@ -43,6 +47,18 @@ export function setupSavedObjects(
|
|||
namespaceType: 'single',
|
||||
migrations: getMigrations(encryptedSavedObjects),
|
||||
mappings: mappings.alert,
|
||||
management: {
|
||||
importableAndExportable: true,
|
||||
getTitle(obj) {
|
||||
return `Rule: [${obj.attributes.name}]`;
|
||||
},
|
||||
onExport<RawAlert>(
|
||||
context: SavedObjectsExportTransformContext,
|
||||
objects: Array<SavedObject<RawAlert>>
|
||||
) {
|
||||
return transformRulesForExport(objects);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
savedObjects.registerType({
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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 { transformRulesForExport } from './transform_rule_for_export';
|
||||
|
||||
describe('transform rule for export', () => {
|
||||
const date = new Date().toISOString();
|
||||
const mockRules = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
enabled: true,
|
||||
name: 'rule-name',
|
||||
tags: ['tag-1', 'tag-2'],
|
||||
alertTypeId: '123',
|
||||
consumer: 'alert-consumer',
|
||||
schedule: { interval: '1m' },
|
||||
actions: [],
|
||||
params: {},
|
||||
createdBy: 'me',
|
||||
updatedBy: 'me',
|
||||
createdAt: date,
|
||||
updatedAt: date,
|
||||
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: false,
|
||||
name: 'disabled-rule',
|
||||
tags: ['tag-1'],
|
||||
alertTypeId: '456',
|
||||
consumer: 'alert-consumer',
|
||||
schedule: { interval: '1h' },
|
||||
actions: [],
|
||||
params: {},
|
||||
createdBy: 'you',
|
||||
updatedBy: 'you',
|
||||
createdAt: date,
|
||||
updatedAt: date,
|
||||
apiKey: null,
|
||||
apiKeyOwner: null,
|
||||
throttle: null,
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
status: 'pending',
|
||||
lastExecutionDate: '2020-08-20T19:23:38Z',
|
||||
error: null,
|
||||
},
|
||||
scheduledTaskId: null,
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
];
|
||||
|
||||
it('should disable rule and clear sensitive values', () => {
|
||||
expect(transformRulesForExport(mockRules)).toEqual(
|
||||
mockRules.map((rule) => ({
|
||||
...rule,
|
||||
attributes: {
|
||||
...rule.attributes,
|
||||
enabled: false,
|
||||
apiKey: null,
|
||||
apiKeyOwner: null,
|
||||
scheduledTaskId: null,
|
||||
},
|
||||
}))
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
export function transformRulesForExport(rules: SavedObject[]): Array<SavedObject<RawAlert>> {
|
||||
return rules.map((rule) => transformRuleForExport(rule as SavedObject<RawAlert>));
|
||||
}
|
||||
|
||||
function transformRuleForExport(rule: SavedObject<RawAlert>): SavedObject<RawAlert> {
|
||||
return {
|
||||
...rule,
|
||||
attributes: {
|
||||
...rule.attributes,
|
||||
enabled: false,
|
||||
apiKey: null,
|
||||
apiKeyOwner: null,
|
||||
scheduledTaskId: null,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -27,6 +27,20 @@ describe('Features Plugin', () => {
|
|||
namespaceType: 'single' as 'single',
|
||||
},
|
||||
]);
|
||||
typeRegistry.getImportableAndExportableTypes.mockReturnValue([
|
||||
{
|
||||
name: 'hidden-importableAndExportable',
|
||||
hidden: true,
|
||||
mappings: { properties: {} },
|
||||
namespaceType: 'single' as 'single',
|
||||
},
|
||||
{
|
||||
name: 'not-hidden-importableAndExportable',
|
||||
hidden: false,
|
||||
mappings: { properties: {} },
|
||||
namespaceType: 'single' as 'single',
|
||||
},
|
||||
]);
|
||||
coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry);
|
||||
});
|
||||
|
||||
|
@ -87,7 +101,9 @@ describe('Features Plugin', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
it('registers kibana features with not hidden saved objects types', async () => {
|
||||
it('registers kibana features with visible saved objects types and hidden saved object types that are importable and exportable', async () => {
|
||||
typeRegistry.isHidden.mockReturnValueOnce(true);
|
||||
typeRegistry.isHidden.mockReturnValueOnce(false);
|
||||
const plugin = new FeaturesPlugin(initContext);
|
||||
await plugin.setup(coreSetup, {});
|
||||
const { getKibanaFeatures } = plugin.start(coreStart);
|
||||
|
@ -98,6 +114,8 @@ describe('Features Plugin', () => {
|
|||
|
||||
expect(soTypes.includes('foo')).toBe(true);
|
||||
expect(soTypes.includes('bar')).toBe(false);
|
||||
expect(soTypes.includes('hidden-importableAndExportable')).toBe(true);
|
||||
expect(soTypes.includes('not-hidden-importableAndExportable')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns registered elasticsearch features', async () => {
|
||||
|
|
|
@ -128,7 +128,15 @@ export class FeaturesPlugin
|
|||
|
||||
private registerOssFeatures(savedObjects: SavedObjectsServiceStart) {
|
||||
const registry = savedObjects.getTypeRegistry();
|
||||
const savedObjectTypes = registry.getVisibleTypes().map((t) => t.name);
|
||||
const savedObjectVisibleTypes = registry.getVisibleTypes().map((t) => t.name);
|
||||
const savedObjectImportableAndExportableHiddenTypes = registry
|
||||
.getImportableAndExportableTypes()
|
||||
.filter((t) => registry.isHidden(t.name))
|
||||
.map((t) => t.name);
|
||||
|
||||
const savedObjectTypes = Array.from(
|
||||
new Set([...savedObjectVisibleTypes, ...savedObjectImportableAndExportableHiddenTypes])
|
||||
);
|
||||
|
||||
this.logger.debug(
|
||||
`Registering OSS features with SO types: ${savedObjectTypes.join(', ')}. "includeTimelion": ${
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue