Merge branch 'main' into renovate/main-minify

This commit is contained in:
Jon 2025-06-16 15:09:30 -05:00 committed by GitHub
commit abd155da66
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
306 changed files with 11107 additions and 3871 deletions

View file

@ -46955,7 +46955,6 @@ paths:
summary: Invalidate user sessions
tags:
- user session
x-state: Technical Preview
/api/short_url:
post:
description: |

View file

@ -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",

View file

@ -991,6 +991,11 @@
"updated",
"version"
],
"security:reference-data": [
"id",
"owner",
"type"
],
"siem-detection-engine-rule-actions": [
"actions",
"actions.actionRef",

View file

@ -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": {

View file

@ -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`

View file

@ -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;

View file

@ -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",

View file

@ -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',

View file

@ -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 ODCBy 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)
};

View file

@ -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,

View file

@ -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 ?? [],
};
}

View file

@ -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,

View file

@ -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 {

View file

@ -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';

View file

@ -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'] => {

View file

@ -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';

View file

@ -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';

View file

@ -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'] => {

View file

@ -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,

View file

@ -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',
},
})
);
});
});
});

View file

@ -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,

View file

@ -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",

View file

@ -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,

View file

@ -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(

View file

@ -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 };
};

View file

@ -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.

View file

@ -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();

View file

@ -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 },
}),

View file

@ -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,
};
}

View file

@ -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');
});
});

View file

@ -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 };
};

View file

@ -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,
});

View file

@ -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<

View file

@ -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 ?? [])],
};
};

View file

@ -8,8 +8,7 @@
*/
export type {
CommonEmbeddableStartContract,
EmbeddableRegistryDefinition,
EmbeddableStateWithType,
EmbeddablePersistableStateService,
EmbeddableRegistryDefinition,
} from './types';
} from '../server';

View file

@ -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> => {

View file

@ -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;
};

View file

@ -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);
});
});

View file

@ -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);

View file

@ -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;
}

View file

@ -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');

View file

@ -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;
});

View file

@ -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';

View file

@ -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;

View file

@ -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()

View file

@ -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,

View file

@ -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;

View file

@ -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;
}

View file

@ -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: [] };

View file

@ -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;
}

View file

@ -1,2 +0,0 @@
@import './lib/converters/index';
@import './lib/content_types/index';

View file

@ -1,3 +0,0 @@
.ffArray__highlight {
color: $euiColorMediumShade;
}

View file

@ -1 +0,0 @@
@import './html_content_type';

View file

@ -1,7 +0,0 @@
.ffString__emptyValue {
color: $euiColorDarkShade;
}
.lnsTableCell--colored .ffString__emptyValue {
color: unset;
}

View file

@ -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();

View file

@ -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;
}

View file

@ -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,
}),
};

View file

@ -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;
}
}

View file

@ -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,
}),
};

View file

@ -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);
});
});

View file

@ -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,
}),
};

View file

@ -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';

View file

@ -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%;
`,
};

View file

@ -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 ?? [])],
};
};

View file

@ -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';

View file

@ -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(),
});
};

View file

@ -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);
});
});
}

View file

@ -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'));
}

View file

@ -6,3 +6,4 @@
*/
export * from './src/es_fields/apm';
export * from './src/es_fields/otel';

View file

@ -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';

View file

@ -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';

View file

@ -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);

View file

@ -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/**/*"]
}

View file

@ -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) &&

View file

@ -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.",

View file

@ -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}\"]を登録できません。アクショングループ定義には、重複する重要度レベルを含めることはできません。",

View file

@ -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}\"]。操作组定义不能包含重复的严重性级别。",

View file

@ -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();

View file

@ -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,

View file

@ -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: [],
});
});
});
});

View file

@ -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(

View file

@ -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: [],
});
});
});
});

View file

@ -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 || [],

View file

@ -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: [],
});
});
});
});

View file

@ -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(

View file

@ -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' });

View file

@ -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(

View file

@ -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',
});
});
});
});

View file

@ -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(

View file

@ -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,
}
);
});
});
});

View file

@ -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 {

View file

@ -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([

View file

@ -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({

View file

@ -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'],

View file

@ -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';

View file

@ -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