mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
Merge branch 'main' into renovate/main-minify
This commit is contained in:
commit
abd155da66
306 changed files with 11107 additions and 3871 deletions
|
@ -46955,7 +46955,6 @@ paths:
|
|||
summary: Invalidate user sessions
|
||||
tags:
|
||||
- user session
|
||||
x-state: Technical Preview
|
||||
/api/short_url:
|
||||
post:
|
||||
description: |
|
||||
|
|
|
@ -1287,7 +1287,7 @@
|
|||
"pretty-ms": "6.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"proxy-from-env": "1.1.0",
|
||||
"puppeteer": "24.8.1",
|
||||
"puppeteer": "24.10.1",
|
||||
"query-string": "^6.13.2",
|
||||
"rbush": "^4.0.1",
|
||||
"re-resizable": "^6.11.2",
|
||||
|
@ -1912,7 +1912,7 @@
|
|||
"regenerate": "^1.4.0",
|
||||
"resolve": "^1.22.0",
|
||||
"rxjs-marbles": "^7.0.1",
|
||||
"sass-embedded": "^1.78.0",
|
||||
"sass-embedded": "^1.79.6",
|
||||
"sass-loader": "^10.5.2",
|
||||
"selenium-webdriver": "^4.33.0",
|
||||
"sharp": "0.32.6",
|
||||
|
|
|
@ -991,6 +991,11 @@
|
|||
"updated",
|
||||
"version"
|
||||
],
|
||||
"security:reference-data": [
|
||||
"id",
|
||||
"owner",
|
||||
"type"
|
||||
],
|
||||
"siem-detection-engine-rule-actions": [
|
||||
"actions",
|
||||
"actions.actionRef",
|
||||
|
|
|
@ -1079,7 +1079,7 @@
|
|||
"managed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"matchers": {
|
||||
"matchers": {
|
||||
"dynamic": false,
|
||||
"type": "object"
|
||||
},
|
||||
|
@ -3219,6 +3219,20 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"security:reference-data": {
|
||||
"dynamic": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"owner": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"type": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"siem-detection-engine-rule-actions": {
|
||||
"properties": {
|
||||
"actions": {
|
||||
|
|
|
@ -88,6 +88,23 @@ export const renderingOverrides = (euiTheme: UseEuiTheme['euiTheme']) => css`
|
|||
--kbnAppHeadersOffset: var(--euiFixedHeadersOffset, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Due to pure HTML and the scope being large, we decided to temporarily apply following 3 style blocks globally.
|
||||
// TODO: refactor within github issue #223571
|
||||
|
||||
// Styles applied to the span.ffArray__highlight from FieldFormat class that is used to visually distinguish array delimiters when rendering array values as HTML in Kibana field formatters
|
||||
.ffArray__highlight {
|
||||
color: ${euiTheme.colors.mediumShade};
|
||||
}
|
||||
|
||||
// Styles applied to the span.ffString__emptyValue from FieldFormat class that is used to visually distinguish empty string values when rendering string values as HTML in Kibana field formatters
|
||||
.ffString__emptyValue {
|
||||
color: ${euiTheme.colors.darkShade};
|
||||
}
|
||||
|
||||
.lnsTableCell--colored .ffString__emptyValue {
|
||||
color: unset;
|
||||
}
|
||||
`;
|
||||
|
||||
export const bannerStyles = (euiTheme: UseEuiTheme['euiTheme']) => css`
|
||||
|
|
|
@ -11,4 +11,4 @@ export { registerCoreObjectTypes } from './registration';
|
|||
|
||||
// set minimum number of registered saved objects to ensure no object types are removed after 8.8
|
||||
// declared in internal implementation explicitly to prevent unintended changes.
|
||||
export const SAVED_OBJECT_TYPES_COUNT = 134 as const;
|
||||
export const SAVED_OBJECT_TYPES_COUNT = 135 as const;
|
||||
|
|
|
@ -166,6 +166,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
|
|||
"security-ai-prompt": "cc8ee5aaa9d001e89c131bbd5af6bc80bc271046",
|
||||
"security-rule": "07abb4d7e707d91675ec0495c73816394c7b521f",
|
||||
"security-solution-signals-migration": "9d99715fe5246f19de2273ba77debd2446c36bb1",
|
||||
"security:reference-data": "66f0523d6ae9c51536bd861c5593e49fd852dff5",
|
||||
"siem-detection-engine-rule-actions": "54f08e23887b20da7c805fab7c60bc67c428aff9",
|
||||
"siem-ui-timeline": "d3de8ff3617be8f2a799d66b1471b9be6124bf40",
|
||||
"siem-ui-timeline-note": "0a32fb776907f596bedca292b8c646496ae9c57b",
|
||||
|
|
|
@ -133,6 +133,7 @@ const previouslyRegisteredTypes = [
|
|||
'security-ai-prompt',
|
||||
'security-rule',
|
||||
'security-solution-signals-migration',
|
||||
'security:reference-data',
|
||||
'risk-engine-configuration',
|
||||
'entity-engine-status',
|
||||
'server',
|
||||
|
|
|
@ -95,5 +95,5 @@ export const LICENSE_OVERRIDES = {
|
|||
'@elastic/eui-theme-borealis@1.1.0': ['Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0'],
|
||||
'language-subtag-registry@0.3.21': ['CC-BY-4.0'], // retired ODC‑By license https://github.com/mattcg/language-subtag-registry
|
||||
'buffers@0.1.1': ['MIT'], // license in importing module https://www.npmjs.com/package/binary
|
||||
'@bufbuild/protobuf@1.2.1': ['Apache-2.0'], // license (Apache-2.0 AND BSD-3-Clause)
|
||||
'@bufbuild/protobuf@2.5.2': ['Apache-2.0'], // license (Apache-2.0 AND BSD-3-Clause)
|
||||
};
|
||||
|
|
|
@ -48,10 +48,10 @@ export class ChromiumArchivePaths {
|
|||
platform: 'darwin',
|
||||
architecture: 'x64',
|
||||
archiveFilename: 'chrome-headless-shell-mac-x64.zip',
|
||||
archiveChecksum: '772b1fec39826e0604d21e515ee27c47bdfb7a5d69e8674d818b0c159c3dfcb2',
|
||||
binaryChecksum: '493f63172a7b76d12c844427bfdc67d391175e42cba99025277ffce31a7587f7',
|
||||
archiveChecksum: '62acfa4614ec5ca93500c360f20893bcdbb2d7404212e875ab36ec7a0582ea38',
|
||||
binaryChecksum: 'bf1acd0d06c4e74dd0d0efe02e427b97ef4fec04f824d4e34042b093ce2d941d',
|
||||
binaryRelativePath: 'chrome-headless-shell-mac-x64/chrome-headless-shell',
|
||||
version: '136.0.7103.49',
|
||||
version: '137.0.7151.70',
|
||||
location: 'chromeForTesting',
|
||||
archivePath: 'mac-x64',
|
||||
isPreInstalled: false,
|
||||
|
@ -60,10 +60,10 @@ export class ChromiumArchivePaths {
|
|||
platform: 'darwin',
|
||||
architecture: 'arm64',
|
||||
archiveFilename: 'chrome-headless-shell-mac-arm64.zip',
|
||||
archiveChecksum: '30c5215f2b3caa3321de7c0e0de7225b13cbdd970f7c3f68c774be0ca34fc4e3',
|
||||
binaryChecksum: '6ada593813656a62560680f510a34f0937537ce8518cb60dcdd0c3ceace97f50',
|
||||
archiveChecksum: '0859f7f77b1770fff3c1d90a18c1e0b0ac4c3ad88b5c94594ceb13d867917044',
|
||||
binaryChecksum: 'bc273b92ea56f963e748bc50ce8f18a97a02fb19accff6081198818a2c49efdc',
|
||||
binaryRelativePath: 'chrome-headless-shell-mac-arm64/chrome-headless-shell',
|
||||
version: '136.0.7103.49',
|
||||
version: '137.0.7151.70',
|
||||
location: 'chromeForTesting',
|
||||
archivePath: 'mac-arm64',
|
||||
isPreInstalled: false,
|
||||
|
@ -71,9 +71,9 @@ export class ChromiumArchivePaths {
|
|||
{
|
||||
platform: 'linux',
|
||||
architecture: 'x64',
|
||||
archiveFilename: 'chromium-031848b-locales-linux_x64.zip',
|
||||
archiveChecksum: '2f8ede6c874cbf71f6d64ad5c88b33e0f91bdddb4f5684fa6148702ce85550d7',
|
||||
binaryChecksum: '12ba32eadf7dc1e3bd2c72707dfc100d4c9dd6eaddd5568f407a2ed66156e1d1',
|
||||
archiveFilename: 'chromium-dfa4dc5-locales-linux_x64.zip',
|
||||
archiveChecksum: 'de7ada038ba05427cde8c88a46ae8fc78f2234d210b32f47cd0be2fe00c49c07',
|
||||
binaryChecksum: 'adf965718b302c4b32fdca7946a1a98b14d8b300fb4ea522f9b82aa4d8a6b925',
|
||||
binaryRelativePath: 'headless_shell-linux_x64/headless_shell',
|
||||
location: 'custom',
|
||||
isPreInstalled: true,
|
||||
|
@ -81,9 +81,9 @@ export class ChromiumArchivePaths {
|
|||
{
|
||||
platform: 'linux',
|
||||
architecture: 'arm64',
|
||||
archiveFilename: 'chromium-031848b-locales-linux_arm64.zip',
|
||||
archiveChecksum: 'de7571709d3b25cd15c48949bb5d72665b98ed527d73dbffbb96116dd5aeaae9',
|
||||
binaryChecksum: 'c2cee1a7906e0c0905153bcc40ece031be5028d075b88736629280ac80877246',
|
||||
archiveFilename: 'chromium-dfa4dc5-locales-linux_arm64.zip',
|
||||
archiveChecksum: '4fc2465cc0d0209120f11ffe02de87b9dcb33bae46352b007c3f9931de54d94e',
|
||||
binaryChecksum: '5a3a531e67726109afda41f67540131482b43856cb2f9d59d865ed5597627b4b',
|
||||
binaryRelativePath: 'headless_shell-linux_arm64/headless_shell',
|
||||
location: 'custom',
|
||||
isPreInstalled: true,
|
||||
|
@ -92,10 +92,10 @@ export class ChromiumArchivePaths {
|
|||
platform: 'win32',
|
||||
architecture: 'x64',
|
||||
archiveFilename: 'chrome-headless-shell-win64.zip',
|
||||
archiveChecksum: 'a4a82311596166c6148df6d1d1497d9bb5397895c47f67abcde4b368180418bc',
|
||||
binaryChecksum: '574d3b27846bdeaafb73632f1f6aac001dc895165465ae11bcd3fedb35cfa9dd',
|
||||
archiveChecksum: 'feb586eb8ee687cfa2da34be97896a8ab8442c16cd46b624234e1b9a9fc9a177',
|
||||
binaryChecksum: 'f0f58682cd343ab34693b60746e9cbdc2b5c4c323a678927dc5dccb8708b2849',
|
||||
binaryRelativePath: path.join('chrome-headless-shell-win64', 'chrome-headless-shell.exe'),
|
||||
version: '136.0.7103.49',
|
||||
version: '137.0.7151.70',
|
||||
location: 'chromeForTesting',
|
||||
archivePath: 'win64',
|
||||
isPreInstalled: true,
|
||||
|
|
|
@ -40,7 +40,7 @@ export const getImageEmbeddableFactory = ({
|
|||
const dynamicActionsManager = embeddableEnhanced?.initializeEmbeddableDynamicActions(
|
||||
uuid,
|
||||
() => titleManager.api.title$.getValue(),
|
||||
initialState.rawState
|
||||
initialState
|
||||
);
|
||||
// if it is provided, start the dynamic actions manager
|
||||
const maybeStopDynamicActions = dynamicActionsManager?.startDynamicActions();
|
||||
|
@ -50,12 +50,15 @@ export const getImageEmbeddableFactory = ({
|
|||
const dataLoading$ = new BehaviorSubject<boolean | undefined>(true);
|
||||
|
||||
function serializeState() {
|
||||
const { rawState: dynamicActionsState, references: dynamicActionsReferences } =
|
||||
dynamicActionsManager?.serializeState() ?? {};
|
||||
return {
|
||||
rawState: {
|
||||
...titleManager.getLatestState(),
|
||||
...(dynamicActionsManager?.getLatestState() ?? {}),
|
||||
...dynamicActionsState,
|
||||
imageConfig: imageConfig$.getValue(),
|
||||
},
|
||||
references: dynamicActionsReferences ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
import { HasDynamicActions } from '@kbn/embeddable-enhanced-plugin/public';
|
||||
import { DynamicActionsSerializedState } from '@kbn/embeddable-enhanced-plugin/public/plugin';
|
||||
import { DynamicActionsSerializedState } from '@kbn/embeddable-enhanced-plugin/public';
|
||||
import { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public';
|
||||
import {
|
||||
HasEditCapabilities,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common';
|
||||
import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/server';
|
||||
import { EmbeddableRegistryDefinition } from '@kbn/embeddable-plugin/server';
|
||||
import { CONTROL_GROUP_TYPE } from '../../common';
|
||||
import {
|
||||
|
|
|
@ -11,7 +11,7 @@ import { SavedObjectReference } from '@kbn/core/types';
|
|||
import {
|
||||
EmbeddablePersistableStateService,
|
||||
EmbeddableStateWithType,
|
||||
} from '@kbn/embeddable-plugin/common/types';
|
||||
} from '@kbn/embeddable-plugin/server';
|
||||
import { MigrateFunctionsObject } from '@kbn/kibana-utils-plugin/common';
|
||||
|
||||
import type { ControlPanelsState, SerializedControlState } from '../../common';
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
import {
|
||||
EmbeddableStateWithType,
|
||||
EmbeddablePersistableStateService,
|
||||
} from '@kbn/embeddable-plugin/common';
|
||||
} from '@kbn/embeddable-plugin/server';
|
||||
import { SavedObjectReference } from '@kbn/core/types';
|
||||
|
||||
export const createEsqlControlInject = (): EmbeddablePersistableStateService['inject'] => {
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
import {
|
||||
EmbeddableStateWithType,
|
||||
EmbeddablePersistableStateService,
|
||||
} from '@kbn/embeddable-plugin/common';
|
||||
} from '@kbn/embeddable-plugin/server';
|
||||
import { SavedObjectReference } from '@kbn/core/types';
|
||||
import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common';
|
||||
import { DefaultDataControlState } from '../../common';
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
import {
|
||||
EmbeddableStateWithType,
|
||||
EmbeddablePersistableStateService,
|
||||
} from '@kbn/embeddable-plugin/common';
|
||||
} from '@kbn/embeddable-plugin/server';
|
||||
import { SavedObjectReference } from '@kbn/core/types';
|
||||
import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common';
|
||||
import { DefaultDataControlState } from '../../common';
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
import {
|
||||
EmbeddableStateWithType,
|
||||
EmbeddablePersistableStateService,
|
||||
} from '@kbn/embeddable-plugin/common';
|
||||
} from '@kbn/embeddable-plugin/server';
|
||||
import { SavedObjectReference } from '@kbn/core/types';
|
||||
|
||||
export const createTimeSliderInject = (): EmbeddablePersistableStateService['inject'] => {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
import type { Reference } from '@kbn/content-management-utils';
|
||||
import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common/types';
|
||||
import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common';
|
||||
|
||||
import {
|
||||
convertPanelSectionMapsToPanelsArray,
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { ComponentType } from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { CopyToDashboardAPI } from './copy_to_dashboard_action';
|
||||
import { CopyToDashboardModal } from './copy_to_dashboard_modal';
|
||||
import { DashboardPickerProps } from '@kbn/presentation-util-plugin/public/components/dashboard_picker/dashboard_picker';
|
||||
|
||||
jest.mock('../utils/get_dashboard_capabilities', () => ({
|
||||
getDashboardCapabilities: () => ({
|
||||
createNew: true,
|
||||
showWriteControls: true,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@kbn/presentation-util-plugin/public', () => ({
|
||||
withSuspense: (Component: ComponentType) => Component,
|
||||
LazyDashboardPicker: ({ idsToOmit, onChange }: DashboardPickerProps) => {
|
||||
const label = idsToOmit?.length
|
||||
? `mockDashboardPicker idsToOmit:${idsToOmit.join(',')}`
|
||||
: `mockDashboardPicker`;
|
||||
return (
|
||||
<button
|
||||
id="mockDashboardPicker"
|
||||
onClick={() => onChange({ name: 'Dashboard Two', id: 'dashboardTwo' })}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
describe('CopyToDashboardModal', () => {
|
||||
const api: CopyToDashboardAPI = {
|
||||
type: 'testPanelType',
|
||||
uuid: 'panelOne',
|
||||
parentApi: {
|
||||
type: 'dashboard',
|
||||
savedObjectId$: new BehaviorSubject<string | undefined>('dashboardOne'),
|
||||
getDashboardPanelFromId: () => ({
|
||||
type: 'testPanelType',
|
||||
gridData: { w: 1, h: 1, x: 0, y: 0, i: 'panelOne' },
|
||||
serializedState: {
|
||||
rawState: {
|
||||
title: 'Panel One',
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
const closeModalMock = jest.fn();
|
||||
const navigateToWithEmbeddablePackageMock = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
require('../services/kibana_services').embeddableService = {
|
||||
getStateTransfer: () => ({
|
||||
navigateToWithEmbeddablePackage: navigateToWithEmbeddablePackageMock,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('"New dashboard" option', () => {
|
||||
test('should be enabled when parent is saved dashboard', async () => {
|
||||
const result = render(<CopyToDashboardModal api={api} closeModal={closeModalMock} />);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.container.querySelector('#new-dashboard-option')).toBeEnabled()
|
||||
);
|
||||
});
|
||||
|
||||
test('should be disabled when parent is new dashboard', async () => {
|
||||
const newDashboardApi = {
|
||||
...api,
|
||||
parentApi: {
|
||||
...api.parentApi,
|
||||
savedObjectId$: new BehaviorSubject<string | undefined>(undefined),
|
||||
},
|
||||
};
|
||||
const result = render(
|
||||
<CopyToDashboardModal api={newDashboardApi} closeModal={closeModalMock} />
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.container.querySelector('#new-dashboard-option')).toBeDisabled()
|
||||
);
|
||||
});
|
||||
|
||||
test('should navigate to new dashboard on submit', async () => {
|
||||
const result = render(<CopyToDashboardModal api={api} closeModal={closeModalMock} />);
|
||||
|
||||
await waitFor(() => {
|
||||
// select new dashboard radio
|
||||
const newDashboardRadio = result.container.querySelector('#new-dashboard-option');
|
||||
expect(newDashboardRadio).not.toBeNull();
|
||||
userEvent.click(newDashboardRadio!);
|
||||
|
||||
// Click submit button
|
||||
const submitButton = result.container.querySelector('[data-test-subj=confirmCopyToButton]');
|
||||
expect(submitButton).not.toBeNull();
|
||||
expect(submitButton).toBeEnabled();
|
||||
userEvent.click(submitButton!);
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(navigateToWithEmbeddablePackageMock).toHaveBeenCalledWith('dashboards', {
|
||||
path: '#/create',
|
||||
state: {
|
||||
serializedState: {
|
||||
rawState: {
|
||||
title: 'Panel One',
|
||||
},
|
||||
},
|
||||
size: {
|
||||
height: 1,
|
||||
width: 1,
|
||||
},
|
||||
type: 'testPanelType',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('"Existing dashboard" option', () => {
|
||||
test('does not show the current dashboard in the dashboard picker', async () => {
|
||||
render(<CopyToDashboardModal api={api} closeModal={closeModalMock} />);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByText('mockDashboardPicker idsToOmit:dashboardOne')).toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
test('should navigate to selected dashboard on submit', async () => {
|
||||
const result = render(<CopyToDashboardModal api={api} closeModal={closeModalMock} />);
|
||||
|
||||
await waitFor(() => {
|
||||
// select a dashboard
|
||||
const mockDashboardPickerButton = result.container.querySelector('#mockDashboardPicker');
|
||||
expect(mockDashboardPickerButton).not.toBeNull();
|
||||
userEvent.click(mockDashboardPickerButton!);
|
||||
|
||||
// Click submit button
|
||||
const submitButton = result.container.querySelector('[data-test-subj=confirmCopyToButton]');
|
||||
expect(submitButton).not.toBeNull();
|
||||
expect(submitButton).toBeEnabled();
|
||||
userEvent.click(submitButton!);
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(navigateToWithEmbeddablePackageMock).toHaveBeenCalledWith('dashboards', {
|
||||
path: '#/view/dashboardTwo?_a=(viewMode:edit)',
|
||||
state: {
|
||||
serializedState: {
|
||||
rawState: {
|
||||
title: 'Panel One',
|
||||
},
|
||||
},
|
||||
size: {
|
||||
height: 1,
|
||||
width: 1,
|
||||
},
|
||||
type: 'testPanelType',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -26,7 +26,6 @@ import { embeddableService } from '../services/kibana_services';
|
|||
import { getDashboardCapabilities } from '../utils/get_dashboard_capabilities';
|
||||
import { dashboardCopyToDashboardActionStrings } from './_dashboard_actions_strings';
|
||||
import { CopyToDashboardAPI } from './copy_to_dashboard_action';
|
||||
import { DashboardApi } from '../dashboard_api/types';
|
||||
|
||||
interface CopyToDashboardModalProps {
|
||||
api: CopyToDashboardAPI;
|
||||
|
@ -50,9 +49,8 @@ export function CopyToDashboardModal({ api, closeModal }: CopyToDashboardModalPr
|
|||
const dashboardId = api.parentApi.savedObjectId$.value;
|
||||
|
||||
const onSubmit = useCallback(() => {
|
||||
const dashboard = api.parentApi as DashboardApi;
|
||||
// TODO handle getDashboardPanelFromId throw
|
||||
const panelToCopy = dashboard.getDashboardPanelFromId(api.uuid);
|
||||
const panelToCopy = api.parentApi.getDashboardPanelFromId(api.uuid);
|
||||
|
||||
const state: EmbeddablePackageState = {
|
||||
type: panelToCopy.type,
|
||||
|
|
|
@ -9,11 +9,7 @@
|
|||
|
||||
import type { DashboardPanelState } from '../../common';
|
||||
|
||||
import {
|
||||
dataService,
|
||||
embeddableService,
|
||||
savedObjectsTaggingService,
|
||||
} from '../services/kibana_services';
|
||||
import { dataService, savedObjectsTaggingService } from '../services/kibana_services';
|
||||
import { getSampleDashboardState } from '../mocks';
|
||||
import { getSerializedState } from './get_serialized_state';
|
||||
|
||||
|
@ -30,10 +26,6 @@ dataService.query.timefilter.timefilter.getRefreshInterval = jest
|
|||
.fn()
|
||||
.mockReturnValue({ pause: true, value: 0 });
|
||||
|
||||
embeddableService.extract = jest
|
||||
.fn()
|
||||
.mockImplementation((attributes) => ({ state: attributes, references: [] }));
|
||||
|
||||
if (savedObjectsTaggingService) {
|
||||
savedObjectsTaggingService.getTaggingApi = jest.fn().mockReturnValue({
|
||||
ui: {
|
||||
|
@ -121,7 +113,6 @@ describe('getSerializedState', () => {
|
|||
"panelConfig": Object {},
|
||||
"panelIndex": "54321",
|
||||
"type": "visualization",
|
||||
"version": undefined,
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
@ -216,7 +207,6 @@ describe('getSerializedState', () => {
|
|||
"panelConfig": Object {},
|
||||
"panelIndex": "54321",
|
||||
"type": "visualization",
|
||||
"version": undefined,
|
||||
},
|
||||
],
|
||||
"title": "Section One",
|
||||
|
|
|
@ -12,7 +12,6 @@ import { pick } from 'lodash';
|
|||
import moment, { Moment } from 'moment';
|
||||
|
||||
import type { Reference } from '@kbn/content-management-utils';
|
||||
import { extractReferences } from '../../common/dashboard_saved_object/persistable_state/dashboard_saved_object_references';
|
||||
import {
|
||||
convertPanelSectionMapsToPanelsArray,
|
||||
generateNewPanelIds,
|
||||
|
@ -25,11 +24,7 @@ import {
|
|||
convertDashboardVersionToNumber,
|
||||
convertNumberToDashboardVersion,
|
||||
} from '../services/dashboard_content_management_service/lib/dashboard_versioning';
|
||||
import {
|
||||
dataService,
|
||||
embeddableService,
|
||||
savedObjectsTaggingService,
|
||||
} from '../services/kibana_services';
|
||||
import { dataService, savedObjectsTaggingService } from '../services/kibana_services';
|
||||
import { DashboardApi } from './types';
|
||||
|
||||
const LATEST_DASHBOARD_CONTAINER_VERSION = convertNumberToDashboardVersion(LATEST_VERSION);
|
||||
|
@ -120,7 +115,7 @@ export const getSerializedState = ({
|
|||
]) as RefreshInterval)
|
||||
: undefined;
|
||||
|
||||
const rawDashboardAttributes: DashboardAttributes = {
|
||||
const attributes: DashboardAttributes = {
|
||||
version: convertDashboardVersionToNumber(LATEST_DASHBOARD_CONTAINER_VERSION),
|
||||
controlGroupInput: controlGroupInput as DashboardAttributes['controlGroupInput'],
|
||||
kibanaSavedObjectMeta: { searchSource },
|
||||
|
@ -134,23 +129,12 @@ export const getSerializedState = ({
|
|||
timeTo,
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract references from raw attributes and tags into the references array.
|
||||
*/
|
||||
const { attributes, references: dashboardReferences } = extractReferences(
|
||||
{
|
||||
attributes: rawDashboardAttributes,
|
||||
references: searchSourceReferences ?? [],
|
||||
},
|
||||
{ embeddablePersistableStateService: embeddableService }
|
||||
);
|
||||
|
||||
// TODO Provide tags as an array of tag names in the attribute. In that case, tag references
|
||||
// will be extracted by the server.
|
||||
const savedObjectsTaggingApi = savedObjectsTaggingService?.getTaggingApi();
|
||||
const references = savedObjectsTaggingApi?.ui.updateTagsReferences
|
||||
? savedObjectsTaggingApi?.ui.updateTagsReferences(dashboardReferences, tags)
|
||||
: dashboardReferences;
|
||||
? savedObjectsTaggingApi?.ui.updateTagsReferences(searchSourceReferences ?? [], tags)
|
||||
: searchSourceReferences ?? [];
|
||||
|
||||
const allReferences = [
|
||||
...references,
|
||||
|
|
|
@ -53,6 +53,47 @@ describe('layout manager', () => {
|
|||
expect(layoutManager.api.children$.getValue()[panels.panelOne.gridData.i]).toBe(childApi);
|
||||
});
|
||||
|
||||
test('should append incoming embeddable to existing panels', () => {
|
||||
const incomingEmbeddable = {
|
||||
embeddableId: 'panelTwo',
|
||||
serializedState: {
|
||||
rawState: {
|
||||
title: 'Panel Two',
|
||||
},
|
||||
},
|
||||
size: {
|
||||
height: 1,
|
||||
width: 1,
|
||||
},
|
||||
type: 'testPanelType',
|
||||
};
|
||||
const layoutManager = initializeLayoutManager(
|
||||
incomingEmbeddable,
|
||||
panels,
|
||||
{},
|
||||
trackPanelMock,
|
||||
() => []
|
||||
);
|
||||
|
||||
const layout = layoutManager.internalApi.layout$.value;
|
||||
expect(Object.keys(layout.panels).length).toBe(Object.keys(panels).length + 1);
|
||||
expect(layout.panels.panelTwo).toEqual({
|
||||
gridData: {
|
||||
h: 1,
|
||||
i: 'panelTwo',
|
||||
sectionId: undefined,
|
||||
w: 1,
|
||||
x: 1,
|
||||
y: 0,
|
||||
},
|
||||
type: 'testPanelType',
|
||||
});
|
||||
const incomingPanelState = layoutManager.internalApi.getSerializedStateForPanel('panelTwo');
|
||||
expect(incomingPanelState.rawState).toEqual({
|
||||
title: 'Panel Two',
|
||||
});
|
||||
});
|
||||
|
||||
describe('serializeLayout', () => {
|
||||
test('should serialize the latest state of all panels', () => {
|
||||
const layoutManager = initializeLayoutManager(
|
||||
|
|
|
@ -104,6 +104,19 @@ export function initializeLayoutManager(
|
|||
...layout.panels[panelId],
|
||||
explicitInput: currentChildState[panelId]?.rawState ?? {},
|
||||
};
|
||||
|
||||
// TODO move savedObjectRef extraction into embeddable implemenations
|
||||
const savedObjectId = (panels[panelId].explicitInput as { savedObjectId?: string })
|
||||
.savedObjectId;
|
||||
if (savedObjectId) {
|
||||
panels[panelId].panelRefName = `panel_${panelId}`;
|
||||
|
||||
references.push({
|
||||
name: `${panelId}:panel_${panelId}`,
|
||||
type: panels[panelId].type,
|
||||
id: savedObjectId,
|
||||
});
|
||||
}
|
||||
}
|
||||
return { panels, sections: { ...layout.sections }, references };
|
||||
};
|
||||
|
|
|
@ -13,7 +13,6 @@ import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/public';
|
|||
import { has } from 'lodash';
|
||||
|
||||
import { getDashboardContentManagementCache } from '..';
|
||||
import { injectReferences } from '../../../../common/dashboard_saved_object/persistable_state/dashboard_saved_object_references';
|
||||
import { convertPanelsArrayToPanelSectionMaps } from '../../../../common/lib/dashboard_panel_converters';
|
||||
import type { DashboardGetIn, DashboardGetOut } from '../../../../server/content_management';
|
||||
import { DEFAULT_DASHBOARD_STATE } from '../../../dashboard_api/default_dashboard_state';
|
||||
|
@ -22,7 +21,6 @@ import { DASHBOARD_CONTENT_ID } from '../../../utils/telemetry_constants';
|
|||
import {
|
||||
contentManagementService,
|
||||
dataService,
|
||||
embeddableService,
|
||||
savedObjectsTaggingService,
|
||||
} from '../../kibana_services';
|
||||
import type {
|
||||
|
@ -112,19 +110,7 @@ export const loadDashboardState = async ({
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject saved object references back into the saved object attributes
|
||||
*/
|
||||
const { references, attributes: rawAttributes, managed } = rawDashboardContent;
|
||||
const attributes = (() => {
|
||||
if (!references || references.length === 0) return rawAttributes;
|
||||
return injectReferences(
|
||||
{ references, attributes: rawAttributes },
|
||||
{
|
||||
embeddablePersistableStateService: embeddableService,
|
||||
}
|
||||
);
|
||||
})();
|
||||
const { references, attributes, managed } = rawDashboardContent;
|
||||
|
||||
/**
|
||||
* Create search source and pull filters and query from it.
|
||||
|
|
|
@ -8,12 +8,7 @@
|
|||
*/
|
||||
|
||||
import { getSampleDashboardState } from '../../../mocks';
|
||||
import {
|
||||
contentManagementService,
|
||||
coreServices,
|
||||
dataService,
|
||||
embeddableService,
|
||||
} from '../../kibana_services';
|
||||
import { contentManagementService, coreServices, dataService } from '../../kibana_services';
|
||||
import { saveDashboardState } from './save_dashboard_state';
|
||||
import { DashboardPanelMap } from '../../../../common/dashboard_container/types';
|
||||
|
||||
|
@ -36,10 +31,6 @@ dataService.query.timefilter.timefilter.getTime = jest
|
|||
.fn()
|
||||
.mockReturnValue({ from: 'then', to: 'now' });
|
||||
|
||||
embeddableService.extract = jest
|
||||
.fn()
|
||||
.mockImplementation((attributes) => ({ state: attributes, references: [] }));
|
||||
|
||||
describe('Save dashboard state', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
|
|
@ -68,7 +68,9 @@ export function dashboardAttributesOut(
|
|||
kibanaSavedObjectMeta: transformSearchSourceOut(kibanaSavedObjectMeta),
|
||||
}),
|
||||
...(optionsJSON && { options: transformOptionsOut(optionsJSON) }),
|
||||
...((panelsJSON || sections) && { panels: transformPanelsOut(panelsJSON, sections) }),
|
||||
...((panelsJSON || sections) && {
|
||||
panels: transformPanelsOut(panelsJSON, sections, references),
|
||||
}),
|
||||
...(refreshInterval && {
|
||||
refreshInterval: { pause: refreshInterval.pause, value: refreshInterval.value },
|
||||
}),
|
||||
|
|
|
@ -7,12 +7,15 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { SavedObjectReference } from '@kbn/core/server';
|
||||
import { SavedDashboardPanel, SavedDashboardSection } from '../../../../dashboard_saved_object';
|
||||
import { DashboardAttributes, DashboardPanel, DashboardSection } from '../../types';
|
||||
import { getReferencesForPanelId } from '../../../../../common/dashboard_container/persistable_state/dashboard_container_references';
|
||||
|
||||
export function transformPanelsOut(
|
||||
panelsJSON: string = '{}',
|
||||
sections: SavedDashboardSection[] = []
|
||||
sections: SavedDashboardSection[] = [],
|
||||
references?: SavedObjectReference[]
|
||||
): DashboardAttributes['panels'] {
|
||||
const panels = JSON.parse(panelsJSON);
|
||||
const sectionsMap: { [uuid: string]: DashboardPanel | DashboardSection } = sections.reduce(
|
||||
|
@ -23,35 +26,48 @@ export function transformPanelsOut(
|
|||
{}
|
||||
);
|
||||
panels.forEach((panel: SavedDashboardPanel) => {
|
||||
const filteredReferences = getReferencesForPanelId(panel.panelIndex, references ?? []);
|
||||
const panelReferences = filteredReferences.length === 0 ? references : filteredReferences;
|
||||
const { sectionId } = panel.gridData;
|
||||
if (sectionId) {
|
||||
(sectionsMap[sectionId] as DashboardSection).panels.push(transformPanelProperties(panel));
|
||||
(sectionsMap[sectionId] as DashboardSection).panels.push(
|
||||
transformPanelProperties(panel, panelReferences)
|
||||
);
|
||||
} else {
|
||||
sectionsMap[panel.panelIndex] = transformPanelProperties(panel);
|
||||
sectionsMap[panel.panelIndex] = transformPanelProperties(panel, panelReferences);
|
||||
}
|
||||
});
|
||||
return Object.values(sectionsMap);
|
||||
}
|
||||
|
||||
function transformPanelProperties({
|
||||
embeddableConfig,
|
||||
gridData,
|
||||
id,
|
||||
panelIndex,
|
||||
panelRefName,
|
||||
title,
|
||||
type,
|
||||
version,
|
||||
}: SavedDashboardPanel) {
|
||||
const { sectionId, ...rest } = gridData; // drop section ID, if it exists
|
||||
return {
|
||||
gridData: rest,
|
||||
function transformPanelProperties(
|
||||
{
|
||||
embeddableConfig,
|
||||
gridData,
|
||||
id,
|
||||
panelConfig: embeddableConfig,
|
||||
panelIndex,
|
||||
panelRefName,
|
||||
title,
|
||||
type,
|
||||
version,
|
||||
}: SavedDashboardPanel,
|
||||
references?: SavedObjectReference[]
|
||||
) {
|
||||
const { sectionId, ...rest } = gridData; // drop section ID, if it exists
|
||||
|
||||
const matchingReference =
|
||||
panelRefName && references
|
||||
? references.find((reference) => reference.name === panelRefName)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
gridData: rest,
|
||||
id: matchingReference ? matchingReference.id : id,
|
||||
panelConfig: embeddableConfig,
|
||||
panelIndex,
|
||||
panelRefName,
|
||||
title,
|
||||
type: matchingReference ? matchingReference.type : type,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { createTabItem } from './utils';
|
||||
import { type TabState } from './types';
|
||||
import { defaultTabState } from './internal_state';
|
||||
|
||||
const createMockTabState = (id: string, label: string): TabState => ({
|
||||
...defaultTabState,
|
||||
id,
|
||||
label,
|
||||
});
|
||||
|
||||
describe('createTabItem', () => {
|
||||
it('should create a tab with default label when no tabs exist', () => {
|
||||
const result = createTabItem([]);
|
||||
expect(result.label).toBe('Untitled');
|
||||
});
|
||||
|
||||
it('should create a tab with default label when no tabs with default label exist', () => {
|
||||
const tabs = [
|
||||
createMockTabState('tab1', 'Custom Label'),
|
||||
createMockTabState('tab2', 'Another Tab'),
|
||||
];
|
||||
|
||||
const result = createTabItem(tabs);
|
||||
expect(result.label).toBe('Untitled');
|
||||
});
|
||||
|
||||
it('should create a tab with number 2 when one default tab exists', () => {
|
||||
const tabs = [createMockTabState('tab1', 'Untitled')];
|
||||
|
||||
const result = createTabItem(tabs);
|
||||
expect(result.label).toBe('Untitled 2');
|
||||
});
|
||||
|
||||
it('should create a tab with incremented number when multiple default tabs exist', () => {
|
||||
const tabs = [
|
||||
createMockTabState('tab1', 'Untitled'),
|
||||
createMockTabState('tab2', 'Untitled 2'),
|
||||
createMockTabState('tab3', 'Untitled 5'),
|
||||
];
|
||||
|
||||
const result = createTabItem(tabs);
|
||||
expect(result.label).toBe('Untitled 6');
|
||||
});
|
||||
|
||||
it('should ignore non-matching tab labels', () => {
|
||||
const tabs = [
|
||||
createMockTabState('tab1', 'Untitled'),
|
||||
createMockTabState('tab2', 'Almost Untitled 2'), // This shouldn't match
|
||||
createMockTabState('tab3', 'UntitledX'), // This shouldn't match
|
||||
];
|
||||
|
||||
const result = createTabItem(tabs);
|
||||
expect(result.label).toBe('Untitled 2');
|
||||
});
|
||||
});
|
|
@ -8,6 +8,7 @@
|
|||
*/
|
||||
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { escapeRegExp } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { TabItem } from '@kbn/unified-tabs';
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
|
@ -46,13 +47,23 @@ export type TabActionInjector = ReturnType<typeof createTabActionInjector>;
|
|||
const DEFAULT_TAB_LABEL = i18n.translate('discover.defaultTabLabel', {
|
||||
defaultMessage: 'Untitled',
|
||||
});
|
||||
const DEFAULT_TAB_REGEX = new RegExp(`^${DEFAULT_TAB_LABEL}( \\d+)?$`);
|
||||
const ESCAPED_DEFAULT_TAB_LABEL = escapeRegExp(DEFAULT_TAB_LABEL);
|
||||
const DEFAULT_TAB_REGEX = new RegExp(`^${ESCAPED_DEFAULT_TAB_LABEL}( \\d+)?$`); // any default tab
|
||||
const DEFAULT_TAB_NUMBER_REGEX = new RegExp(`^${ESCAPED_DEFAULT_TAB_LABEL} (?<tabNumber>\\d+)$`); // tab with a number
|
||||
|
||||
export const createTabItem = (allTabs: TabState[]): TabItem => {
|
||||
const id = uuid();
|
||||
const untitledTabCount = allTabs.filter((tab) => DEFAULT_TAB_REGEX.test(tab.label.trim())).length;
|
||||
const label =
|
||||
untitledTabCount > 0 ? `${DEFAULT_TAB_LABEL} ${untitledTabCount}` : DEFAULT_TAB_LABEL;
|
||||
|
||||
const existingNumbers = allTabs
|
||||
.filter((tab) => DEFAULT_TAB_REGEX.test(tab.label.trim()))
|
||||
.map((tab) => {
|
||||
const match = tab.label.trim().match(DEFAULT_TAB_NUMBER_REGEX);
|
||||
const tabNumber = match?.groups?.tabNumber;
|
||||
return tabNumber ? Number(tabNumber) : 1;
|
||||
});
|
||||
|
||||
const nextNumber = existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : null;
|
||||
const label = nextNumber ? `${DEFAULT_TAB_LABEL} ${nextNumber}` : DEFAULT_TAB_LABEL;
|
||||
|
||||
return { id, label };
|
||||
};
|
||||
|
|
|
@ -95,7 +95,7 @@ export const getSearchEmbeddableFactory = ({
|
|||
discoverServices.embeddableEnhanced?.initializeEmbeddableDynamicActions(
|
||||
uuid,
|
||||
() => titleManager.api.title$.getValue(),
|
||||
initialState.rawState
|
||||
initialState
|
||||
);
|
||||
const maybeStopDynamicActions = dynamicActionsManager?.startDynamicActions();
|
||||
const searchEmbeddable = await initializeSearchEmbeddableApi(runtimeState, {
|
||||
|
@ -129,7 +129,7 @@ export const getSearchEmbeddableFactory = ({
|
|||
savedSearch: searchEmbeddable.api.savedSearch$.getValue(),
|
||||
serializeTitles: titleManager.getLatestState,
|
||||
serializeTimeRange: timeRangeManager.getLatestState,
|
||||
serializeDynamicActions: dynamicActionsManager?.getLatestState,
|
||||
serializeDynamicActions: dynamicActionsManager?.serializeState,
|
||||
savedObjectId,
|
||||
});
|
||||
|
||||
|
|
|
@ -33,8 +33,10 @@ import type {
|
|||
import type { DataTableColumnsMeta } from '@kbn/unified-data-table';
|
||||
import type { BehaviorSubject } from 'rxjs';
|
||||
import type { PublishesWritableDataViews } from '@kbn/presentation-publishing/interfaces/publishes_data_views';
|
||||
import type { DynamicActionsSerializedState } from '@kbn/embeddable-enhanced-plugin/public/plugin';
|
||||
import type { HasDynamicActions } from '@kbn/embeddable-enhanced-plugin/public';
|
||||
import type {
|
||||
DynamicActionsSerializedState,
|
||||
HasDynamicActions,
|
||||
} from '@kbn/embeddable-enhanced-plugin/public';
|
||||
import type { EDITABLE_SAVED_SEARCH_KEYS } from './constants';
|
||||
|
||||
export type SearchEmbeddableState = Pick<
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
type SavedSearchAttributes,
|
||||
} from '@kbn/saved-search-plugin/common';
|
||||
import type { SavedSearchUnwrapResult } from '@kbn/saved-search-plugin/public';
|
||||
import type { DynamicActionsSerializedState } from '@kbn/embeddable-enhanced-plugin/public/plugin';
|
||||
import type { DynamicActionsSerializedState } from '@kbn/embeddable-enhanced-plugin/public';
|
||||
import { extract, inject } from '../../../common/embeddable/search_inject_extract';
|
||||
import type { DiscoverServices } from '../../build_services';
|
||||
import {
|
||||
|
@ -92,13 +92,16 @@ export const serializeState = ({
|
|||
savedSearch: SavedSearch;
|
||||
serializeTitles: () => SerializedTitles;
|
||||
serializeTimeRange: () => SerializedTimeRange;
|
||||
serializeDynamicActions: (() => DynamicActionsSerializedState) | undefined;
|
||||
serializeDynamicActions: (() => SerializedPanelState<DynamicActionsSerializedState>) | undefined;
|
||||
savedObjectId?: string;
|
||||
}): SerializedPanelState<SearchEmbeddableSerializedState> => {
|
||||
const searchSource = savedSearch.searchSource;
|
||||
const { searchSourceJSON, references: originalReferences } = searchSource.serialize();
|
||||
const savedSearchAttributes = toSavedSearchAttributes(savedSearch, searchSourceJSON);
|
||||
|
||||
const { rawState: dynamicActionsState, references: dynamicActionsReferences } =
|
||||
serializeDynamicActions?.() ?? {};
|
||||
|
||||
if (savedObjectId) {
|
||||
const editableAttributesBackup = initialState.rawSavedObjectAttributes ?? {};
|
||||
|
||||
|
@ -116,11 +119,10 @@ export const serializeState = ({
|
|||
// Serialize the current dashboard state into the panel state **without** updating the saved object
|
||||
...serializeTitles(),
|
||||
...serializeTimeRange(),
|
||||
...serializeDynamicActions?.(),
|
||||
...dynamicActionsState,
|
||||
...overwriteState,
|
||||
},
|
||||
// No references to extract for by-reference embeddable since all references are stored with by-reference saved object
|
||||
references: [],
|
||||
references: dynamicActionsReferences ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -136,9 +138,9 @@ export const serializeState = ({
|
|||
rawState: {
|
||||
...serializeTitles(),
|
||||
...serializeTimeRange(),
|
||||
...serializeDynamicActions?.(),
|
||||
...dynamicActionsState,
|
||||
...(state as unknown as SavedSearchAttributes),
|
||||
},
|
||||
references,
|
||||
references: [...references, ...(dynamicActionsReferences ?? [])],
|
||||
};
|
||||
};
|
||||
|
|
|
@ -8,8 +8,7 @@
|
|||
*/
|
||||
|
||||
export type {
|
||||
CommonEmbeddableStartContract,
|
||||
EmbeddableRegistryDefinition,
|
||||
EmbeddableStateWithType,
|
||||
EmbeddablePersistableStateService,
|
||||
EmbeddableRegistryDefinition,
|
||||
} from './types';
|
||||
} from '../server';
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { EmbeddablePersistableStateService } from './types';
|
||||
import type { EmbeddablePersistableStateService } from '.';
|
||||
|
||||
export const createEmbeddablePersistableStateServiceMock =
|
||||
(): jest.Mocked<EmbeddablePersistableStateService> => {
|
||||
|
|
|
@ -55,11 +55,8 @@ const createSetupContract = (): Setup => {
|
|||
|
||||
const createStartContract = (): Start => {
|
||||
const startContract: Start = {
|
||||
telemetry: jest.fn(),
|
||||
extract: jest.fn(),
|
||||
inject: jest.fn(),
|
||||
getAllMigrations: jest.fn(),
|
||||
getStateTransfer: jest.fn(() => createEmbeddableStateTransferMock() as EmbeddableStateTransfer),
|
||||
getEnhancement: jest.fn(),
|
||||
};
|
||||
return startContract;
|
||||
};
|
||||
|
|
|
@ -1,70 +0,0 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { testPlugin } from './tests/test_plugin';
|
||||
|
||||
describe('embeddable enhancements', () => {
|
||||
const coreSetup = coreMock.createSetup();
|
||||
const coreStart = coreMock.createStart();
|
||||
const { setup, doStart } = testPlugin(coreSetup, coreStart);
|
||||
const start = doStart();
|
||||
const embeddableEnhancement = {
|
||||
id: 'test',
|
||||
extract: jest.fn().mockImplementation((state) => ({ state, references: [] })),
|
||||
inject: jest.fn().mockImplementation((state) => state),
|
||||
telemetry: jest.fn().mockResolvedValue({}),
|
||||
migrations: { '7.11.0': jest.fn().mockImplementation((state) => state) },
|
||||
} as any;
|
||||
const embeddableState = {
|
||||
enhancements: {
|
||||
test: {
|
||||
my: 'state',
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
|
||||
setup.registerEnhancement(embeddableEnhancement);
|
||||
|
||||
test('cannot register embeddable enhancement with the same ID', async () => {
|
||||
expect(() => setup.registerEnhancement(embeddableEnhancement)).toThrowError(
|
||||
'enhancement with id test already exists in the registry'
|
||||
);
|
||||
});
|
||||
|
||||
test('enhancement extract function gets called when calling embeddable extract', () => {
|
||||
start.extract(embeddableState);
|
||||
expect(embeddableEnhancement.extract).toBeCalledWith(embeddableState.enhancements.test);
|
||||
});
|
||||
|
||||
test('enhancement inject function gets called when calling embeddable inject', () => {
|
||||
start.inject(embeddableState, []);
|
||||
expect(embeddableEnhancement.extract).toBeCalledWith(embeddableState.enhancements.test);
|
||||
});
|
||||
|
||||
test('enhancement telemetry function gets called when calling embeddable telemetry', () => {
|
||||
start.telemetry(embeddableState, {});
|
||||
expect(embeddableEnhancement.telemetry).toBeCalledWith(embeddableState.enhancements.test, {});
|
||||
});
|
||||
|
||||
test('enhancement migrate function gets called when calling embeddable migrate', () => {
|
||||
start.getAllMigrations!()['7.11.0']!(embeddableState);
|
||||
expect(embeddableEnhancement.migrations['7.11.0']).toBeCalledWith(
|
||||
embeddableState.enhancements.test
|
||||
);
|
||||
});
|
||||
|
||||
test('doesnt fail if there is no migration function registered for specific version', () => {
|
||||
expect(() => {
|
||||
start.getAllMigrations!()['7.11.0']!(embeddableState);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(start.getAllMigrations!()['7.11.0']!(embeddableState)).toEqual(embeddableState);
|
||||
});
|
||||
});
|
|
@ -16,17 +16,8 @@ import {
|
|||
PublicAppInfo,
|
||||
} from '@kbn/core/public';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { migrateToLatest } from '@kbn/kibana-utils-plugin/common';
|
||||
import { registerTriggers } from './ui_actions/register_triggers';
|
||||
import { EmbeddableStateTransfer } from './state_transfer';
|
||||
import { EmbeddableStateWithType, CommonEmbeddableStartContract } from '../common/types';
|
||||
import {
|
||||
getExtractFunction,
|
||||
getInjectFunction,
|
||||
getMigrateFunction,
|
||||
getTelemetryFunction,
|
||||
} from '../common/lib';
|
||||
import { getAllMigrations } from '../common/lib/get_all_migrations';
|
||||
import { setKibanaServices } from './kibana_services';
|
||||
import { registerReactEmbeddableFactory } from './react_embeddable_system';
|
||||
import { registerAddFromLibraryType } from './add_from_library/registry';
|
||||
|
@ -67,17 +58,6 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
|
|||
this.appList
|
||||
);
|
||||
|
||||
const commonContract: CommonEmbeddableStartContract = {
|
||||
getEnhancement: this.enhancementsRegistry.getEnhancement,
|
||||
};
|
||||
|
||||
const getAllMigrationsFn = () =>
|
||||
getAllMigrations(
|
||||
[],
|
||||
this.enhancementsRegistry.getEnhancements(),
|
||||
getMigrateFunction(commonContract)
|
||||
);
|
||||
|
||||
const embeddableStart: EmbeddableStart = {
|
||||
getStateTransfer: (storage?: Storage) =>
|
||||
storage
|
||||
|
@ -88,13 +68,7 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
|
|||
storage
|
||||
)
|
||||
: this.stateTransferService,
|
||||
telemetry: getTelemetryFunction(commonContract),
|
||||
extract: getExtractFunction(commonContract),
|
||||
inject: getInjectFunction(commonContract),
|
||||
getAllMigrations: getAllMigrationsFn,
|
||||
migrateToLatest: (state) => {
|
||||
return migrateToLatest(getAllMigrationsFn(), state) as EmbeddableStateWithType;
|
||||
},
|
||||
getEnhancement: this.enhancementsRegistry.getEnhancement,
|
||||
};
|
||||
|
||||
setKibanaServices(core, embeddableStart, deps);
|
||||
|
|
|
@ -14,11 +14,10 @@ import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-manag
|
|||
import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
|
||||
import type { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-tagging-oss-plugin/public';
|
||||
import type { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import type { PersistableStateService } from '@kbn/kibana-utils-plugin/common';
|
||||
import { PersistableState } from '@kbn/kibana-utils-plugin/common';
|
||||
import type { registerAddFromLibraryType } from './add_from_library/registry';
|
||||
import type { registerReactEmbeddableFactory } from './react_embeddable_system';
|
||||
import type { EmbeddableStateTransfer } from './state_transfer';
|
||||
import type { EmbeddableStateWithType } from '../common';
|
||||
import { EnhancementRegistryDefinition } from './enhancements/types';
|
||||
|
||||
export interface EmbeddableSetupDependencies {
|
||||
|
@ -74,6 +73,7 @@ export interface EmbeddableSetup {
|
|||
registerEnhancement: (enhancement: EnhancementRegistryDefinition) => void;
|
||||
}
|
||||
|
||||
export interface EmbeddableStart extends PersistableStateService<EmbeddableStateWithType> {
|
||||
export interface EmbeddableStart {
|
||||
getStateTransfer: (storage?: Storage) => EmbeddableStateTransfer;
|
||||
getEnhancement: (enhancementId: string) => PersistableState;
|
||||
}
|
||||
|
|
|
@ -11,9 +11,12 @@ import { EmbeddableSetup, EmbeddableStart } from './plugin';
|
|||
|
||||
export type { EmbeddableSetup, EmbeddableStart };
|
||||
|
||||
export type { EnhancementRegistryDefinition } from './types';
|
||||
export type { EmbeddableRegistryDefinition, EnhancementRegistryDefinition } from './types';
|
||||
|
||||
export type { EmbeddableRegistryDefinition } from '../common';
|
||||
export type {
|
||||
EmbeddableStateWithType,
|
||||
EmbeddablePersistableStateService,
|
||||
} from './persistable_state';
|
||||
|
||||
export const plugin = async () => {
|
||||
const { EmbeddableServerPlugin } = await import('./plugin');
|
||||
|
|
|
@ -8,13 +8,17 @@
|
|||
*/
|
||||
|
||||
import type { SerializableRecord } from '@kbn/utility-types';
|
||||
import { CommonEmbeddableStartContract, EmbeddableStateWithType } from '../types';
|
||||
import { PersistableState } from '@kbn/kibana-utils-plugin/common';
|
||||
import { EmbeddableStateWithType } from './types';
|
||||
import { extractBaseEmbeddableInput } from './migrate_base_input';
|
||||
|
||||
export const getExtractFunction = (embeddables: CommonEmbeddableStartContract) => {
|
||||
export const getExtractFunction = (
|
||||
getEmbeddableFactory: (embeddableFactoryId: string) => PersistableState<EmbeddableStateWithType>,
|
||||
getEnhancement: (enhancementId: string) => PersistableState
|
||||
) => {
|
||||
return (state: EmbeddableStateWithType) => {
|
||||
const enhancements = state.enhancements || {};
|
||||
const factory = embeddables.getEmbeddableFactory?.(state.type);
|
||||
const factory = getEmbeddableFactory(state.type);
|
||||
|
||||
const baseResponse = extractBaseEmbeddableInput(state);
|
||||
let updatedInput = baseResponse.state;
|
||||
|
@ -29,9 +33,9 @@ export const getExtractFunction = (embeddables: CommonEmbeddableStartContract) =
|
|||
updatedInput.enhancements = {};
|
||||
Object.keys(enhancements).forEach((key) => {
|
||||
if (!enhancements[key]) return;
|
||||
const enhancementResult = embeddables
|
||||
.getEnhancement(key)
|
||||
.extract(enhancements[key] as SerializableRecord);
|
||||
const enhancementResult = getEnhancement(key).extract(
|
||||
enhancements[key] as SerializableRecord
|
||||
);
|
||||
refs.push(...enhancementResult.references);
|
||||
updatedInput.enhancements![key] = enhancementResult.state;
|
||||
});
|
|
@ -12,3 +12,4 @@ export { getInjectFunction } from './inject';
|
|||
export type { MigrateFunction } from './migrate';
|
||||
export { getMigrateFunction } from './migrate';
|
||||
export { getTelemetryFunction } from './telemetry';
|
||||
export type { EmbeddableStateWithType, EmbeddablePersistableStateService } from './types';
|
|
@ -9,13 +9,17 @@
|
|||
|
||||
import type { SerializableRecord } from '@kbn/utility-types';
|
||||
import { SavedObjectReference } from '@kbn/core/types';
|
||||
import { CommonEmbeddableStartContract, EmbeddableStateWithType } from '../types';
|
||||
import { PersistableState } from '@kbn/kibana-utils-plugin/common';
|
||||
import { EmbeddableStateWithType } from './types';
|
||||
import { injectBaseEmbeddableInput } from './migrate_base_input';
|
||||
|
||||
export const getInjectFunction = (embeddables: CommonEmbeddableStartContract) => {
|
||||
export const getInjectFunction = (
|
||||
getEmbeddableFactory: (embeddableFactoryId: string) => PersistableState<EmbeddableStateWithType>,
|
||||
getEnhancement: (enhancementId: string) => PersistableState
|
||||
) => {
|
||||
return (state: EmbeddableStateWithType, references: SavedObjectReference[]) => {
|
||||
const enhancements = state.enhancements || {};
|
||||
const factory = embeddables.getEmbeddableFactory?.(state.type);
|
||||
const factory = getEmbeddableFactory(state.type);
|
||||
|
||||
let updatedInput = injectBaseEmbeddableInput(state, references);
|
||||
|
||||
|
@ -26,9 +30,10 @@ export const getInjectFunction = (embeddables: CommonEmbeddableStartContract) =>
|
|||
updatedInput.enhancements = {};
|
||||
Object.keys(enhancements).forEach((key) => {
|
||||
if (!enhancements[key]) return;
|
||||
updatedInput.enhancements![key] = embeddables
|
||||
.getEnhancement(key)
|
||||
.inject(enhancements[key] as SerializableRecord, references);
|
||||
updatedInput.enhancements![key] = getEnhancement(key).inject(
|
||||
enhancements[key] as SerializableRecord,
|
||||
references
|
||||
);
|
||||
});
|
||||
|
||||
return updatedInput;
|
|
@ -8,15 +8,19 @@
|
|||
*/
|
||||
|
||||
import type { SerializableRecord } from '@kbn/utility-types';
|
||||
import { CommonEmbeddableStartContract } from '../types';
|
||||
import { PersistableState } from '@kbn/kibana-utils-plugin/common';
|
||||
import { baseEmbeddableMigrations } from './migrate_base_input';
|
||||
import { EmbeddableStateWithType } from './types';
|
||||
|
||||
export type MigrateFunction = (state: SerializableRecord, version: string) => SerializableRecord;
|
||||
|
||||
export const getMigrateFunction = (embeddables: CommonEmbeddableStartContract) => {
|
||||
export const getMigrateFunction = (
|
||||
getEmbeddableFactory: (embeddableFactoryId: string) => PersistableState<EmbeddableStateWithType>,
|
||||
getEnhancement: (enhancementId: string) => PersistableState
|
||||
) => {
|
||||
const migrateFn: MigrateFunction = (state: SerializableRecord, version: string) => {
|
||||
const enhancements = (state.enhancements as SerializableRecord) || {};
|
||||
const factory = embeddables.getEmbeddableFactory?.(state.type as string);
|
||||
const factory = getEmbeddableFactory?.(state.type as string);
|
||||
|
||||
let updatedInput = baseEmbeddableMigrations[version]
|
||||
? baseEmbeddableMigrations[version](state)
|
||||
|
@ -28,8 +32,8 @@ export const getMigrateFunction = (embeddables: CommonEmbeddableStartContract) =
|
|||
updatedInput = factoryMigrations[version](updatedInput);
|
||||
}
|
||||
|
||||
if (factory?.isContainerType) {
|
||||
updatedInput.panels = ((state.panels as SerializableRecord[]) || []).map((panel) => {
|
||||
if ('panels' in state && Array.isArray(state.panels)) {
|
||||
updatedInput.panels = (state.panels as SerializableRecord[]).map((panel) => {
|
||||
return migrateFn(panel, version);
|
||||
});
|
||||
}
|
||||
|
@ -37,7 +41,7 @@ export const getMigrateFunction = (embeddables: CommonEmbeddableStartContract) =
|
|||
updatedInput.enhancements = {};
|
||||
Object.keys(enhancements).forEach((key) => {
|
||||
if (!enhancements[key]) return;
|
||||
const enhancementDefinition = embeddables.getEnhancement(key);
|
||||
const enhancementDefinition = getEnhancement(key);
|
||||
const enchantmentMigrations =
|
||||
typeof enhancementDefinition?.migrations === 'function'
|
||||
? enhancementDefinition?.migrations()
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
import { SavedObjectReference } from '@kbn/core/types';
|
||||
import { MigrateFunctionsObject } from '@kbn/kibana-utils-plugin/common';
|
||||
import { EmbeddableStateWithType } from '../types';
|
||||
import { EmbeddableStateWithType } from './types';
|
||||
|
||||
export const telemetryBaseEmbeddableInput = (
|
||||
state: EmbeddableStateWithType,
|
|
@ -8,16 +8,20 @@
|
|||
*/
|
||||
|
||||
import { SerializableRecord } from '@kbn/utility-types';
|
||||
import { CommonEmbeddableStartContract, EmbeddableStateWithType } from '../types';
|
||||
import { PersistableState } from '@kbn/kibana-utils-plugin/common';
|
||||
import { EmbeddableStateWithType } from './types';
|
||||
import { telemetryBaseEmbeddableInput } from './migrate_base_input';
|
||||
|
||||
export const getTelemetryFunction = (embeddables: CommonEmbeddableStartContract) => {
|
||||
export const getTelemetryFunction = (
|
||||
getEmbeddableFactory: (embeddableFactoryId: string) => PersistableState<EmbeddableStateWithType>,
|
||||
getEnhancement: (enhancementId: string) => PersistableState
|
||||
) => {
|
||||
return (
|
||||
state: EmbeddableStateWithType,
|
||||
telemetryData: Record<string, string | number | boolean> = {}
|
||||
) => {
|
||||
const enhancements = state.enhancements || {};
|
||||
const factory = embeddables.getEmbeddableFactory?.(state.type);
|
||||
const factory = getEmbeddableFactory(state.type);
|
||||
|
||||
let outputTelemetryData = telemetryBaseEmbeddableInput(state, telemetryData);
|
||||
if (factory) {
|
||||
|
@ -25,9 +29,10 @@ export const getTelemetryFunction = (embeddables: CommonEmbeddableStartContract)
|
|||
}
|
||||
Object.keys(enhancements).map((key) => {
|
||||
if (!enhancements[key]) return;
|
||||
outputTelemetryData = embeddables
|
||||
.getEnhancement(key)
|
||||
.telemetry(enhancements[key] as Record<string, SerializableRecord>, outputTelemetryData);
|
||||
outputTelemetryData = getEnhancement(key).telemetry(
|
||||
enhancements[key] as Record<string, SerializableRecord>,
|
||||
outputTelemetryData
|
||||
);
|
||||
});
|
||||
|
||||
return outputTelemetryData;
|
|
@ -7,29 +7,12 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { SerializableRecord } from '@kbn/utility-types';
|
||||
import type {
|
||||
PersistableStateService,
|
||||
PersistableState,
|
||||
PersistableStateDefinition,
|
||||
} from '@kbn/kibana-utils-plugin/common';
|
||||
import { PersistableStateService } from '@kbn/kibana-utils-plugin/common';
|
||||
import { SerializableRecord } from '@kbn/utility-types';
|
||||
|
||||
export type EmbeddableStateWithType = {
|
||||
enhancements?: SerializableRecord;
|
||||
type: string;
|
||||
};
|
||||
|
||||
export interface EmbeddableRegistryDefinition<
|
||||
P extends EmbeddableStateWithType = EmbeddableStateWithType
|
||||
> extends PersistableStateDefinition<P> {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export type EmbeddablePersistableStateService = PersistableStateService<EmbeddableStateWithType>;
|
||||
|
||||
export interface CommonEmbeddableStartContract {
|
||||
getEmbeddableFactory?: (
|
||||
embeddableFactoryId: string
|
||||
) => PersistableState & { isContainerType: boolean };
|
||||
getEnhancement: (enhancementId: string) => PersistableState;
|
||||
}
|
|
@ -14,25 +14,23 @@ import {
|
|||
PersistableStateService,
|
||||
PersistableStateMigrateFn,
|
||||
MigrateFunctionsObject,
|
||||
PersistableState,
|
||||
} from '@kbn/kibana-utils-plugin/common';
|
||||
import {
|
||||
EmbeddableFactoryRegistry,
|
||||
EnhancementsRegistry,
|
||||
EnhancementRegistryDefinition,
|
||||
EnhancementRegistryItem,
|
||||
EmbeddableRegistryDefinition,
|
||||
} from './types';
|
||||
import { EmbeddableStateWithType } from './persistable_state/types';
|
||||
import {
|
||||
getExtractFunction,
|
||||
getInjectFunction,
|
||||
getMigrateFunction,
|
||||
getTelemetryFunction,
|
||||
} from '../common/lib';
|
||||
import {
|
||||
EmbeddableStateWithType,
|
||||
CommonEmbeddableStartContract,
|
||||
EmbeddableRegistryDefinition,
|
||||
} from '../common/types';
|
||||
import { getAllMigrations } from '../common/lib/get_all_migrations';
|
||||
} from './persistable_state';
|
||||
import { getAllMigrations } from './persistable_state/get_all_migrations';
|
||||
|
||||
export interface EmbeddableSetup extends PersistableStateService<EmbeddableStateWithType> {
|
||||
registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => void;
|
||||
|
@ -48,19 +46,13 @@ export class EmbeddableServerPlugin implements Plugin<EmbeddableSetup, Embeddabl
|
|||
private migrateFn: PersistableStateMigrateFn | undefined;
|
||||
|
||||
public setup(core: CoreSetup) {
|
||||
const commonContract: CommonEmbeddableStartContract = {
|
||||
getEmbeddableFactory: this
|
||||
.getEmbeddableFactory as unknown as CommonEmbeddableStartContract['getEmbeddableFactory'],
|
||||
getEnhancement: this.getEnhancement,
|
||||
};
|
||||
|
||||
this.migrateFn = getMigrateFunction(commonContract);
|
||||
this.migrateFn = getMigrateFunction(this.getEmbeddableFactory, this.getEnhancement);
|
||||
return {
|
||||
registerEmbeddableFactory: this.registerEmbeddableFactory,
|
||||
registerEnhancement: this.registerEnhancement,
|
||||
telemetry: getTelemetryFunction(commonContract),
|
||||
extract: getExtractFunction(commonContract),
|
||||
inject: getInjectFunction(commonContract),
|
||||
telemetry: getTelemetryFunction(this.getEmbeddableFactory, this.getEnhancement),
|
||||
extract: getExtractFunction(this.getEmbeddableFactory, this.getEnhancement),
|
||||
inject: getInjectFunction(this.getEmbeddableFactory, this.getEnhancement),
|
||||
getAllMigrations: () =>
|
||||
getAllMigrations(
|
||||
Array.from(this.embeddableFactories.values()),
|
||||
|
@ -71,16 +63,10 @@ export class EmbeddableServerPlugin implements Plugin<EmbeddableSetup, Embeddabl
|
|||
}
|
||||
|
||||
public start(core: CoreStart) {
|
||||
const commonContract: CommonEmbeddableStartContract = {
|
||||
getEmbeddableFactory: this
|
||||
.getEmbeddableFactory as unknown as CommonEmbeddableStartContract['getEmbeddableFactory'],
|
||||
getEnhancement: this.getEnhancement,
|
||||
};
|
||||
|
||||
return {
|
||||
telemetry: getTelemetryFunction(commonContract),
|
||||
extract: getExtractFunction(commonContract),
|
||||
inject: getInjectFunction(commonContract),
|
||||
telemetry: getTelemetryFunction(this.getEmbeddableFactory, this.getEnhancement),
|
||||
extract: getExtractFunction(this.getEmbeddableFactory, this.getEnhancement),
|
||||
inject: getInjectFunction(this.getEmbeddableFactory, this.getEnhancement),
|
||||
getAllMigrations: () =>
|
||||
getAllMigrations(
|
||||
Array.from(this.embeddableFactories.values()),
|
||||
|
@ -138,11 +124,15 @@ export class EmbeddableServerPlugin implements Plugin<EmbeddableSetup, Embeddabl
|
|||
});
|
||||
};
|
||||
|
||||
private getEmbeddableFactory = (embeddableFactoryId: string) => {
|
||||
private getEmbeddableFactory = (
|
||||
embeddableFactoryId: string
|
||||
): PersistableState<EmbeddableStateWithType> => {
|
||||
return (
|
||||
this.embeddableFactories.get(embeddableFactoryId) || {
|
||||
id: 'unknown',
|
||||
telemetry: (state, stats) => stats,
|
||||
telemetry: (
|
||||
state: EmbeddableStateWithType,
|
||||
stats: Record<string, string | number | boolean>
|
||||
) => stats,
|
||||
inject: (state: EmbeddableStateWithType) => state,
|
||||
extract: (state: EmbeddableStateWithType) => {
|
||||
return { state, references: [] };
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
import type { SerializableRecord } from '@kbn/utility-types';
|
||||
import { PersistableState, PersistableStateDefinition } from '@kbn/kibana-utils-plugin/common';
|
||||
import { EmbeddableStateWithType } from '../common/types';
|
||||
import { EmbeddableStateWithType } from './persistable_state/types';
|
||||
|
||||
export type EmbeddableFactoryRegistry = Map<string, EmbeddableRegistryItem>;
|
||||
export type EnhancementsRegistry = Map<string, EnhancementRegistryItem>;
|
||||
|
@ -28,3 +28,9 @@ export interface EmbeddableRegistryItem<P extends EmbeddableStateWithType = Embe
|
|||
extends PersistableState<P> {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface EmbeddableRegistryDefinition<
|
||||
P extends EmbeddableStateWithType = EmbeddableStateWithType
|
||||
> extends PersistableStateDefinition<P> {
|
||||
id: string;
|
||||
}
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
@import './lib/converters/index';
|
||||
@import './lib/content_types/index';
|
|
@ -1,3 +0,0 @@
|
|||
.ffArray__highlight {
|
||||
color: $euiColorMediumShade;
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
@import './html_content_type';
|
|
@ -1 +0,0 @@
|
|||
@import './string';
|
|
@ -1,7 +0,0 @@
|
|||
.ffString__emptyValue {
|
||||
color: $euiColorDarkShade;
|
||||
}
|
||||
|
||||
.lnsTableCell--colored .ffString__emptyValue {
|
||||
color: unset;
|
||||
}
|
|
@ -11,7 +11,6 @@ import { CoreSetup, Plugin } from '@kbn/core/public';
|
|||
import { FieldFormatsRegistry, FORMATS_UI_SETTINGS } from '../common';
|
||||
import { baseFormattersPublic } from './lib';
|
||||
import { FormatFactory } from '../common/types';
|
||||
import './index.scss';
|
||||
|
||||
export class FieldFormatsPlugin implements Plugin<FieldFormatsSetup, FieldFormatsStart> {
|
||||
private readonly fieldFormatsRegistry: FieldFormatsRegistry = new FieldFormatsRegistry();
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
.sourceViewer__loading {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: left;
|
||||
flex: 1 0 100%;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin-top: $euiSizeS;
|
||||
}
|
||||
|
||||
.sourceViewer__loadingSpinner {
|
||||
margin-right: $euiSizeS;
|
||||
}
|
|
@ -7,17 +7,24 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import './source.scss';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { omit } from 'lodash';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { monaco } from '@kbn/monaco';
|
||||
import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiEmptyPrompt,
|
||||
EuiLoadingSpinner,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
type UseEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { DataTableRecord } from '@kbn/discover-utils/types';
|
||||
import { ElasticRequestState } from '@kbn/unified-doc-viewer';
|
||||
import { omit } from 'lodash';
|
||||
import { useMemoizedStyles } from '@kbn/core/public';
|
||||
import { useEsDocSearch } from '../../hooks';
|
||||
import { getHeight, DEFAULT_MARGIN_BOTTOM } from './get_height';
|
||||
import { JSONCodeEditorCommonMemoized } from '../json_code_editor';
|
||||
|
@ -44,6 +51,8 @@ export const DocViewerSource = ({
|
|||
decreaseAvailableHeightBy,
|
||||
onRefresh,
|
||||
}: SourceViewerProps) => {
|
||||
const styles = useMemoizedStyles(componentStyles);
|
||||
|
||||
const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor>();
|
||||
const [editorHeight, setEditorHeight] = useState<number>();
|
||||
const [jsonValue, setJsonValue] = useState<string>('');
|
||||
|
@ -84,8 +93,8 @@ export const DocViewerSource = ({
|
|||
}, [editor, jsonValue, setEditorHeight, decreaseAvailableHeightBy]);
|
||||
|
||||
const loadingState = (
|
||||
<div className="sourceViewer__loading">
|
||||
<EuiLoadingSpinner className="sourceViewer__loadingSpinner" />
|
||||
<div css={styles.loading}>
|
||||
<EuiLoadingSpinner css={styles.loadingSpinner} />
|
||||
<EuiText size="xs" color="subdued">
|
||||
<FormattedMessage id="unifiedDocViewer.loadingJSON" defaultMessage="Loading JSON" />
|
||||
</EuiText>
|
||||
|
@ -135,3 +144,21 @@ export const DocViewerSource = ({
|
|||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const componentStyles = {
|
||||
loading: ({ euiTheme }: UseEuiTheme) =>
|
||||
css({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'left',
|
||||
flex: '1 0 100%',
|
||||
textAlign: 'center',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
marginTop: euiTheme.size.s,
|
||||
}),
|
||||
loadingSpinner: ({ euiTheme }: UseEuiTheme) =>
|
||||
css({
|
||||
marginRight: euiTheme.size.s,
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
.kbnDocViewer__value {
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
line-height: $euiLineHeight;
|
||||
vertical-align: top;
|
||||
|
||||
&--highlighted {
|
||||
font-weight: $euiFontWeightBold;
|
||||
}
|
||||
|
||||
.euiDataGridRowCell__popover & {
|
||||
font-size: $euiFontSizeS;
|
||||
}
|
||||
}
|
||||
|
||||
.kbnDocViewer__fieldsGrid {
|
||||
&.euiDataGrid--noControls.euiDataGrid--bordersHorizontal .euiDataGridHeader {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
&.euiDataGrid--headerUnderline .euiDataGridHeader {
|
||||
border-bottom: $euiBorderThin;
|
||||
}
|
||||
|
||||
& [data-gridcell-column-id='name'] .euiDataGridRowCell__content {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
& [data-gridcell-column-id='pin_field'] .euiDataGridRowCell__content {
|
||||
padding: calc($euiSizeXS / 2) 0 0 $euiSizeXS;
|
||||
}
|
||||
|
||||
.kbnDocViewer__fieldsGrid__pinAction {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
& [data-gridcell-column-id='pin_field']:focus-within {
|
||||
.kbnDocViewer__fieldsGrid__pinAction {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.euiDataGridRow:hover .kbnDocViewer__fieldsGrid__pinAction {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
|
@ -7,7 +7,6 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import './table.scss';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import useWindowSize from 'react-use/lib/useWindowSize';
|
||||
import useLocalStorage from 'react-use/lib/useLocalStorage';
|
||||
|
@ -25,7 +24,7 @@ import {
|
|||
useResizeObserver,
|
||||
EuiSwitch,
|
||||
EuiSwitchEvent,
|
||||
useEuiTheme,
|
||||
type UseEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { css } from '@emotion/react';
|
||||
|
@ -39,6 +38,8 @@ import {
|
|||
canPrependTimeFieldColumn,
|
||||
} from '@kbn/discover-utils';
|
||||
import type { DocViewRenderProps } from '@kbn/unified-doc-viewer/types';
|
||||
import { useMemoizedStyles } from '@kbn/core/public';
|
||||
|
||||
import { getUnifiedDocViewerServices } from '../../plugin';
|
||||
import {
|
||||
getFieldCellActions,
|
||||
|
@ -130,7 +131,8 @@ export const DocViewerTable = ({
|
|||
onAddColumn,
|
||||
onRemoveColumn,
|
||||
}: DocViewRenderProps) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const styles = useMemoizedStyles(componentStyles);
|
||||
|
||||
const isEsqlMode = Array.isArray(textBasedHits);
|
||||
const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null);
|
||||
const { fieldFormats, storage, uiSettings, toasts } = getUnifiedDocViewerServices();
|
||||
|
@ -501,7 +503,7 @@ export const DocViewerTable = ({
|
|||
</EuiFlexItem>
|
||||
|
||||
{rows.length === 0 ? (
|
||||
<EuiSelectableMessage css={{ minHeight: 300 }}>
|
||||
<EuiSelectableMessage css={styles.noFieldsFound}>
|
||||
<p>
|
||||
<EuiI18n
|
||||
token="unifiedDocViewer.docViews.table.noFieldFound"
|
||||
|
@ -510,19 +512,7 @@ export const DocViewerTable = ({
|
|||
</p>
|
||||
</EuiSelectableMessage>
|
||||
) : (
|
||||
<EuiFlexItem
|
||||
grow={Boolean(containerHeight)}
|
||||
css={css`
|
||||
min-block-size: 0;
|
||||
display: block;
|
||||
.euiDataGridRow {
|
||||
&:hover {
|
||||
// we keep using a deprecated shade until proper token is available
|
||||
background-color: ${euiTheme.colors.lightestShade};
|
||||
}
|
||||
}
|
||||
`}
|
||||
>
|
||||
<EuiFlexItem grow={Boolean(containerHeight)} css={styles.fieldsGridWrapper}>
|
||||
<EuiDataGrid
|
||||
key={`fields-table-${hit.id}`}
|
||||
{...GRID_PROPS}
|
||||
|
@ -530,6 +520,7 @@ export const DocViewerTable = ({
|
|||
defaultMessage: 'Field values',
|
||||
})}
|
||||
className="kbnDocViewer__fieldsGrid"
|
||||
css={styles.fieldsGrid}
|
||||
columns={gridColumns}
|
||||
toolbarVisibility={false}
|
||||
rowCount={rows.length}
|
||||
|
@ -543,3 +534,54 @@ export const DocViewerTable = ({
|
|||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const componentStyles = {
|
||||
fieldsGridWrapper: ({ euiTheme }: UseEuiTheme) =>
|
||||
css({
|
||||
minBlockSize: 0,
|
||||
display: 'block',
|
||||
|
||||
'.euiDataGridRow': {
|
||||
'&:hover': {
|
||||
// we keep using a deprecated shade until proper token is available
|
||||
backgroundColor: euiTheme.colors.lightestShade,
|
||||
},
|
||||
},
|
||||
}),
|
||||
fieldsGrid: ({ euiTheme }: UseEuiTheme) =>
|
||||
css({
|
||||
'&.euiDataGrid--noControls.euiDataGrid--bordersHorizontal .euiDataGridHeader': {
|
||||
borderTop: 'none',
|
||||
},
|
||||
|
||||
'&.euiDataGrid--headerUnderline .euiDataGridHeader': {
|
||||
borderBottom: euiTheme.border.thin,
|
||||
},
|
||||
|
||||
'& [data-gridcell-column-id="name"] .euiDataGridRowCell__content': {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
},
|
||||
|
||||
'& [data-gridcell-column-id="pin_field"] .euiDataGridRowCell__content': {
|
||||
padding: `calc(${euiTheme.size.xs} / 2) 0 0 ${euiTheme.size.xs}`,
|
||||
},
|
||||
|
||||
'.kbnDocViewer__fieldsGrid__pinAction': {
|
||||
opacity: 0,
|
||||
},
|
||||
|
||||
'& [data-gridcell-column-id="pin_field"]:focus-within': {
|
||||
'.kbnDocViewer__fieldsGrid__pinAction': {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
|
||||
'.euiDataGridRow:hover .kbnDocViewer__fieldsGrid__pinAction': {
|
||||
opacity: 1,
|
||||
},
|
||||
}),
|
||||
noFieldsFound: css({
|
||||
minHeight: 300,
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -10,12 +10,17 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { userEvent } from '@testing-library/user-event';
|
||||
import { TableFieldValue } from './table_cell_value';
|
||||
import { TableFieldValue, DOC_VIEWER_DEFAULT_TRUNCATE_MAX_HEIGHT } from './table_cell_value';
|
||||
import { setUnifiedDocViewerServices } from '../../plugin';
|
||||
import { mockUnifiedDocViewerServices } from '../../__mocks__';
|
||||
|
||||
setUnifiedDocViewerServices(mockUnifiedDocViewerServices);
|
||||
|
||||
const truncationStyles = {
|
||||
overflow: 'hidden',
|
||||
maxHeight: `${DOC_VIEWER_DEFAULT_TRUNCATE_MAX_HEIGHT}px`,
|
||||
};
|
||||
|
||||
let mockScrollHeight = 0;
|
||||
jest.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockImplementation(() => mockScrollHeight);
|
||||
|
||||
|
@ -62,8 +67,8 @@ describe('TableFieldValue', () => {
|
|||
expect(toggleButton.getAttribute('aria-expanded')).toBe('false');
|
||||
|
||||
let valueElement = screen.getByTestId('tableDocViewRow-message-value');
|
||||
expect(valueElement.getAttribute('css')).toBeDefined();
|
||||
expect(valueElement.classList.contains('kbnDocViewer__value--truncated')).toBe(true);
|
||||
|
||||
expect(valueElement).toHaveStyle(truncationStyles);
|
||||
|
||||
await userEvent.click(toggleButton);
|
||||
|
||||
|
@ -72,8 +77,8 @@ describe('TableFieldValue', () => {
|
|||
expect(toggleButton.getAttribute('aria-expanded')).toBe('true');
|
||||
|
||||
valueElement = screen.getByTestId('tableDocViewRow-message-value');
|
||||
expect(valueElement.getAttribute('css')).toBeNull();
|
||||
expect(valueElement.classList.contains('kbnDocViewer__value--truncated')).toBe(false);
|
||||
|
||||
expect(valueElement).not.toHaveStyle(truncationStyles);
|
||||
|
||||
await userEvent.click(toggleButton);
|
||||
|
||||
|
@ -82,8 +87,8 @@ describe('TableFieldValue', () => {
|
|||
expect(toggleButton.getAttribute('aria-expanded')).toBe('false');
|
||||
|
||||
valueElement = screen.getByTestId('tableDocViewRow-message-value');
|
||||
expect(valueElement.getAttribute('css')).toBeDefined();
|
||||
expect(valueElement.classList.contains('kbnDocViewer__value--truncated')).toBe(true);
|
||||
|
||||
expect(valueElement).toHaveStyle(truncationStyles);
|
||||
});
|
||||
|
||||
it('should not truncate a long value when inside a popover', async () => {
|
||||
|
@ -104,7 +109,7 @@ describe('TableFieldValue', () => {
|
|||
expect(screen.queryByTestId('toggleLongFieldValue-message')).toBeNull();
|
||||
|
||||
const valueElement = screen.getByTestId('tableDocViewRow-message-value');
|
||||
expect(valueElement.getAttribute('css')).toBeNull();
|
||||
expect(valueElement.classList.contains('kbnDocViewer__value--truncated')).toBe(false);
|
||||
|
||||
expect(valueElement).not.toHaveStyle(truncationStyles);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,15 +15,15 @@ import {
|
|||
EuiIcon,
|
||||
EuiTextColor,
|
||||
EuiToolTip,
|
||||
useEuiTheme,
|
||||
useResizeObserver,
|
||||
type UseEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import classnames from 'classnames';
|
||||
import React, { Fragment, useCallback, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { IgnoredReason } from '@kbn/discover-utils';
|
||||
import { useMemoizedStyles } from '@kbn/core/public';
|
||||
|
||||
const DOC_VIEWER_DEFAULT_TRUNCATE_MAX_HEIGHT = 110;
|
||||
export const DOC_VIEWER_DEFAULT_TRUNCATE_MAX_HEIGHT = 110;
|
||||
|
||||
// Keep in memory what field values were expanded by the user and restore this state when the user opens DocViewer again
|
||||
const expandedFieldValuesSet = new Set<string>();
|
||||
|
@ -111,7 +111,8 @@ export const TableFieldValue = ({
|
|||
isDetails,
|
||||
isHighlighted,
|
||||
}: TableFieldValueProps) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const styles = useMemoizedStyles(componentStyles);
|
||||
|
||||
const truncationHeight = DOC_VIEWER_DEFAULT_TRUNCATE_MAX_HEIGHT;
|
||||
|
||||
const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null);
|
||||
|
@ -152,11 +153,6 @@ export const TableFieldValue = ({
|
|||
const shouldTruncate = isCollapsible && isCollapsed;
|
||||
const valueElementId = `tableDocViewRow-${field}-value`;
|
||||
|
||||
const valueClasses = classnames('kbnDocViewer__value', {
|
||||
'kbnDocViewer__value--truncated': shouldTruncate,
|
||||
'kbnDocViewer__value--highlighted': isHighlighted && !isDetails,
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{ignoreReason && (
|
||||
|
@ -168,12 +164,7 @@ export const TableFieldValue = ({
|
|||
)}
|
||||
<EuiFlexGroup gutterSize="s" direction="row" alignItems="flexStart">
|
||||
{isCollapsible && (
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
css={css`
|
||||
margin-top: -${euiTheme.size.xxs};
|
||||
`}
|
||||
>
|
||||
<EuiFlexItem grow={false} css={styles.collapseButtonWrapper}>
|
||||
<EuiButtonIcon
|
||||
iconType={isCollapsed ? 'plusInSquare' : 'minusInSquare'}
|
||||
size="xs"
|
||||
|
@ -190,17 +181,12 @@ export const TableFieldValue = ({
|
|||
<EuiFlexItem>
|
||||
<div
|
||||
ref={setContainerRef}
|
||||
className={valueClasses}
|
||||
css={
|
||||
shouldTruncate
|
||||
? css`
|
||||
&.kbnDocViewer__value--truncated {
|
||||
max-height: ${truncationHeight}px;
|
||||
overflow: hidden;
|
||||
}
|
||||
`
|
||||
: undefined
|
||||
}
|
||||
className="kbnDocViewer__value"
|
||||
css={[
|
||||
styles.docViewerValue,
|
||||
isHighlighted && !isDetails && styles.docViewerValueHighlighted,
|
||||
shouldTruncate && styles.docViewerValueTruncated,
|
||||
]}
|
||||
id={valueElementId}
|
||||
data-test-subj={valueElementId}
|
||||
// Value returned from formatFieldValue is always sanitized
|
||||
|
@ -212,3 +198,30 @@ export const TableFieldValue = ({
|
|||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const componentStyles = {
|
||||
docViewerValue: ({ euiTheme }: UseEuiTheme) =>
|
||||
css({
|
||||
wordBreak: 'break-all',
|
||||
wordWrap: 'break-word',
|
||||
whiteSpace: 'pre-wrap',
|
||||
lineHeight: euiTheme.font.lineHeightMultiplier,
|
||||
verticalAlign: 'top',
|
||||
|
||||
'.euiDataGridRowCell__popover &': {
|
||||
fontSize: euiTheme.font.scale.s,
|
||||
},
|
||||
}),
|
||||
docViewerValueHighlighted: ({ euiTheme }: UseEuiTheme) =>
|
||||
css({
|
||||
fontWeight: euiTheme.font.weight.bold,
|
||||
}),
|
||||
docViewerValueTruncated: css({
|
||||
overflow: 'hidden',
|
||||
maxHeight: DOC_VIEWER_DEFAULT_TRUNCATE_MAX_HEIGHT,
|
||||
}),
|
||||
collapseButtonWrapper: ({ euiTheme }: UseEuiTheme) =>
|
||||
css({
|
||||
marginTop: -euiTheme.size.xxs,
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
.dscJsonCodeEditor {
|
||||
width: 100%;
|
||||
}
|
|
@ -7,8 +7,6 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import './json_code_editor.scss';
|
||||
|
||||
import React from 'react';
|
||||
import { JsonCodeEditorCommon } from './json_code_editor_common';
|
||||
|
||||
|
|
|
@ -7,9 +7,8 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import './json_code_editor.scss';
|
||||
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { monaco, XJsonLang } from '@kbn/monaco';
|
||||
import { EuiButtonEmpty, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
|
@ -75,7 +74,7 @@ export const JsonCodeEditorCommon = ({
|
|||
return codeEditor;
|
||||
}
|
||||
return (
|
||||
<EuiFlexGroup className="dscJsonCodeEditor" direction="column" gutterSize="s">
|
||||
<EuiFlexGroup css={styles.codeEditor} direction="column" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="s" />
|
||||
<div className="eui-textRight">
|
||||
|
@ -96,3 +95,9 @@ export const JsonCodeEditorCommon = ({
|
|||
export const JSONCodeEditorCommonMemoized = React.memo((props: JsonCodeEditorCommonProps) => {
|
||||
return <JsonCodeEditorCommon {...props} />;
|
||||
});
|
||||
|
||||
const styles = {
|
||||
codeEditor: css`
|
||||
height: 100%;
|
||||
`,
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ import type { SerializedSearchSourceFields } from '@kbn/data-plugin/public';
|
|||
import { extractSearchSourceReferences } from '@kbn/data-plugin/public';
|
||||
import { SerializedTitles, SerializedPanelState } from '@kbn/presentation-publishing';
|
||||
import { cloneDeep, isEmpty, omit } from 'lodash';
|
||||
import { DynamicActionsSerializedState } from '@kbn/embeddable-enhanced-plugin/public';
|
||||
import { Reference } from '../../common/content_management';
|
||||
import {
|
||||
getAnalytics,
|
||||
|
@ -179,7 +180,7 @@ export const serializeState: (props: {
|
|||
id?: string;
|
||||
savedObjectProperties?: ExtraSavedObjectProperties;
|
||||
linkedToLibrary?: boolean;
|
||||
enhancements?: VisualizeRuntimeState['enhancements'];
|
||||
serializeDynamicActions?: (() => SerializedPanelState<DynamicActionsSerializedState>) | undefined;
|
||||
timeRange?: VisualizeRuntimeState['timeRange'];
|
||||
}) => Required<SerializedPanelState<VisualizeSerializedState>> = ({
|
||||
serializedVis, // Serialize the vis before passing it to this function for easier testing
|
||||
|
@ -187,11 +188,14 @@ export const serializeState: (props: {
|
|||
id,
|
||||
savedObjectProperties,
|
||||
linkedToLibrary,
|
||||
enhancements,
|
||||
serializeDynamicActions,
|
||||
timeRange,
|
||||
}) => {
|
||||
const { references, serializedSearchSource } = serializeReferences(serializedVis);
|
||||
|
||||
const { rawState: dynamicActionsState, references: dynamicActionsReferences } =
|
||||
serializeDynamicActions?.() ?? {};
|
||||
|
||||
// Serialize ONLY the savedObjectId. This ensures that when this vis is loaded again, it will always fetch the
|
||||
// latest revision of the saved object
|
||||
if (linkedToLibrary) {
|
||||
|
@ -199,11 +203,11 @@ export const serializeState: (props: {
|
|||
rawState: {
|
||||
...(titles ? titles : {}),
|
||||
savedObjectId: id,
|
||||
...(enhancements ? { enhancements } : {}),
|
||||
...dynamicActionsState,
|
||||
...(!isEmpty(serializedVis.uiState) ? { uiState: serializedVis.uiState } : {}),
|
||||
...(timeRange ? { timeRange } : {}),
|
||||
} as VisualizeSavedObjectInputState,
|
||||
references,
|
||||
references: [...references, ...(dynamicActionsReferences ?? [])],
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -215,7 +219,7 @@ export const serializeState: (props: {
|
|||
rawState: {
|
||||
...(titles ? titles : {}),
|
||||
...savedObjectProperties,
|
||||
...(enhancements ? { enhancements } : {}),
|
||||
...dynamicActionsState,
|
||||
...(timeRange ? { timeRange } : {}),
|
||||
savedVis: {
|
||||
...serializedVis,
|
||||
|
@ -231,6 +235,6 @@ export const serializeState: (props: {
|
|||
},
|
||||
},
|
||||
} as VisualizeSavedVisInputState,
|
||||
references,
|
||||
references: [...references, ...(dynamicActionsReferences ?? [])],
|
||||
};
|
||||
};
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
import type { OverlayRef } from '@kbn/core-mount-utils-browser';
|
||||
import { DynamicActionsSerializedState } from '@kbn/embeddable-enhanced-plugin/public/plugin';
|
||||
import { DynamicActionsSerializedState } from '@kbn/embeddable-enhanced-plugin/public';
|
||||
import { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public';
|
||||
import type { TimeRange } from '@kbn/es-query';
|
||||
import { HasInspectorAdapters } from '@kbn/inspector-plugin/public';
|
||||
|
|
|
@ -71,7 +71,7 @@ export const getVisualizeEmbeddableFactory: (deps: {
|
|||
const dynamicActionsManager = embeddableEnhancedStart?.initializeEmbeddableDynamicActions(
|
||||
uuid,
|
||||
() => titleManager.api.title$.getValue(),
|
||||
initialState.rawState
|
||||
initialState
|
||||
);
|
||||
// if it is provided, start the dynamic actions manager
|
||||
const maybeStopDynamicActions = dynamicActionsManager?.startDynamicActions();
|
||||
|
@ -165,7 +165,7 @@ export const getVisualizeEmbeddableFactory: (deps: {
|
|||
...(runtimeState.savedObjectProperties
|
||||
? { savedObjectProperties: runtimeState.savedObjectProperties }
|
||||
: {}),
|
||||
...(dynamicActionsManager?.getLatestState() ?? {}),
|
||||
serializeDynamicActions: dynamicActionsManager?.serializeState,
|
||||
...timeRangeManager.getLatestState(),
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,131 +0,0 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const dashboardVisualizations = getService('dashboardVisualizations');
|
||||
const dashboardPanelActions = getService('dashboardPanelActions');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
const { dashboard, timePicker } = getPageObjects(['dashboard', 'timePicker']);
|
||||
|
||||
const fewPanelsTitle = 'few panels';
|
||||
const markdownTitle = 'Copy To Markdown';
|
||||
let fewPanelsPanelCount = 0;
|
||||
|
||||
const openCopyToModal = async (panelName: string) => {
|
||||
await dashboardPanelActions.openCopyToModalByTitle(panelName);
|
||||
const modalIsOpened = await testSubjects.exists('copyToDashboardPanel');
|
||||
expect(modalIsOpened).to.be(true);
|
||||
const hasDashboardSelector = await testSubjects.exists('add-to-dashboard-options');
|
||||
expect(hasDashboardSelector).to.be(true);
|
||||
};
|
||||
|
||||
describe('dashboard panel copy to', function viewEditModeTests() {
|
||||
before(async function () {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
await kibanaServer.importExport.load(
|
||||
'src/platform/test/functional/fixtures/kbn_archiver/dashboard/current/kibana'
|
||||
);
|
||||
await kibanaServer.uiSettings.replace({
|
||||
defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c',
|
||||
});
|
||||
await dashboard.navigateToApp();
|
||||
await dashboard.preserveCrossAppState();
|
||||
await dashboard.loadSavedDashboard(fewPanelsTitle);
|
||||
await dashboard.waitForRenderComplete();
|
||||
fewPanelsPanelCount = await dashboard.getPanelCount();
|
||||
|
||||
await dashboard.gotoDashboardLandingPage();
|
||||
await dashboard.clickNewDashboard();
|
||||
await timePicker.setHistoricalDataRange();
|
||||
await dashboardVisualizations.createAndAddMarkdown({
|
||||
name: markdownTitle,
|
||||
markdown: 'Please add me to some other dashboard',
|
||||
});
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await dashboard.gotoDashboardLandingPage();
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
it('does not show the new dashboard option when on a new dashboard', async () => {
|
||||
await openCopyToModal(markdownTitle);
|
||||
const dashboardSelector = await testSubjects.find('add-to-dashboard-options');
|
||||
const isDisabled = await dashboardSelector.findByCssSelector(
|
||||
`input[id="new-dashboard-option"]:disabled`
|
||||
);
|
||||
expect(isDisabled).not.to.be(null);
|
||||
|
||||
await testSubjects.click('cancelCopyToButton');
|
||||
});
|
||||
|
||||
it('copies a panel to an existing dashboard', async () => {
|
||||
await openCopyToModal(markdownTitle);
|
||||
const dashboardSelector = await testSubjects.find('add-to-dashboard-options');
|
||||
const label = await dashboardSelector.findByCssSelector(
|
||||
`label[for="existing-dashboard-option"]`
|
||||
);
|
||||
await label.click();
|
||||
|
||||
await testSubjects.click('open-dashboard-picker');
|
||||
await testSubjects.setValue('dashboard-picker-search', fewPanelsTitle);
|
||||
await testSubjects.existOrFail(`dashboard-picker-option-few-panels`);
|
||||
await testSubjects.click(`dashboard-picker-option-few-panels`);
|
||||
await testSubjects.click('confirmCopyToButton');
|
||||
|
||||
await dashboard.waitForRenderComplete();
|
||||
await dashboard.expectOnDashboard(`Editing ${fewPanelsTitle}`);
|
||||
const newPanelCount = await dashboard.getPanelCount();
|
||||
expect(newPanelCount).to.be(fewPanelsPanelCount + 1);
|
||||
|
||||
// Save & ensure that view mode is applied properly.
|
||||
await dashboard.clickQuickSave();
|
||||
await testSubjects.existOrFail('saveDashboardSuccess');
|
||||
|
||||
await dashboard.clickCancelOutOfEditMode();
|
||||
await dashboardPanelActions.expectMissingEditPanelAction(markdownTitle);
|
||||
});
|
||||
|
||||
it('does not show the current dashboard in the dashboard picker', async () => {
|
||||
await openCopyToModal(markdownTitle);
|
||||
const dashboardSelector = await testSubjects.find('add-to-dashboard-options');
|
||||
const label = await dashboardSelector.findByCssSelector(
|
||||
`label[for="existing-dashboard-option"]`
|
||||
);
|
||||
await label.click();
|
||||
|
||||
await testSubjects.click('open-dashboard-picker');
|
||||
await testSubjects.setValue('dashboard-picker-search', fewPanelsTitle);
|
||||
await testSubjects.missingOrFail(`dashboard-picker-option-few-panels`);
|
||||
|
||||
await testSubjects.click('cancelCopyToButton');
|
||||
});
|
||||
|
||||
it('copies a panel to a new dashboard', async () => {
|
||||
await openCopyToModal(markdownTitle);
|
||||
const dashboardSelector = await testSubjects.find('add-to-dashboard-options');
|
||||
const label = await dashboardSelector.findByCssSelector(`label[for="new-dashboard-option"]`);
|
||||
await label.click();
|
||||
await testSubjects.click('confirmCopyToButton');
|
||||
|
||||
await dashboard.waitForRenderComplete();
|
||||
await dashboard.expectOnDashboard(`Editing New Dashboard`);
|
||||
});
|
||||
|
||||
it('it always appends new panels instead of overwriting', async () => {
|
||||
const newPanelCount = await dashboard.getPanelCount();
|
||||
expect(newPanelCount).to.be(2);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -36,7 +36,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
|||
} else {
|
||||
loadTestFile(require.resolve('./dashboard_time_picker'));
|
||||
loadTestFile(require.resolve('./bwc_short_urls'));
|
||||
loadTestFile(require.resolve('./copy_panel_to'));
|
||||
loadTestFile(require.resolve('./panel_context_menu'));
|
||||
loadTestFile(require.resolve('./dashboard_state'));
|
||||
}
|
||||
|
|
|
@ -6,3 +6,4 @@
|
|||
*/
|
||||
|
||||
export * from './src/es_fields/apm';
|
||||
export * from './src/es_fields/otel';
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const STATUS_CODE = 'status.code';
|
||||
export const ERROR_MESSAGE = 'error.message';
|
||||
export const EXCEPTION_TYPE = 'exception.type';
|
||||
export const EXCEPTION_MESSAGE = 'exception.message';
|
||||
export const DURATION = 'duration';
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { EmbeddableStart } from '@kbn/embeddable-plugin/public';
|
||||
import type { EmbeddableStart } from '@kbn/embeddable-plugin/server';
|
||||
import { embeddableFunctionFactory } from './embeddable';
|
||||
import { savedLens } from './saved_lens';
|
||||
import { savedMap } from './saved_map';
|
||||
|
|
|
@ -11,6 +11,8 @@ import { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/publi
|
|||
import { EmbeddableStart } from '@kbn/embeddable-plugin/public';
|
||||
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import { Start as InspectorStart } from '@kbn/inspector-plugin/public';
|
||||
import type { EmbeddableStateWithType } from '@kbn/embeddable-plugin/server';
|
||||
import type { Reference } from '@kbn/content-management-utils';
|
||||
import { CanvasSetup } from '../public';
|
||||
|
||||
import { functions } from './functions/browser';
|
||||
|
@ -44,9 +46,12 @@ export class CanvasSrcPlugin implements Plugin<void, void, SetupDeps, StartDeps>
|
|||
core.getStartServices().then(([coreStart, depsStart]) => {
|
||||
const externalFunctions = initFunctions({
|
||||
embeddablePersistableStateService: {
|
||||
extract: depsStart.embeddable.extract,
|
||||
inject: depsStart.embeddable.inject,
|
||||
getAllMigrations: depsStart.embeddable.getAllMigrations,
|
||||
extract: (state: EmbeddableStateWithType) => ({
|
||||
state,
|
||||
references: [],
|
||||
}),
|
||||
inject: (state: EmbeddableStateWithType, references: Reference[]) => state,
|
||||
getAllMigrations: () => ({}),
|
||||
},
|
||||
});
|
||||
plugins.canvas.addFunctions(externalFunctions);
|
||||
|
|
|
@ -85,7 +85,8 @@
|
|||
"@kbn/visualizations-plugin",
|
||||
"@kbn/react-kibana-context-theme",
|
||||
"@kbn/shared-ux-error-boundary",
|
||||
"@kbn/shared-ux-markdown"
|
||||
"@kbn/shared-ux-markdown",
|
||||
"@kbn/content-management-utils"
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -240,7 +240,7 @@ export const useDataVisualizerGridData = (
|
|||
return;
|
||||
}
|
||||
const fieldName = field.displayName !== undefined ? field.displayName : field.name;
|
||||
if (!OMIT_FIELDS.includes(fieldName)) {
|
||||
if (field.type !== SUPPORTED_FIELD_TYPES.UNKNOWN && !OMIT_FIELDS.includes(fieldName)) {
|
||||
if (
|
||||
field.aggregatable === true &&
|
||||
!NON_AGGREGATABLE_FIELD_TYPES.has(field.type) &&
|
||||
|
|
|
@ -10609,7 +10609,6 @@
|
|||
"xpack.alerting.rulesClient.validateActions.misconfiguredConnector": "Connecteurs non valides : {groups}",
|
||||
"xpack.alerting.rulesClient.validateActions.mixAndMatchFreqParams": "Impossible de spécifier des paramètres de fréquence par action lorsque notify_when et throttle sont définis au niveau de la règle : {groups}",
|
||||
"xpack.alerting.rulesClient.validateActions.notAllActionsWithFreq": "Paramètres de fréquence manquants pour les actions : {groups}",
|
||||
"xpack.alerting.rulesClient.validateLegacyActions.errorSummary": "Échec de la migration des actions existantes pour la règle SIEM {ruleId} : {errorMessage}",
|
||||
"xpack.alerting.ruleTypeRegistry.get.missingRuleTypeError": "Le type de règle \"{id}\" n'est pas enregistré.",
|
||||
"xpack.alerting.ruleTypeRegistry.register.customRecoveryActionGroupUsageError": "Le type de règle [id=\"{id}\"] ne peut pas être enregistré. Le groupe d'actions [{actionGroup}] ne peut pas être utilisé à la fois comme groupe de récupération et comme groupe d'actions actif.",
|
||||
"xpack.alerting.ruleTypeRegistry.register.duplicateActionGroupSeverityError": "Le type de règle [id=\"{id}\"] ne peut pas être enregistré. Les définitions de groupe d'action ne peuvent pas contenir des niveaux de sévérité en doublon.",
|
||||
|
|
|
@ -10599,7 +10599,6 @@
|
|||
"xpack.alerting.rulesClient.validateActions.misconfiguredConnector": "無効なコネクター:{groups}",
|
||||
"xpack.alerting.rulesClient.validateActions.mixAndMatchFreqParams": "notify_whenとスロットルがルールレベルで定義されているときには、アクション単位の頻度パラメーターを指定できません:{groups}",
|
||||
"xpack.alerting.rulesClient.validateActions.notAllActionsWithFreq": "アクションの頻度パラメーターがありません:{groups}",
|
||||
"xpack.alerting.rulesClient.validateLegacyActions.errorSummary": "SIEMルール{ruleId}のレガシーアクションの移行に失敗しました:{errorMessage}",
|
||||
"xpack.alerting.ruleTypeRegistry.get.missingRuleTypeError": "ルールタイプ「{id}」は登録されていません。",
|
||||
"xpack.alerting.ruleTypeRegistry.register.customRecoveryActionGroupUsageError": "ルールタイプ[id=\"{id}\"]を登録できません。アクショングループ [{actionGroup}] は、復元とアクティブなアクショングループの両方として使用できません。",
|
||||
"xpack.alerting.ruleTypeRegistry.register.duplicateActionGroupSeverityError": "ルールタイプ[id=\"{id}\"]を登録できません。アクショングループ定義には、重複する重要度レベルを含めることはできません。",
|
||||
|
|
|
@ -10616,7 +10616,6 @@
|
|||
"xpack.alerting.rulesClient.validateActions.misconfiguredConnector": "无效的连接器:{groups}",
|
||||
"xpack.alerting.rulesClient.validateActions.mixAndMatchFreqParams": "在规则级别定义了 notify_when 或限制时,无法指定每个操作的频率参数:{groups}",
|
||||
"xpack.alerting.rulesClient.validateActions.notAllActionsWithFreq": "操作缺少频率参数:{groups}",
|
||||
"xpack.alerting.rulesClient.validateLegacyActions.errorSummary": "无法迁移 SIEM 规则 {ruleId} 的旧版操作:{errorMessage}",
|
||||
"xpack.alerting.ruleTypeRegistry.get.missingRuleTypeError": "未注册规则类型“{id}”。",
|
||||
"xpack.alerting.ruleTypeRegistry.register.customRecoveryActionGroupUsageError": "无法注册规则类型 [id=\"{id}\"]。操作组 [{actionGroup}] 无法同时用作恢复和活动操作组。",
|
||||
"xpack.alerting.ruleTypeRegistry.register.duplicateActionGroupSeverityError": "无法注册规则类型 [id=\"{id}\"]。操作组定义不能包含重复的严重性级别。",
|
||||
|
|
|
@ -33,28 +33,15 @@ import {
|
|||
returnedRuleForBulkOps1,
|
||||
returnedRuleForBulkOps2,
|
||||
returnedRuleForBulkOps3,
|
||||
siemRuleForBulkOps1,
|
||||
enabledRuleForBulkOpsWithActions1,
|
||||
enabledRuleForBulkOpsWithActions2,
|
||||
returnedRuleForBulkEnableWithActions1,
|
||||
returnedRuleForBulkEnableWithActions2,
|
||||
} from '../../../../rules_client/tests/test_helpers';
|
||||
import { migrateLegacyActions } from '../../../../rules_client/lib';
|
||||
import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
|
||||
import { backfillClientMock } from '../../../../backfill_client/backfill_client.mock';
|
||||
|
||||
jest.mock('../../../../rules_client/lib/siem_legacy_actions/migrate_legacy_actions', () => {
|
||||
return {
|
||||
migrateLegacyActions: jest.fn(),
|
||||
};
|
||||
});
|
||||
(migrateLegacyActions as jest.Mock).mockResolvedValue({
|
||||
hasLegacyActions: false,
|
||||
resultedActions: [],
|
||||
resultedReferences: [],
|
||||
});
|
||||
|
||||
jest.mock('../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({
|
||||
bulkMarkApiKeysForInvalidation: jest.fn(),
|
||||
}));
|
||||
|
@ -539,48 +526,6 @@ describe('bulkDelete', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('legacy actions migration for SIEM', () => {
|
||||
test('should call migrateLegacyActions', async () => {
|
||||
encryptedSavedObjects.createPointInTimeFinderDecryptedAsInternalUser = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
close: jest.fn(),
|
||||
find: function* asyncGenerator() {
|
||||
yield {
|
||||
saved_objects: [enabledRuleForBulkOps1, enabledRuleForBulkOps2, siemRuleForBulkOps1],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
unsecuredSavedObjectsClient.bulkDelete.mockResolvedValue({
|
||||
statuses: [
|
||||
{ id: enabledRuleForBulkOps1.id, type: RULE_SAVED_OBJECT_TYPE, success: true },
|
||||
{ id: enabledRuleForBulkOps2.id, type: RULE_SAVED_OBJECT_TYPE, success: true },
|
||||
{ id: siemRuleForBulkOps1.id, type: RULE_SAVED_OBJECT_TYPE, success: true },
|
||||
],
|
||||
});
|
||||
|
||||
await rulesClient.bulkDeleteRules({ filter: 'fake_filter' });
|
||||
|
||||
expect(migrateLegacyActions).toHaveBeenCalledTimes(3);
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
ruleId: enabledRuleForBulkOps1.id,
|
||||
skipActionsValidation: true,
|
||||
attributes: enabledRuleForBulkOps1.attributes,
|
||||
});
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
ruleId: enabledRuleForBulkOps2.id,
|
||||
skipActionsValidation: true,
|
||||
attributes: enabledRuleForBulkOps2.attributes,
|
||||
});
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
ruleId: siemRuleForBulkOps1.id,
|
||||
skipActionsValidation: true,
|
||||
attributes: siemRuleForBulkOps1.attributes,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('auditLogger', () => {
|
||||
jest.spyOn(auditLogger, 'log').mockImplementation();
|
||||
|
||||
|
|
|
@ -4,22 +4,21 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import pMap from 'p-map';
|
||||
|
||||
import Boom from '@hapi/boom';
|
||||
import type { KueryNode } from '@kbn/es-query';
|
||||
import { nodeBuilder } from '@kbn/es-query';
|
||||
import type { SavedObjectsBulkUpdateObject } from '@kbn/core/server';
|
||||
import type { SavedObject } from '@kbn/core/server';
|
||||
import { withSpan } from '@kbn/apm-utils';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
|
||||
import { convertRuleIdsToKueryNode } from '../../../../lib';
|
||||
import { bulkMarkApiKeysForInvalidation } from '../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
|
||||
import { ruleAuditEvent, RuleAuditAction } from '../../../../rules_client/common/audit_events';
|
||||
import { tryToRemoveTasks } from '../../../../rules_client/common';
|
||||
import { API_KEY_GENERATE_CONCURRENCY } from '../../../../rules_client/common/constants';
|
||||
import {
|
||||
getAuthorizationFilter,
|
||||
checkAuthorizationAndGetTotal,
|
||||
migrateLegacyActions,
|
||||
bulkMigrateLegacyActions,
|
||||
} from '../../../../rules_client/lib';
|
||||
import {
|
||||
retryIfBulkOperationConflicts,
|
||||
|
@ -152,7 +151,7 @@ const bulkDeleteWithOCC = async (
|
|||
})
|
||||
);
|
||||
|
||||
const rulesToDelete: Array<SavedObjectsBulkUpdateObject<RawRule>> = [];
|
||||
const rulesToDelete: Array<SavedObject<RawRule>> = [];
|
||||
const apiKeyToRuleIdMapping: Record<string, string> = {};
|
||||
const taskIdToRuleIdMapping: Record<string, string> = {};
|
||||
const ruleNameToRuleIdMapping: Record<string, string> = {};
|
||||
|
@ -161,6 +160,7 @@ const bulkDeleteWithOCC = async (
|
|||
{ name: 'Get rules, collect them and their attributes', type: 'rules' },
|
||||
async () => {
|
||||
for await (const response of rulesFinder.find()) {
|
||||
await bulkMigrateLegacyActions({ context, rules: response.saved_objects });
|
||||
for (const rule of response.saved_objects) {
|
||||
if (rule.attributes.apiKey && !rule.attributes.apiKeyCreatedByUser) {
|
||||
apiKeyToRuleIdMapping[rule.id] = rule.attributes.apiKey;
|
||||
|
@ -231,21 +231,6 @@ const bulkDeleteWithOCC = async (
|
|||
});
|
||||
const rules = rulesToDelete.filter((rule) => deletedRuleIds.includes(rule.id));
|
||||
|
||||
// migrate legacy actions only for SIEM rules
|
||||
// TODO (http-versioning) Remove RawRule casts
|
||||
await pMap(
|
||||
rules,
|
||||
async (rule) => {
|
||||
await migrateLegacyActions(context, {
|
||||
ruleId: rule.id,
|
||||
attributes: rule.attributes as RawRule,
|
||||
skipActionsValidation: true,
|
||||
});
|
||||
},
|
||||
// max concurrency for bulk edit operations, that is limited by api key generations, should be sufficient for bulk migrations
|
||||
{ concurrency: API_KEY_GENERATE_CONCURRENCY }
|
||||
);
|
||||
|
||||
return {
|
||||
errors,
|
||||
rules,
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
|
||||
import type { ConstructorOptions } from '../../../../rules_client/rules_client';
|
||||
import { RulesClient } from '../../../../rules_client/rules_client';
|
||||
import {
|
||||
|
@ -40,25 +40,12 @@ import {
|
|||
disabledRuleForBulkOpsWithActions2,
|
||||
returnedRuleForBulkDisable1,
|
||||
returnedRuleForBulkDisable2,
|
||||
siemRuleForBulkOps1,
|
||||
siemRuleForBulkOps2,
|
||||
} from '../../../../rules_client/tests/test_helpers';
|
||||
import { migrateLegacyActions } from '../../../../rules_client/lib';
|
||||
import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry';
|
||||
import type { ActionsClient } from '@kbn/actions-plugin/server';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
|
||||
import { backfillClientMock } from '../../../../backfill_client/backfill_client.mock';
|
||||
|
||||
jest.mock('../../../../rules_client/lib/siem_legacy_actions/migrate_legacy_actions', () => {
|
||||
return {
|
||||
migrateLegacyActions: jest.fn().mockResolvedValue({
|
||||
hasLegacyActions: false,
|
||||
resultedActions: [],
|
||||
resultedReferences: [],
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({
|
||||
bulkMarkApiKeysForInvalidation: jest.fn(),
|
||||
}));
|
||||
|
@ -166,11 +153,6 @@ describe('bulkDisableRules', () => {
|
|||
});
|
||||
mockCreatePointInTimeFinderAsInternalUser();
|
||||
mockUnsecuredSavedObjectFind(2);
|
||||
(migrateLegacyActions as jest.Mock).mockResolvedValue({
|
||||
hasLegacyActions: false,
|
||||
resultedActions: [],
|
||||
resultedReferences: [],
|
||||
});
|
||||
actionsClient.isSystemAction.mockImplementation((id: string) => id === 'system_action:id');
|
||||
});
|
||||
|
||||
|
@ -776,73 +758,4 @@ describe('bulkDisableRules', () => {
|
|||
expect(auditLogger.log.mock.calls[0][0]?.event?.outcome).toEqual('failure');
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacy actions migration for SIEM', () => {
|
||||
test('should call migrateLegacyActions', async () => {
|
||||
encryptedSavedObjects.createPointInTimeFinderDecryptedAsInternalUser = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
close: jest.fn(),
|
||||
find: function* asyncGenerator() {
|
||||
yield {
|
||||
saved_objects: [
|
||||
enabledRuleForBulkOps1,
|
||||
enabledRuleForBulkOps2,
|
||||
{
|
||||
...siemRuleForBulkOps1,
|
||||
attributes: { ...siemRuleForBulkOps1.attributes, enabled: true },
|
||||
},
|
||||
{
|
||||
...siemRuleForBulkOps2,
|
||||
attributes: { ...siemRuleForBulkOps2.attributes, enabled: true },
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: [
|
||||
enabledRuleForBulkOps1,
|
||||
enabledRuleForBulkOps2,
|
||||
{
|
||||
...siemRuleForBulkOps1,
|
||||
attributes: { ...siemRuleForBulkOps1.attributes, enabled: true },
|
||||
},
|
||||
{
|
||||
...siemRuleForBulkOps2,
|
||||
attributes: { ...siemRuleForBulkOps2.attributes, enabled: true },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await rulesClient.bulkDisableRules({ filter: 'fake_filter' });
|
||||
|
||||
expect(migrateLegacyActions).toHaveBeenCalledTimes(4);
|
||||
expect(migrateLegacyActions).toHaveBeenNthCalledWith(1, expect.any(Object), {
|
||||
attributes: enabledRuleForBulkOps1.attributes,
|
||||
ruleId: enabledRuleForBulkOps1.id,
|
||||
actions: [],
|
||||
references: [],
|
||||
});
|
||||
expect(migrateLegacyActions).toHaveBeenNthCalledWith(2, expect.any(Object), {
|
||||
attributes: enabledRuleForBulkOps2.attributes,
|
||||
ruleId: enabledRuleForBulkOps2.id,
|
||||
actions: [],
|
||||
references: [],
|
||||
});
|
||||
expect(migrateLegacyActions).toHaveBeenNthCalledWith(3, expect.any(Object), {
|
||||
attributes: expect.objectContaining({ consumer: AlertConsumers.SIEM }),
|
||||
ruleId: siemRuleForBulkOps1.id,
|
||||
actions: [],
|
||||
references: [],
|
||||
});
|
||||
expect(migrateLegacyActions).toHaveBeenNthCalledWith(4, expect.any(Object), {
|
||||
attributes: expect.objectContaining({ consumer: AlertConsumers.SIEM }),
|
||||
ruleId: siemRuleForBulkOps2.id,
|
||||
actions: [],
|
||||
references: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,7 +13,7 @@ import pMap from 'p-map';
|
|||
import type { Logger } from '@kbn/core/server';
|
||||
import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
|
||||
import type { RawRule, SanitizedRule, RawRuleAction } from '../../../../types';
|
||||
import type { RawRule, SanitizedRule } from '../../../../types';
|
||||
import { convertRuleIdsToKueryNode } from '../../../../lib';
|
||||
import { ruleAuditEvent, RuleAuditAction } from '../../../../rules_client/common/audit_events';
|
||||
import {
|
||||
|
@ -26,7 +26,7 @@ import {
|
|||
checkAuthorizationAndGetTotal,
|
||||
untrackRuleAlerts,
|
||||
updateMeta,
|
||||
migrateLegacyActions,
|
||||
bulkMigrateLegacyActions,
|
||||
} from '../../../../rules_client/lib';
|
||||
import { transformRuleAttributesToRuleDomain, transformRuleDomainToRule } from '../../transforms';
|
||||
import type {
|
||||
|
@ -156,6 +156,7 @@ const bulkDisableRulesWithOCC = async (
|
|||
{ name: 'Get rules, collect them and their attributes', type: 'rules' },
|
||||
async () => {
|
||||
for await (const response of rulesFinder.find()) {
|
||||
await bulkMigrateLegacyActions({ context, rules: response.saved_objects });
|
||||
await pMap(response.saved_objects, async (rule) => {
|
||||
const ruleName = rule.attributes.name;
|
||||
|
||||
|
@ -168,26 +169,10 @@ const bulkDisableRulesWithOCC = async (
|
|||
ruleNameToRuleIdMapping[rule.id] = ruleName;
|
||||
}
|
||||
|
||||
// migrate legacy actions only for SIEM rules
|
||||
// TODO (http-versioning) Remove RawRuleAction and RawRule casts
|
||||
const migratedActions = await migrateLegacyActions(context, {
|
||||
ruleId: rule.id,
|
||||
actions: rule.attributes.actions as RawRuleAction[],
|
||||
references: rule.references,
|
||||
attributes: rule.attributes as RawRule,
|
||||
});
|
||||
|
||||
// TODO (http-versioning) Remove casts when updateMeta has been converted
|
||||
const castedAttributes = rule.attributes as RawRule;
|
||||
const updatedAttributes = updateMeta(context, {
|
||||
...castedAttributes,
|
||||
...(migratedActions.hasLegacyActions
|
||||
? {
|
||||
actions: migratedActions.resultedActions,
|
||||
throttle: undefined,
|
||||
notifyWhen: undefined,
|
||||
}
|
||||
: {}),
|
||||
enabled: false,
|
||||
scheduledTaskId:
|
||||
rule.attributes.scheduledTaskId === rule.id
|
||||
|
@ -203,9 +188,6 @@ const bulkDisableRulesWithOCC = async (
|
|||
attributes: {
|
||||
...updatedAttributes,
|
||||
} as RawRule,
|
||||
...(migratedActions.hasLegacyActions
|
||||
? { references: migratedActions.resultedReferences }
|
||||
: {}),
|
||||
});
|
||||
|
||||
context.auditLogger?.log(
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import { omit } from 'lodash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import type { ConstructorOptions } from '../../../../rules_client/rules_client';
|
||||
import { RulesClient } from '../../../../rules_client/rules_client';
|
||||
import {
|
||||
|
@ -31,14 +30,6 @@ import type { ActionsAuthorization, ActionsClient } from '@kbn/actions-plugin/se
|
|||
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
||||
import { getBeforeSetup, setGlobalDate } from '../../../../rules_client/tests/lib';
|
||||
import { bulkMarkApiKeysForInvalidation } from '../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
|
||||
import {
|
||||
enabledRule1,
|
||||
enabledRule2,
|
||||
siemRule1,
|
||||
siemRule2,
|
||||
} from '../../../../rules_client/tests/test_helpers';
|
||||
import { migrateLegacyActions } from '../../../../rules_client/lib';
|
||||
import { migrateLegacyActionsMock } from '../../../../rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock';
|
||||
import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry';
|
||||
import type { ConnectorAdapter } from '../../../../connector_adapters/types';
|
||||
import type { SavedObject } from '@kbn/core/server';
|
||||
|
@ -47,17 +38,6 @@ import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
|
|||
import { backfillClientMock } from '../../../../backfill_client/backfill_client.mock';
|
||||
import type { RawRule } from '../../../../types';
|
||||
|
||||
jest.mock('../../../../rules_client/lib/siem_legacy_actions/migrate_legacy_actions', () => {
|
||||
return {
|
||||
migrateLegacyActions: jest.fn(),
|
||||
};
|
||||
});
|
||||
(migrateLegacyActions as jest.Mock).mockResolvedValue({
|
||||
hasLegacyActions: false,
|
||||
resultedActions: [],
|
||||
resultedReferences: [],
|
||||
});
|
||||
|
||||
jest.mock('../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({
|
||||
bulkMarkApiKeysForInvalidation: jest.fn(),
|
||||
}));
|
||||
|
@ -273,8 +253,6 @@ describe('bulkEdit()', () => {
|
|||
},
|
||||
});
|
||||
|
||||
(migrateLegacyActions as jest.Mock).mockResolvedValue(migrateLegacyActionsMock);
|
||||
|
||||
rulesClientParams.isSystemAction.mockImplementation((id: string) => id === 'system_action-id');
|
||||
actionsClient.isSystemAction.mockImplementation((id: string) => id === 'system_action-id');
|
||||
});
|
||||
|
@ -3769,53 +3747,4 @@ describe('bulkEdit()', () => {
|
|||
expect(taskManager.bulkUpdateSchedules).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacy actions migration for SIEM', () => {
|
||||
test('should call migrateLegacyActions', async () => {
|
||||
encryptedSavedObjects.createPointInTimeFinderDecryptedAsInternalUser = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
close: jest.fn(),
|
||||
find: function* asyncGenerator() {
|
||||
yield { saved_objects: [enabledRule1, enabledRule2, siemRule1, siemRule2] };
|
||||
},
|
||||
});
|
||||
|
||||
await rulesClient.bulkEdit({
|
||||
operations: [
|
||||
{
|
||||
field: 'tags',
|
||||
operation: 'set',
|
||||
value: ['test-tag'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(migrateLegacyActions).toHaveBeenCalledTimes(4);
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
attributes: enabledRule1.attributes,
|
||||
ruleId: enabledRule1.id,
|
||||
actions: [],
|
||||
references: [],
|
||||
});
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
attributes: enabledRule2.attributes,
|
||||
ruleId: enabledRule2.id,
|
||||
actions: [],
|
||||
references: [],
|
||||
});
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
attributes: expect.objectContaining({ consumer: AlertConsumers.SIEM }),
|
||||
ruleId: siemRule1.id,
|
||||
actions: [],
|
||||
references: [],
|
||||
});
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
attributes: expect.objectContaining({ consumer: AlertConsumers.SIEM }),
|
||||
ruleId: siemRule2.id,
|
||||
actions: [],
|
||||
references: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -62,7 +62,7 @@ import type {
|
|||
NormalizedAlertActionWithGeneratedValues,
|
||||
NormalizedAlertAction,
|
||||
} from '../../../../rules_client/types';
|
||||
import { migrateLegacyActions } from '../../../../rules_client/lib';
|
||||
import { bulkMigrateLegacyActions } from '../../../../rules_client/lib';
|
||||
import type {
|
||||
BulkEditFields,
|
||||
BulkEditOperation,
|
||||
|
@ -311,6 +311,8 @@ async function bulkEditRulesOcc<Params extends RuleParams>(
|
|||
|
||||
prevInterval.concat(intervals);
|
||||
|
||||
await bulkMigrateLegacyActions({ context, rules: response.saved_objects });
|
||||
|
||||
await pMap(
|
||||
response.saved_objects,
|
||||
async (rule: SavedObjectsFindResult<RawRule>) =>
|
||||
|
@ -459,20 +461,6 @@ async function updateRuleAttributesAndParamsInMemory<Params extends RuleParams>(
|
|||
|
||||
await ensureAuthorizationForBulkUpdate(context, operations, rule);
|
||||
|
||||
// migrate legacy actions only for SIEM rules
|
||||
// TODO (http-versioning) Remove RawRuleAction and RawRule casts
|
||||
const migratedActions = await migrateLegacyActions(context, {
|
||||
ruleId: rule.id,
|
||||
actions: rule.attributes.actions as RawRuleAction[],
|
||||
references: rule.references,
|
||||
attributes: rule.attributes as RawRule,
|
||||
});
|
||||
|
||||
if (migratedActions.hasLegacyActions) {
|
||||
rule.attributes.actions = migratedActions.resultedActions;
|
||||
rule.references = migratedActions.resultedReferences;
|
||||
}
|
||||
|
||||
const ruleActions = injectReferencesIntoActions(
|
||||
rule.id,
|
||||
rule.attributes.actions || [],
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
|
||||
import type { ConstructorOptions } from '../../../../rules_client/rules_client';
|
||||
import { RulesClient } from '../../../../rules_client/rules_client';
|
||||
import {
|
||||
|
@ -39,22 +39,12 @@ import {
|
|||
enabledRuleForBulkOps2,
|
||||
returnedRuleForBulkOps1,
|
||||
returnedRuleForBulkOps2,
|
||||
disabledRuleForBulkDisable1,
|
||||
siemRuleForBulkOps1,
|
||||
siemRuleForBulkOps2,
|
||||
} from '../../../../rules_client/tests/test_helpers';
|
||||
import { TaskStatus } from '@kbn/task-manager-plugin/server';
|
||||
import { migrateLegacyActions } from '../../../../rules_client/lib';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
|
||||
import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry';
|
||||
import { backfillClientMock } from '../../../../backfill_client/backfill_client.mock';
|
||||
|
||||
jest.mock('../../../../rules_client/lib/siem_legacy_actions/migrate_legacy_actions', () => {
|
||||
return {
|
||||
migrateLegacyActions: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({
|
||||
bulkMarkApiKeysForInvalidation: jest.fn(),
|
||||
}));
|
||||
|
@ -114,11 +104,6 @@ beforeEach(() => {
|
|||
} as unknown as BulkUpdateTaskResult)
|
||||
);
|
||||
(auditLogger.log as jest.Mock).mockClear();
|
||||
(migrateLegacyActions as jest.Mock).mockResolvedValue({
|
||||
hasLegacyActions: false,
|
||||
resultedActions: [],
|
||||
resultedReferences: [],
|
||||
});
|
||||
});
|
||||
|
||||
setGlobalDate();
|
||||
|
@ -875,49 +860,4 @@ describe('bulkEnableRules', () => {
|
|||
expect(auditLogger.log.mock.calls[0][0]?.event?.outcome).toEqual('failure');
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacy actions migration for SIEM', () => {
|
||||
test('should call migrateLegacyActions', async () => {
|
||||
encryptedSavedObjects.createPointInTimeFinderDecryptedAsInternalUser = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
close: jest.fn(),
|
||||
find: function* asyncGenerator() {
|
||||
yield {
|
||||
saved_objects: [
|
||||
disabledRuleForBulkDisable1,
|
||||
siemRuleForBulkOps1,
|
||||
siemRuleForBulkOps2,
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: [disabledRuleForBulkDisable1, siemRuleForBulkOps1, siemRuleForBulkOps2],
|
||||
});
|
||||
|
||||
await rulesClient.bulkEnableRules({ filter: 'fake_filter' });
|
||||
|
||||
expect(migrateLegacyActions).toHaveBeenCalledTimes(3);
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
attributes: disabledRuleForBulkDisable1.attributes,
|
||||
ruleId: disabledRuleForBulkDisable1.id,
|
||||
actions: [],
|
||||
references: [],
|
||||
});
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
attributes: expect.objectContaining({ consumer: AlertConsumers.SIEM }),
|
||||
ruleId: siemRuleForBulkOps1.id,
|
||||
actions: [],
|
||||
references: [],
|
||||
});
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
attributes: expect.objectContaining({ consumer: AlertConsumers.SIEM }),
|
||||
ruleId: siemRuleForBulkOps2.id,
|
||||
actions: [],
|
||||
references: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -35,8 +35,8 @@ import {
|
|||
getAuthorizationFilter,
|
||||
checkAuthorizationAndGetTotal,
|
||||
createNewAPIKeySet,
|
||||
migrateLegacyActions,
|
||||
updateMetaAttributes,
|
||||
bulkMigrateLegacyActions,
|
||||
} from '../../../../rules_client/lib';
|
||||
import type { RulesClientContext, BulkOperationError } from '../../../../rules_client/types';
|
||||
import { validateScheduleLimit } from '../get_schedule_frequency';
|
||||
|
@ -203,6 +203,8 @@ const bulkEnableRulesWithOCC = async (
|
|||
});
|
||||
}
|
||||
|
||||
await bulkMigrateLegacyActions({ context, rules: rulesFinderRules });
|
||||
|
||||
await pMap(
|
||||
rulesFinderRules,
|
||||
async (rule) => {
|
||||
|
@ -223,13 +225,6 @@ const bulkEnableRulesWithOCC = async (
|
|||
ruleNameToRuleIdMapping[rule.id] = ruleName;
|
||||
}
|
||||
|
||||
const migratedActions = await migrateLegacyActions(context, {
|
||||
ruleId: rule.id,
|
||||
actions: rule.attributes.actions,
|
||||
references: rule.references,
|
||||
attributes: rule.attributes,
|
||||
});
|
||||
|
||||
const updatedAttributes = updateMetaAttributes(context, {
|
||||
...rule.attributes,
|
||||
...(!rule.attributes.apiKey &&
|
||||
|
@ -239,13 +234,6 @@ const bulkEnableRulesWithOCC = async (
|
|||
username,
|
||||
shouldUpdateApiKey: true,
|
||||
}))),
|
||||
...(migratedActions.hasLegacyActions
|
||||
? {
|
||||
actions: migratedActions.resultedActions,
|
||||
throttle: undefined,
|
||||
notifyWhen: undefined,
|
||||
}
|
||||
: {}),
|
||||
enabled: true,
|
||||
updatedBy: username,
|
||||
updatedAt: new Date().toISOString(),
|
||||
|
@ -287,9 +275,6 @@ const bulkEnableRulesWithOCC = async (
|
|||
rulesToEnable.push({
|
||||
...rule,
|
||||
attributes: updatedAttributes,
|
||||
...(migratedActions.hasLegacyActions
|
||||
? { references: migratedActions.resultedReferences }
|
||||
: {}),
|
||||
});
|
||||
|
||||
context.auditLogger?.log(
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
|
||||
import type { ConstructorOptions } from '../../../../rules_client/rules_client';
|
||||
import { RulesClient } from '../../../../rules_client/rules_client';
|
||||
import {
|
||||
|
@ -25,22 +23,10 @@ import type { ActionsAuthorization } from '@kbn/actions-plugin/server';
|
|||
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
||||
import { getBeforeSetup } from '../../../../rules_client/tests/lib';
|
||||
import { bulkMarkApiKeysForInvalidation } from '../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
|
||||
import { migrateLegacyActions } from '../../../../rules_client/lib';
|
||||
import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
|
||||
import { backfillClientMock } from '../../../../backfill_client/backfill_client.mock';
|
||||
|
||||
jest.mock('../../../../rules_client/lib/siem_legacy_actions/migrate_legacy_actions', () => {
|
||||
return {
|
||||
migrateLegacyActions: jest.fn(),
|
||||
};
|
||||
});
|
||||
(migrateLegacyActions as jest.Mock).mockResolvedValue({
|
||||
hasLegacyActions: false,
|
||||
resultedActions: [],
|
||||
resultedReferences: [],
|
||||
});
|
||||
|
||||
jest.mock('../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({
|
||||
bulkMarkApiKeysForInvalidation: jest.fn(),
|
||||
}));
|
||||
|
@ -285,27 +271,6 @@ describe('delete()', () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe('legacy actions migration for SIEM', () => {
|
||||
test('should call migrateLegacyActions', async () => {
|
||||
const existingDecryptedSiemAlert = {
|
||||
...existingDecryptedAlert,
|
||||
attributes: { ...existingDecryptedAlert.attributes, consumer: AlertConsumers.SIEM },
|
||||
};
|
||||
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce(
|
||||
existingDecryptedSiemAlert
|
||||
);
|
||||
|
||||
await rulesClient.delete({ id: '1' });
|
||||
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
ruleId: '1',
|
||||
skipActionsValidation: true,
|
||||
attributes: existingDecryptedSiemAlert.attributes,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('authorization', () => {
|
||||
test('ensures user is authorised to delete this type of alert under the consumer', async () => {
|
||||
await rulesClient.delete({ id: '1' });
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import Boom from '@hapi/boom';
|
||||
import type { SavedObject } from '@kbn/core/server';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import type { RawRule } from '../../../../types';
|
||||
import { WriteOperations, AlertingAuthorizationEntity } from '../../../../authorization';
|
||||
|
@ -13,7 +14,7 @@ import { retryIfConflicts } from '../../../../lib/retry_if_conflicts';
|
|||
import { bulkMarkApiKeysForInvalidation } from '../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
|
||||
import { ruleAuditEvent, RuleAuditAction } from '../../../../rules_client/common/audit_events';
|
||||
import type { RulesClientContext } from '../../../../rules_client/types';
|
||||
import { untrackRuleAlerts, migrateLegacyActions } from '../../../../rules_client/lib';
|
||||
import { untrackRuleAlerts, bulkMigrateLegacyActions } from '../../../../rules_client/lib';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
|
||||
import type { DeleteRuleParams } from './types';
|
||||
import { deleteRuleParamsSchema } from './schemas';
|
||||
|
@ -40,6 +41,7 @@ async function deleteRuleWithOCC(context: RulesClientContext, { id }: { id: stri
|
|||
let apiKeyToInvalidate: string | null = null;
|
||||
let apiKeyCreatedByUser: boolean | undefined | null = false;
|
||||
let attributes: RawRule;
|
||||
let rule: SavedObject<RawRule>;
|
||||
|
||||
try {
|
||||
const decryptedRule = await getDecryptedRuleSo({
|
||||
|
@ -53,6 +55,7 @@ async function deleteRuleWithOCC(context: RulesClientContext, { id }: { id: stri
|
|||
apiKeyCreatedByUser = decryptedRule.attributes.apiKeyCreatedByUser;
|
||||
taskIdToRemove = decryptedRule.attributes.scheduledTaskId;
|
||||
attributes = decryptedRule.attributes;
|
||||
rule = decryptedRule;
|
||||
} catch (e) {
|
||||
// We'll skip invalidating the API key since we failed to load the decrypted saved object
|
||||
context.logger.error(
|
||||
|
@ -60,7 +63,7 @@ async function deleteRuleWithOCC(context: RulesClientContext, { id }: { id: stri
|
|||
);
|
||||
|
||||
// Still attempt to load the scheduledTaskId using SOC
|
||||
const rule = await getRuleSo({
|
||||
rule = await getRuleSo({
|
||||
savedObjectsClient: context.unsecuredSavedObjectsClient,
|
||||
id,
|
||||
});
|
||||
|
@ -92,11 +95,7 @@ async function deleteRuleWithOCC(context: RulesClientContext, { id }: { id: stri
|
|||
// TODO (http-versioning): Remove this cast, this enables us to move forward
|
||||
// without fixing all of other solution types
|
||||
if (attributes.consumer === AlertConsumers.SIEM) {
|
||||
await migrateLegacyActions(context, {
|
||||
ruleId: id,
|
||||
attributes: attributes as RawRule,
|
||||
skipActionsValidation: true,
|
||||
});
|
||||
await bulkMigrateLegacyActions({ context, rules: [rule], skipActionsValidation: true });
|
||||
}
|
||||
|
||||
context.auditLogger?.log(
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
|
||||
import type { ConstructorOptions } from '../../../../rules_client/rules_client';
|
||||
import { RulesClient } from '../../../../rules_client/rules_client';
|
||||
|
@ -25,18 +24,10 @@ import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
|||
import { getBeforeSetup, setGlobalDate } from '../../../../rules_client/tests/lib';
|
||||
import { eventLoggerMock } from '@kbn/event-log-plugin/server/event_logger.mock';
|
||||
import { TaskStatus } from '@kbn/task-manager-plugin/server';
|
||||
import { migrateLegacyActions } from '../../../../rules_client/lib';
|
||||
import { migrateLegacyActionsMock } from '../../../../rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock';
|
||||
import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
|
||||
import { backfillClientMock } from '../../../../backfill_client/backfill_client.mock';
|
||||
|
||||
jest.mock('../../../../rules_client/lib/siem_legacy_actions/migrate_legacy_actions', () => {
|
||||
return {
|
||||
migrateLegacyActions: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({
|
||||
bulkMarkApiKeysForInvalidation: jest.fn(),
|
||||
}));
|
||||
|
@ -153,11 +144,6 @@ describe('disableRule()', () => {
|
|||
rulesClient = new RulesClient(rulesClientParams);
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValue(existingRule);
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedRule);
|
||||
(migrateLegacyActions as jest.Mock).mockResolvedValue({
|
||||
hasLegacyActions: false,
|
||||
resultedActions: [],
|
||||
resultedReferences: [],
|
||||
});
|
||||
});
|
||||
|
||||
describe('authorization', () => {
|
||||
|
@ -719,35 +705,4 @@ describe('disableRule()', () => {
|
|||
);
|
||||
expect(taskManager.bulkDisable).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('legacy actions migration for SIEM', () => {
|
||||
test('should call migrateLegacyActions', async () => {
|
||||
const existingDecryptedSiemRule = {
|
||||
...existingDecryptedRule,
|
||||
attributes: { ...existingDecryptedRule.attributes, consumer: AlertConsumers.SIEM },
|
||||
};
|
||||
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedSiemRule);
|
||||
(migrateLegacyActions as jest.Mock).mockResolvedValue(migrateLegacyActionsMock);
|
||||
|
||||
await rulesClient.disableRule({ id: '1' });
|
||||
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
attributes: expect.objectContaining({ consumer: AlertConsumers.SIEM }),
|
||||
actions: [
|
||||
{
|
||||
actionRef: '1',
|
||||
actionTypeId: '1',
|
||||
group: 'default',
|
||||
id: '1',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
references: [],
|
||||
ruleId: '1',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { SavedObjectReference } from '@kbn/core/server';
|
||||
import type { SavedObject } from '@kbn/core/server';
|
||||
|
||||
import Boom from '@hapi/boom';
|
||||
import type { RawRule } from '../../../../types';
|
||||
|
@ -12,7 +12,11 @@ import { WriteOperations, AlertingAuthorizationEntity } from '../../../../author
|
|||
import { retryIfConflicts } from '../../../../lib/retry_if_conflicts';
|
||||
import { ruleAuditEvent, RuleAuditAction } from '../../../../rules_client/common/audit_events';
|
||||
import type { RulesClientContext } from '../../../../rules_client/types';
|
||||
import { untrackRuleAlerts, updateMeta, migrateLegacyActions } from '../../../../rules_client/lib';
|
||||
import {
|
||||
untrackRuleAlerts,
|
||||
updateMeta,
|
||||
bulkMigrateLegacyActions,
|
||||
} from '../../../../rules_client/lib';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
|
||||
import type { DisableRuleParams } from './types';
|
||||
import { disableRuleParamsSchema } from './schemas';
|
||||
|
@ -34,14 +38,13 @@ async function disableWithOCC(
|
|||
) {
|
||||
let attributes: RawRule;
|
||||
let version: string | undefined;
|
||||
let references: SavedObjectReference[];
|
||||
|
||||
try {
|
||||
disableRuleParamsSchema.validate({ id, untrack });
|
||||
} catch (error) {
|
||||
throw Boom.badRequest(`Error validating disable rule parameters - ${error.message}`);
|
||||
}
|
||||
|
||||
let alert: SavedObject<RawRule>;
|
||||
try {
|
||||
const decryptedAlert =
|
||||
await context.encryptedSavedObjectsClient.getDecryptedAsInternalUser<RawRule>(
|
||||
|
@ -53,17 +56,13 @@ async function disableWithOCC(
|
|||
);
|
||||
attributes = decryptedAlert.attributes;
|
||||
version = decryptedAlert.version;
|
||||
references = decryptedAlert.references;
|
||||
alert = decryptedAlert;
|
||||
} catch (e) {
|
||||
context.logger.error(`disable(): Failed to load API key of alert ${id}: ${e.message}`);
|
||||
// Still attempt to load the attributes and version using SOC
|
||||
const alert = await context.unsecuredSavedObjectsClient.get<RawRule>(
|
||||
RULE_SAVED_OBJECT_TYPE,
|
||||
id
|
||||
);
|
||||
alert = await context.unsecuredSavedObjectsClient.get<RawRule>(RULE_SAVED_OBJECT_TYPE, id);
|
||||
attributes = alert.attributes;
|
||||
version = alert.version;
|
||||
references = alert.references;
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -99,12 +98,7 @@ async function disableWithOCC(
|
|||
context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId);
|
||||
|
||||
if (attributes.enabled === true) {
|
||||
const migratedActions = await migrateLegacyActions(context, {
|
||||
ruleId: id,
|
||||
actions: attributes.actions,
|
||||
references,
|
||||
attributes,
|
||||
});
|
||||
const migratedIds = await bulkMigrateLegacyActions({ context, rules: [alert] });
|
||||
|
||||
await context.unsecuredSavedObjectsClient.update(
|
||||
RULE_SAVED_OBJECT_TYPE,
|
||||
|
@ -116,15 +110,10 @@ async function disableWithOCC(
|
|||
updatedBy: await context.getUserName(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
nextRun: null,
|
||||
...(migratedActions.hasLegacyActions
|
||||
? { actions: migratedActions.resultedActions, throttle: undefined, notifyWhen: undefined }
|
||||
: {}),
|
||||
}),
|
||||
{
|
||||
version,
|
||||
...(migratedActions.hasLegacyActions
|
||||
? { references: migratedActions.resultedReferences }
|
||||
: {}),
|
||||
...(migratedIds.includes(alert.id) ? { references: alert.references } : {}),
|
||||
}
|
||||
);
|
||||
const { autoRecoverAlerts: isLifecycleAlert } = context.ruleTypeRegistry.get(
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
|
||||
import type { ConstructorOptions } from '../../../../rules_client/rules_client';
|
||||
import { RulesClient } from '../../../../rules_client/rules_client';
|
||||
|
@ -24,8 +23,7 @@ import type { ActionsAuthorization } from '@kbn/actions-plugin/server';
|
|||
import { TaskStatus } from '@kbn/task-manager-plugin/server';
|
||||
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
||||
import { getBeforeSetup, setGlobalDate } from '../../../../rules_client/tests/lib';
|
||||
import { migrateLegacyActions } from '../../../../rules_client/lib';
|
||||
import { migrateLegacyActionsMock } from '../../../../rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock';
|
||||
import { bulkMigrateLegacyActions } from '../../../../rules_client/lib';
|
||||
import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry';
|
||||
import {
|
||||
API_KEY_PENDING_INVALIDATION_TYPE,
|
||||
|
@ -35,7 +33,7 @@ import { backfillClientMock } from '../../../../backfill_client/backfill_client.
|
|||
|
||||
jest.mock('../../../../rules_client/lib/siem_legacy_actions/migrate_legacy_actions', () => {
|
||||
return {
|
||||
migrateLegacyActions: jest.fn(),
|
||||
bulkMigrateLegacyActions: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -156,11 +154,7 @@ describe('enable()', () => {
|
|||
apiKeysEnabled: false,
|
||||
});
|
||||
taskManager.get.mockResolvedValue(mockTask);
|
||||
(migrateLegacyActions as jest.Mock).mockResolvedValue({
|
||||
hasLegacyActions: false,
|
||||
resultedActions: [],
|
||||
resultedReferences: [],
|
||||
});
|
||||
(bulkMigrateLegacyActions as jest.Mock).mockResolvedValue([]);
|
||||
});
|
||||
|
||||
describe('authorization', () => {
|
||||
|
@ -764,58 +758,4 @@ describe('enable()', () => {
|
|||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('legacy actions migration for SIEM', () => {
|
||||
test('should call migrateLegacyActions', async () => {
|
||||
(migrateLegacyActions as jest.Mock).mockResolvedValueOnce({
|
||||
hasLegacyActions: true,
|
||||
resultedActions: ['fake-action-1'],
|
||||
resultedReferences: ['fake-ref-1'],
|
||||
});
|
||||
|
||||
const existingDecryptedSiemRule = {
|
||||
...existingRule,
|
||||
attributes: { ...existingRule.attributes, consumer: AlertConsumers.SIEM },
|
||||
};
|
||||
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedSiemRule);
|
||||
(migrateLegacyActions as jest.Mock).mockResolvedValue(migrateLegacyActionsMock);
|
||||
|
||||
await rulesClient.enableRule({ id: '1' });
|
||||
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
attributes: expect.objectContaining({ consumer: AlertConsumers.SIEM }),
|
||||
actions: [
|
||||
{
|
||||
actionRef: '1',
|
||||
actionTypeId: '1',
|
||||
group: 'default',
|
||||
id: '1',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
references: [],
|
||||
ruleId: '1',
|
||||
});
|
||||
// to mitigate AAD issues, we call create with overwrite=true and actions related props
|
||||
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith(
|
||||
RULE_SAVED_OBJECT_TYPE,
|
||||
expect.objectContaining({
|
||||
...existingDecryptedSiemRule.attributes,
|
||||
actions: ['fake-action-1'],
|
||||
throttle: undefined,
|
||||
notifyWhen: undefined,
|
||||
enabled: true,
|
||||
}),
|
||||
{
|
||||
id: existingDecryptedSiemRule.id,
|
||||
overwrite: true,
|
||||
references: ['fake-ref-1'],
|
||||
version: existingDecryptedSiemRule.version,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import Boom from '@hapi/boom';
|
||||
import type { SavedObjectReference } from '@kbn/core/server';
|
||||
import type { SavedObject } from '@kbn/core/server';
|
||||
import { TaskStatus } from '@kbn/task-manager-plugin/server';
|
||||
import type { RawRule, IntervalSchedule } from '../../../../types';
|
||||
import { resetMonitoringLastRun, getNextRun } from '../../../../lib';
|
||||
|
@ -17,7 +17,7 @@ import {
|
|||
updateMeta,
|
||||
createNewAPIKeySet,
|
||||
scheduleTask,
|
||||
migrateLegacyActions,
|
||||
bulkMigrateLegacyActions,
|
||||
} from '../../../../rules_client/lib';
|
||||
import { validateScheduleLimit } from '../get_schedule_frequency';
|
||||
import { getRuleCircuitBreakerErrorMessage } from '../../../../../common';
|
||||
|
@ -40,7 +40,6 @@ async function enableWithOCC(context: RulesClientContext, params: EnableRulePara
|
|||
let existingApiKey: string | null = null;
|
||||
let attributes: RawRule;
|
||||
let version: string | undefined;
|
||||
let references: SavedObjectReference[];
|
||||
|
||||
try {
|
||||
enableRuleParamsSchema.validate(params);
|
||||
|
@ -49,6 +48,7 @@ async function enableWithOCC(context: RulesClientContext, params: EnableRulePara
|
|||
}
|
||||
|
||||
const { id } = params;
|
||||
let alert: SavedObject<RawRule>;
|
||||
try {
|
||||
const decryptedAlert =
|
||||
await context.encryptedSavedObjectsClient.getDecryptedAsInternalUser<RawRule>(
|
||||
|
@ -61,17 +61,13 @@ async function enableWithOCC(context: RulesClientContext, params: EnableRulePara
|
|||
existingApiKey = decryptedAlert.attributes.apiKey;
|
||||
attributes = decryptedAlert.attributes;
|
||||
version = decryptedAlert.version;
|
||||
references = decryptedAlert.references;
|
||||
alert = decryptedAlert;
|
||||
} catch (e) {
|
||||
context.logger.error(`enable(): Failed to load API key of alert ${id}: ${e.message}`);
|
||||
// Still attempt to load the attributes and version using SOC
|
||||
const alert = await context.unsecuredSavedObjectsClient.get<RawRule>(
|
||||
RULE_SAVED_OBJECT_TYPE,
|
||||
id
|
||||
);
|
||||
alert = await context.unsecuredSavedObjectsClient.get<RawRule>(RULE_SAVED_OBJECT_TYPE, id);
|
||||
attributes = alert.attributes;
|
||||
version = alert.version;
|
||||
references = alert.references;
|
||||
}
|
||||
|
||||
const validationPayload = await validateScheduleLimit({
|
||||
|
@ -123,12 +119,7 @@ async function enableWithOCC(context: RulesClientContext, params: EnableRulePara
|
|||
context.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId);
|
||||
|
||||
if (attributes.enabled === false) {
|
||||
const migratedActions = await migrateLegacyActions(context, {
|
||||
ruleId: id,
|
||||
actions: attributes.actions,
|
||||
references,
|
||||
attributes,
|
||||
});
|
||||
const migratedIds = await bulkMigrateLegacyActions({ context, rules: [alert] });
|
||||
|
||||
const username = await context.getUserName();
|
||||
const now = new Date();
|
||||
|
@ -163,20 +154,15 @@ async function enableWithOCC(context: RulesClientContext, params: EnableRulePara
|
|||
try {
|
||||
// to mitigate AAD issues(actions property is not used for encrypting API key in partial SO update)
|
||||
// we call create with overwrite=true
|
||||
if (migratedActions.hasLegacyActions) {
|
||||
if (migratedIds.includes(alert.id)) {
|
||||
await context.unsecuredSavedObjectsClient.create<RawRule>(
|
||||
RULE_SAVED_OBJECT_TYPE,
|
||||
{
|
||||
...updateAttributes,
|
||||
actions: migratedActions.resultedActions,
|
||||
throttle: undefined,
|
||||
notifyWhen: undefined,
|
||||
},
|
||||
updateAttributes,
|
||||
{
|
||||
id,
|
||||
overwrite: true,
|
||||
version,
|
||||
references: migratedActions.resultedReferences,
|
||||
references: alert.references,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -1182,7 +1182,7 @@ describe('find()', () => {
|
|||
});
|
||||
|
||||
describe('legacy actions migration for SIEM', () => {
|
||||
test('should call migrateLegacyActions', async () => {
|
||||
test('should call formatLegacyActions', async () => {
|
||||
const rulesClient = new RulesClient(rulesClientParams);
|
||||
|
||||
(formatLegacyActions as jest.Mock).mockResolvedValueOnce([
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import type { ConstructorOptions } from '../../../../rules_client/rules_client';
|
||||
import { RulesClient } from '../../../../rules_client/rules_client';
|
||||
import {
|
||||
|
@ -30,18 +29,11 @@ import { TaskStatus } from '@kbn/task-manager-plugin/server';
|
|||
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
||||
import { getBeforeSetup, setGlobalDate } from '../../../../rules_client/tests/lib';
|
||||
import { bulkMarkApiKeysForInvalidation } from '../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
|
||||
import { migrateLegacyActions } from '../../../../rules_client/lib';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
|
||||
import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry';
|
||||
import type { RuleDomain } from '../../types';
|
||||
import { backfillClientMock } from '../../../../backfill_client/backfill_client.mock';
|
||||
|
||||
jest.mock('../../../../rules_client/lib/siem_legacy_actions/migrate_legacy_actions', () => {
|
||||
return {
|
||||
migrateLegacyActions: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@kbn/core-saved-objects-utils-server', () => {
|
||||
const actual = jest.requireActual('@kbn/core-saved-objects-utils-server');
|
||||
return {
|
||||
|
@ -225,11 +217,6 @@ describe('update()', () => {
|
|||
},
|
||||
validLegacyConsumers: [],
|
||||
});
|
||||
(migrateLegacyActions as jest.Mock).mockResolvedValue({
|
||||
hasLegacyActions: false,
|
||||
resultedActions: [],
|
||||
resultedReferences: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('updates given parameters', async () => {
|
||||
|
@ -3454,68 +3441,6 @@ describe('update()', () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe('legacy actions migration for SIEM', () => {
|
||||
beforeEach(() => {
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: RULE_SAVED_OBJECT_TYPE,
|
||||
attributes: {
|
||||
enabled: true,
|
||||
schedule: { interval: '1m' },
|
||||
params: {
|
||||
bar: true,
|
||||
},
|
||||
actions: [],
|
||||
notifyWhen: 'onActiveAlert',
|
||||
scheduledTaskId: 'task-123',
|
||||
executionStatus: {
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending',
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('should call migrateLegacyActions', async () => {
|
||||
const existingDecryptedSiemAlert = {
|
||||
...existingDecryptedAlert,
|
||||
attributes: { ...existingDecryptedAlert.attributes, consumer: AlertConsumers.SIEM },
|
||||
};
|
||||
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce(
|
||||
existingDecryptedSiemAlert
|
||||
);
|
||||
|
||||
actionsClient.getBulk.mockReset();
|
||||
actionsClient.isPreconfigured.mockReset();
|
||||
|
||||
await rulesClient.update({
|
||||
id: '1',
|
||||
data: {
|
||||
schedule: { interval: '1m' },
|
||||
name: 'abc',
|
||||
tags: ['foo'],
|
||||
params: {
|
||||
bar: true,
|
||||
risk_score: 40,
|
||||
severity: 'low',
|
||||
},
|
||||
throttle: null,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
actions: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
ruleId: '1',
|
||||
attributes: existingDecryptedSiemAlert.attributes,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('calls the authentication API key function if the user is authenticated using an api key', async () => {
|
||||
rulesClientParams.isAuthenticationTypeAPIKey.mockReturnValueOnce(true);
|
||||
rulesClientParams.getAuthenticationAPIKey.mockReturnValueOnce({
|
||||
|
|
|
@ -27,8 +27,8 @@ import {
|
|||
addGeneratedActionValues,
|
||||
incrementRevision,
|
||||
createNewAPIKeySet,
|
||||
migrateLegacyActions,
|
||||
updateMetaAttributes,
|
||||
bulkMigrateLegacyActions,
|
||||
} from '../../../../rules_client/lib';
|
||||
import type { RuleParams } from '../../types';
|
||||
import type { UpdateRuleData } from './types';
|
||||
|
@ -302,12 +302,12 @@ async function updateRuleAttributes<Params extends RuleParams = never>({
|
|||
isSystemAction: (connectorId: string) => boolean;
|
||||
// TODO (http-versioning): This should be of type Rule, change this when all rule types are fixed
|
||||
}): Promise<SanitizedRule<Params>> {
|
||||
await bulkMigrateLegacyActions({ context, rules: [originalRuleSavedObject] });
|
||||
const originalRule = originalRuleSavedObject.attributes;
|
||||
let updatedRule = { ...originalRule };
|
||||
|
||||
const allActions = [...updateRuleData.actions, ...(updateRuleData.systemActions ?? [])];
|
||||
const artifacts = updateRuleData.artifacts ?? {};
|
||||
const ruleType = context.ruleTypeRegistry.get(updatedRule.alertTypeId);
|
||||
const ruleType = context.ruleTypeRegistry.get(originalRule.alertTypeId);
|
||||
|
||||
// Extract saved object references for this rule
|
||||
const {
|
||||
|
@ -332,20 +332,6 @@ async function updateRuleAttributes<Params extends RuleParams = never>({
|
|||
})
|
||||
: originalRule.revision;
|
||||
|
||||
// TODO (http-versioning) Remove RawRuleAction and RawRule casts
|
||||
const migratedActions = await migrateLegacyActions(context, {
|
||||
ruleId: originalRuleSavedObject.id,
|
||||
attributes: originalRule as RawRule,
|
||||
});
|
||||
|
||||
if (migratedActions.hasLegacyActions) {
|
||||
updatedRule = {
|
||||
...updatedRule,
|
||||
notifyWhen: undefined,
|
||||
throttle: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const username = await context.getUserName();
|
||||
|
||||
const apiKeyAttributes = await createNewAPIKeySet(context, {
|
||||
|
@ -362,7 +348,7 @@ async function updateRuleAttributes<Params extends RuleParams = never>({
|
|||
);
|
||||
|
||||
const updatedRuleAttributes = updateMetaAttributes(context, {
|
||||
...updatedRule,
|
||||
...originalRule,
|
||||
...omit(updateRuleData, 'actions', 'systemActions', 'artifacts'),
|
||||
...apiKeyAttributes,
|
||||
params: updatedParams as RawRule['params'],
|
||||
|
|
|
@ -19,7 +19,7 @@ export { checkAuthorizationAndGetTotal } from './check_authorization_and_get_tot
|
|||
export { scheduleTask } from './schedule_task';
|
||||
export { createNewAPIKeySet } from './create_new_api_key_set';
|
||||
export { untrackRuleAlerts } from './untrack_rule_alerts';
|
||||
export { migrateLegacyActions } from './siem_legacy_actions/migrate_legacy_actions';
|
||||
export { bulkMigrateLegacyActions } from './siem_legacy_actions/migrate_legacy_actions';
|
||||
export { formatLegacyActions } from './siem_legacy_actions/format_legacy_actions';
|
||||
export { addGeneratedActionValues } from './add_generated_action_values';
|
||||
export { incrementRevision } from './increment_revision';
|
||||
|
|
|
@ -1,327 +0,0 @@
|
|||
/*
|
||||
* 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 { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
|
||||
import type { SavedObjectReference } from '@kbn/core/server';
|
||||
|
||||
import { migrateLegacyActions } from './migrate_legacy_actions';
|
||||
import { retrieveMigratedLegacyActions } from './retrieve_migrated_legacy_actions';
|
||||
import { injectReferencesIntoActions } from '../../common';
|
||||
|
||||
import { validateActions } from '../validate_actions';
|
||||
import type { RulesClientContext } from '../..';
|
||||
import type { RawRuleAction, RawRule } from '../../../types';
|
||||
|
||||
import type { UntypedNormalizedRuleType } from '../../../rule_type_registry';
|
||||
import { RecoveredActionGroup } from '../../../../common';
|
||||
|
||||
jest.mock('./retrieve_migrated_legacy_actions', () => ({
|
||||
retrieveMigratedLegacyActions: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../validate_actions', () => ({
|
||||
validateActions: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../common', () => ({
|
||||
injectReferencesIntoActions: jest.fn(),
|
||||
}));
|
||||
|
||||
const ruleType: jest.Mocked<UntypedNormalizedRuleType> = {
|
||||
id: 'test',
|
||||
name: 'My test rule',
|
||||
actionGroups: [{ id: 'default', name: 'Default' }, RecoveredActionGroup],
|
||||
defaultActionGroupId: 'default',
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
executor: jest.fn(),
|
||||
category: 'test',
|
||||
producer: 'alerts',
|
||||
solution: 'stack',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
ruleTaskTimeout: '5m',
|
||||
validate: {
|
||||
params: { validate: (params) => params },
|
||||
},
|
||||
validLegacyConsumers: [],
|
||||
};
|
||||
|
||||
const context = {
|
||||
ruleTypeRegistry: {
|
||||
get: () => ruleType,
|
||||
},
|
||||
logger: {
|
||||
error: jest.fn(),
|
||||
},
|
||||
} as unknown as RulesClientContext;
|
||||
|
||||
const ruleId = 'rule_id_1';
|
||||
|
||||
const attributes = {
|
||||
alertTypeId: 'siem.query',
|
||||
consumer: AlertConsumers.SIEM,
|
||||
} as unknown as RawRule;
|
||||
|
||||
(retrieveMigratedLegacyActions as jest.Mock).mockResolvedValue({
|
||||
legacyActions: [],
|
||||
legacyActionsReferences: [],
|
||||
});
|
||||
|
||||
const legacyActionsMock: RawRuleAction[] = [
|
||||
{
|
||||
group: 'default',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
to: ['test@test.com'],
|
||||
subject: 'Test Actions',
|
||||
},
|
||||
actionTypeId: '.email',
|
||||
uuid: '11403909-ca9b-49ba-9d7a-7e5320e68d05',
|
||||
actionRef: 'action_0',
|
||||
frequency: {
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
summary: true,
|
||||
throttle: '1d',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const legacyReferencesMock: SavedObjectReference[] = [
|
||||
{
|
||||
id: 'cc85da20-d480-11ed-8e69-1df522116c28',
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
},
|
||||
];
|
||||
|
||||
const existingActionsMock: RawRuleAction[] = [
|
||||
{
|
||||
group: 'default',
|
||||
params: {
|
||||
body: {
|
||||
test_web_hook: 'alert.id - {{alert.id}}',
|
||||
},
|
||||
},
|
||||
actionTypeId: '.webhook',
|
||||
uuid: '6e253775-693c-4dcb-a4f5-ad37d9524ecf',
|
||||
actionRef: 'action_0',
|
||||
},
|
||||
];
|
||||
|
||||
const referencesMock: SavedObjectReference[] = [
|
||||
{
|
||||
id: 'b2fd3f90-cd81-11ed-9f6d-a746729ca213',
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
},
|
||||
];
|
||||
|
||||
describe('migrateLegacyActions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should throw an exception when error is thrown within method', async () => {
|
||||
(retrieveMigratedLegacyActions as jest.Mock).mockRejectedValueOnce(new Error('test failure'));
|
||||
await expect(
|
||||
migrateLegacyActions(context, {
|
||||
ruleId,
|
||||
attributes,
|
||||
})
|
||||
).rejects.toThrowError(
|
||||
`Failed to migrate legacy actions for SIEM rule ${ruleId}: test failure`
|
||||
);
|
||||
|
||||
expect(context.logger.error).toHaveBeenCalledWith(
|
||||
`migrateLegacyActions(): Failed to migrate legacy actions for SIEM rule ${ruleId}: test failure`
|
||||
);
|
||||
});
|
||||
|
||||
it('should return earley empty migratedActions when consumer is not SIEM', async () => {
|
||||
(retrieveMigratedLegacyActions as jest.Mock).mockResolvedValue({
|
||||
legacyActions: [],
|
||||
legacyActionsReferences: [],
|
||||
});
|
||||
const migratedActions = await migrateLegacyActions(context, {
|
||||
ruleId,
|
||||
attributes: { ...attributes, consumer: 'mine' },
|
||||
});
|
||||
|
||||
expect(migratedActions).toEqual({
|
||||
resultedActions: [],
|
||||
hasLegacyActions: false,
|
||||
resultedReferences: [],
|
||||
});
|
||||
expect(retrieveMigratedLegacyActions).not.toHaveBeenCalled();
|
||||
expect(validateActions).not.toHaveBeenCalled();
|
||||
expect(injectReferencesIntoActions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call retrieveMigratedLegacyActions with correct rule id', async () => {
|
||||
(retrieveMigratedLegacyActions as jest.Mock).mockResolvedValue({
|
||||
legacyActions: [],
|
||||
legacyActionsReferences: [],
|
||||
});
|
||||
await migrateLegacyActions(context, { ruleId, attributes });
|
||||
|
||||
expect(retrieveMigratedLegacyActions).toHaveBeenCalledWith(
|
||||
context,
|
||||
{ ruleId },
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('should not call validateActions and injectReferencesIntoActions if skipActionsValidation=true', async () => {
|
||||
await migrateLegacyActions(context, { ruleId, attributes, skipActionsValidation: true });
|
||||
|
||||
expect(validateActions).not.toHaveBeenCalled();
|
||||
expect(injectReferencesIntoActions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set frequency props from rule level to existing actions', async () => {
|
||||
const result = await migrateLegacyActions(context, {
|
||||
ruleId,
|
||||
actions: existingActionsMock,
|
||||
references: referencesMock,
|
||||
attributes: { ...attributes, throttle: '1h', notifyWhen: 'onThrottleInterval' },
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('hasLegacyActions', false);
|
||||
expect(result).toHaveProperty('resultedReferences', referencesMock);
|
||||
expect(result).toHaveProperty('resultedActions', [
|
||||
{
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: '.webhook',
|
||||
group: 'default',
|
||||
params: { body: { test_web_hook: 'alert.id - {{alert.id}}' } },
|
||||
uuid: '6e253775-693c-4dcb-a4f5-ad37d9524ecf',
|
||||
frequency: {
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
summary: true,
|
||||
throttle: '1h',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return correct response when legacy actions empty and existing empty', async () => {
|
||||
const result = await migrateLegacyActions(context, {
|
||||
ruleId,
|
||||
actions: existingActionsMock,
|
||||
references: referencesMock,
|
||||
attributes,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('hasLegacyActions', false);
|
||||
expect(result).toHaveProperty('resultedReferences', referencesMock);
|
||||
expect(result).toHaveProperty('resultedActions', [
|
||||
{
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: '.webhook',
|
||||
group: 'default',
|
||||
params: { body: { test_web_hook: 'alert.id - {{alert.id}}' } },
|
||||
uuid: '6e253775-693c-4dcb-a4f5-ad37d9524ecf',
|
||||
frequency: {
|
||||
notifyWhen: 'onActiveAlert',
|
||||
summary: true,
|
||||
throttle: null,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return correct response when legacy actions empty and existing actions empty', async () => {
|
||||
const result = await migrateLegacyActions(context, {
|
||||
ruleId,
|
||||
attributes,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('hasLegacyActions', false);
|
||||
expect(result).toHaveProperty('resultedReferences', []);
|
||||
expect(result).toHaveProperty('resultedActions', []);
|
||||
});
|
||||
it('should return correct response when existing actions empty and legacy present', async () => {
|
||||
(retrieveMigratedLegacyActions as jest.Mock).mockResolvedValueOnce({
|
||||
legacyActions: legacyActionsMock,
|
||||
legacyActionsReferences: legacyReferencesMock,
|
||||
});
|
||||
|
||||
const result = await migrateLegacyActions(context, {
|
||||
ruleId,
|
||||
attributes,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('hasLegacyActions', true);
|
||||
expect(result).toHaveProperty('resultedReferences', legacyReferencesMock);
|
||||
expect(result).toHaveProperty('resultedActions', legacyActionsMock);
|
||||
});
|
||||
it('should merge actions and references correctly when existing and legacy actions both present', async () => {
|
||||
(retrieveMigratedLegacyActions as jest.Mock).mockResolvedValueOnce({
|
||||
legacyActions: legacyActionsMock,
|
||||
legacyActionsReferences: legacyReferencesMock,
|
||||
});
|
||||
|
||||
const result = await migrateLegacyActions(context, {
|
||||
ruleId,
|
||||
actions: existingActionsMock,
|
||||
references: referencesMock,
|
||||
attributes,
|
||||
});
|
||||
|
||||
expect(result.resultedReferences[0].name).toBe('action_0');
|
||||
expect(result.resultedReferences[1].name).toBe('action_1');
|
||||
|
||||
expect(result).toHaveProperty('hasLegacyActions', true);
|
||||
|
||||
// ensure references are correct
|
||||
expect(result.resultedReferences[0].name).toBe('action_0');
|
||||
expect(result.resultedReferences[1].name).toBe('action_1');
|
||||
expect(result).toHaveProperty('resultedReferences', [
|
||||
{
|
||||
id: 'b2fd3f90-cd81-11ed-9f6d-a746729ca213',
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
},
|
||||
{
|
||||
id: 'cc85da20-d480-11ed-8e69-1df522116c28',
|
||||
name: 'action_1',
|
||||
type: 'action',
|
||||
},
|
||||
]);
|
||||
|
||||
// ensure actionsRefs are correct
|
||||
expect(result.resultedActions[0].actionRef).toBe('action_0');
|
||||
expect(result.resultedActions[1].actionRef).toBe('action_1');
|
||||
expect(result).toHaveProperty('resultedActions', [
|
||||
{
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: '.webhook',
|
||||
group: 'default',
|
||||
params: { body: { test_web_hook: 'alert.id - {{alert.id}}' } },
|
||||
uuid: '6e253775-693c-4dcb-a4f5-ad37d9524ecf',
|
||||
frequency: {
|
||||
notifyWhen: 'onActiveAlert',
|
||||
summary: true,
|
||||
throttle: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
actionRef: 'action_1',
|
||||
actionTypeId: '.email',
|
||||
frequency: { notifyWhen: 'onThrottleInterval', summary: true, throttle: '1d' },
|
||||
group: 'default',
|
||||
params: {
|
||||
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
|
||||
subject: 'Test Actions',
|
||||
to: ['test@test.com'],
|
||||
},
|
||||
uuid: '11403909-ca9b-49ba-9d7a-7e5320e68d05',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue