diff --git a/.buildkite/scripts/steps/openapi_bundling/security_solution_openapi_bundling.sh b/.buildkite/scripts/steps/openapi_bundling/security_solution_openapi_bundling.sh index b17a1589190a5..ba4411b7d5ef4 100755 --- a/.buildkite/scripts/steps/openapi_bundling/security_solution_openapi_bundling.sh +++ b/.buildkite/scripts/steps/openapi_bundling/security_solution_openapi_bundling.sh @@ -23,6 +23,11 @@ check_for_changed_files "yarn openapi:bundle:entity-analytics" true echo -e "\n[Security Solution OpenAPI Bundling] Lists API\n" +echo -e "\n[Security Solution OpenAPI Bundling] Endpoint Management API\n" + +(cd x-pack/plugins/security_solution && yarn openapi:bundle:endpoint-management) +check_for_changed_files "yarn openapi:bundle:endpoint-management" true + (cd packages/kbn-securitysolution-lists-common && yarn openapi:bundle) check_for_changed_files "yarn openapi:bundle" true diff --git a/config/serverless.es.yml b/config/serverless.es.yml index 5dd773912c3a0..62e201955d9c8 100644 --- a/config/serverless.es.yml +++ b/config/serverless.es.yml @@ -46,7 +46,7 @@ telemetry.labels.serverless: search # Alerts and LLM config xpack.actions.enabledActionTypes: - ['.email', '.index', '.slack', '.jira', '.webhook', '.teams', '.gen-ai', '.bedrock'] + ['.email', '.index', '.slack', '.jira', '.webhook', '.teams', '.gen-ai', '.bedrock', '.gemini'] # Customize empty page state for analytics apps no_data_page.analyticsNoDataPageFlavor: 'serverless_search' diff --git a/packages/kbn-search-connectors/components/scheduling/connector_cron_editor.tsx b/packages/kbn-search-connectors/components/scheduling/connector_cron_editor.tsx index 629f2b432fd11..d5c8572dfa595 100644 --- a/packages/kbn-search-connectors/components/scheduling/connector_cron_editor.tsx +++ b/packages/kbn-search-connectors/components/scheduling/connector_cron_editor.tsx @@ -26,7 +26,7 @@ interface ConnectorCronEditorProps { export const ConnectorCronEditor: React.FC = ({ dataTelemetryIdPrefix, disabled = false, - frequencyBlockList = ['MINUTE'], + frequencyBlockList = [], hasSyncTypeChanges, onReset, onSave, diff --git a/packages/kbn-search-connectors/components/scheduling/full_content.tsx b/packages/kbn-search-connectors/components/scheduling/full_content.tsx index 3a453605f82e2..76051923ff033 100644 --- a/packages/kbn-search-connectors/components/scheduling/full_content.tsx +++ b/packages/kbn-search-connectors/components/scheduling/full_content.tsx @@ -216,9 +216,6 @@ export const ConnectorContentScheduling: React.FC { setScheduling({ diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.test.ts b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/add_panel_action_menu_items.test.ts similarity index 100% rename from src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.test.ts rename to src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/add_panel_action_menu_items.test.ts diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.ts b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/add_panel_action_menu_items.ts similarity index 97% rename from src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.ts rename to src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/add_panel_action_menu_items.ts index 4e90d94caa388..f13b6128ed405 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.ts +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/add_panel_action_menu_items.ts @@ -55,7 +55,7 @@ const onAddPanelActionClick = export const getAddPanelActionMenuItemsGroup = ( api: PresentationContainer, actions: Array> | undefined, - closePopover: () => void + onPanelSelected: () => void ) => { const grouped: Record = {}; @@ -72,7 +72,7 @@ export const getAddPanelActionMenuItemsGroup = ( name: actionName, icon: (typeof item.getIconType === 'function' ? item.getIconType(context) : undefined) ?? 'empty', - onClick: onAddPanelActionClick(item, context, closePopover), + onClick: onAddPanelActionClick(item, context, onPanelSelected), 'data-test-subj': `create-action-${actionName}`, description: item?.getDisplayNameTooltip?.(context), order: item.order ?? 0, diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/dashboard_panel_selection_flyout.test.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/dashboard_panel_selection_flyout.test.tsx new file mode 100644 index 0000000000000..17fa7f23f4adb --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/dashboard_panel_selection_flyout.test.tsx @@ -0,0 +1,110 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React, { type ComponentProps } from 'react'; +import { render, screen, fireEvent, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; +import { DashboardPanelSelectionListFlyout } from './dashboard_panel_selection_flyout'; +import type { GroupedAddPanelActions } from './add_panel_action_menu_items'; + +const defaultProps: Omit< + ComponentProps, + 'fetchDashboardPanels' +> = { + close: jest.fn(), + paddingSize: 's', +}; + +const renderComponent = ({ + fetchDashboardPanels, +}: Pick, 'fetchDashboardPanels'>) => + render( + + + + ); + +const panelConfiguration: GroupedAddPanelActions[] = [ + { + id: 'panel1', + title: 'App 1', + items: [ + { + icon: 'icon1', + id: 'mockFactory', + name: 'Factory 1', + description: 'Factory 1 description', + 'data-test-subj': 'createNew-mockFactory', + onClick: jest.fn(), + order: 0, + }, + ], + order: 10, + 'data-test-subj': 'dashboardEditorMenu-group1Group', + }, +]; + +describe('DashboardPanelSelectionListFlyout', () => { + it('renders a loading indicator when fetchDashboardPanel has not yielded any value', async () => { + const promiseDelay = 5000; + + renderComponent({ + fetchDashboardPanels: jest.fn( + () => + new Promise((resolve) => { + setTimeout(() => resolve(panelConfiguration), promiseDelay); + }) + ), + }); + + expect( + await screen.findByTestId('dashboardPanelSelectionLoadingIndicator') + ).toBeInTheDocument(); + }); + + it('renders an error indicator when fetchDashboardPanel errors', async () => { + renderComponent({ + fetchDashboardPanels: jest.fn().mockRejectedValue(new Error('simulated error')), + }); + + expect(await screen.findByTestId('dashboardPanelSelectionErrorIndicator')).toBeInTheDocument(); + }); + + it('renders the list of available panels when fetchDashboardPanel resolves a value', async () => { + renderComponent({ fetchDashboardPanels: jest.fn().mockResolvedValue(panelConfiguration) }); + + expect(await screen.findByTestId(panelConfiguration[0]['data-test-subj']!)).toBeInTheDocument(); + }); + + it('renders a not found message when a user searches for an item that is not in the selection list', async () => { + renderComponent({ fetchDashboardPanels: jest.fn().mockResolvedValue(panelConfiguration) }); + + expect(await screen.findByTestId(panelConfiguration[0]['data-test-subj']!)).toBeInTheDocument(); + + act(() => { + userEvent.type( + screen.getByTestId('dashboardPanelSelectionFlyout__searchInput'), + 'non existent panel' + ); + }); + + expect(await screen.findByTestId('dashboardPanelSelectionNoPanelMessage')).toBeInTheDocument(); + }); + + it('invokes the close method when the flyout close btn is clicked', async () => { + renderComponent({ fetchDashboardPanels: jest.fn().mockResolvedValue(panelConfiguration) }); + + fireEvent.click(await screen.findByTestId('dashboardPanelSelectionCloseBtn')); + + expect(defaultProps.close).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/dashboard_panel_selection_flyout.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/dashboard_panel_selection_flyout.tsx new file mode 100644 index 0000000000000..bb23fd6798b11 --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/dashboard_panel_selection_flyout.tsx @@ -0,0 +1,287 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React, { useEffect, useState } from 'react'; +import { i18n as i18nFn } from '@kbn/i18n'; +import { type EuiFlyoutProps, EuiLoadingChart } from '@elastic/eui'; +import orderBy from 'lodash/orderBy'; +import { + EuiEmptyPrompt, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiForm, + EuiBadge, + EuiFormRow, + EuiTitle, + EuiFieldSearch, + useEuiTheme, + EuiListGroup, + EuiListGroupItem, + EuiToolTip, + EuiText, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + type PanelSelectionMenuItem, + type GroupedAddPanelActions, +} from './add_panel_action_menu_items'; + +export interface DashboardPanelSelectionListFlyoutProps { + /** Handler to close flyout */ + close: () => void; + /** Padding for flyout */ + paddingSize: Exclude; + /** Fetches the panels available for a dashboard */ + fetchDashboardPanels: () => Promise; +} + +export const DashboardPanelSelectionListFlyout: React.FC< + DashboardPanelSelectionListFlyoutProps +> = ({ close, paddingSize, fetchDashboardPanels }) => { + const { euiTheme } = useEuiTheme(); + const [{ data: panels, loading, error }, setPanelState] = useState<{ + loading: boolean; + data: GroupedAddPanelActions[] | null; + error: unknown | null; + }>({ loading: true, data: null, error: null }); + + const [searchTerm, setSearchTerm] = useState(''); + const [panelsSearchResult, setPanelsSearchResult] = useState( + panels + ); + + useEffect(() => { + const requestDashboardPanels = () => { + fetchDashboardPanels() + .then((_panels) => + setPanelState((prevState) => ({ + ...prevState, + loading: false, + data: _panels, + })) + ) + .catch((err) => + setPanelState((prevState) => ({ + ...prevState, + loading: false, + error: err, + })) + ); + }; + + requestDashboardPanels(); + }, [fetchDashboardPanels]); + + useEffect(() => { + const _panels = (panels ?? []).slice(0); + + if (!searchTerm) { + return setPanelsSearchResult(_panels); + } + + const q = searchTerm.toLowerCase(); + + setPanelsSearchResult( + orderBy( + _panels.map((panel) => { + const groupSearchMatch = panel.title.toLowerCase().includes(q); + + const [groupSearchMatchAgg, items] = panel.items.reduce( + (acc, cur) => { + const searchMatch = cur.name.toLowerCase().includes(q); + + acc[0] = acc[0] || searchMatch; + acc[1].push({ + ...cur, + isDisabled: !(groupSearchMatch || searchMatch), + }); + + return acc; + }, + [groupSearchMatch, [] as PanelSelectionMenuItem[]] + ); + + return { + ...panel, + isDisabled: !groupSearchMatchAgg, + items, + }; + }), + ['isDisabled'] + ) + ); + }, [panels, searchTerm]); + + return ( + <> + + +

+ +

+
+
+ + + + + + { + setSearchTerm(e.target.value); + }} + aria-label={i18nFn.translate( + 'dashboard.editorMenu.addPanelFlyout.searchLabelText', + { defaultMessage: 'search field for panels' } + )} + className="nsPanelSelectionFlyout__searchInput" + data-test-subj="dashboardPanelSelectionFlyout__searchInput" + /> + + + + + {loading ? ( + } + /> + ) : ( + + {panelsSearchResult?.some(({ isDisabled }) => !isDisabled) ? ( + panelsSearchResult.map( + ({ id, title, items, isDisabled, ['data-test-subj']: dataTestSubj }) => + !isDisabled ? ( + + + {typeof title === 'string' ?

{title}

: title} +
+ + {items?.map((item, idx) => { + return ( + + {!item.isDeprecated ? ( + {item.name} + ) : ( + + + {item.name} + + + + + + + + )} + + } + onClick={item?.onClick} + iconType={item.icon} + data-test-subj={item['data-test-subj']} + isDisabled={item.isDisabled} + /> + ); + })} + +
+ ) : null + ) + ) : ( + <> + {Boolean(error) ? ( + + + + } + data-test-subj="dashboardPanelSelectionErrorIndicator" + /> + ) : ( + + + + )} + + )} +
+ )} +
+
+
+ + + + + + + + + + + ); +}; diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/index.ts b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/index.ts new file mode 100644 index 0000000000000..5b94fba6bfffc --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/index.ts @@ -0,0 +1,10 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +export { useGetDashboardPanels } from './use_get_dashboard_panels'; +export { DashboardPanelSelectionListFlyout } from './dashboard_panel_selection_flyout'; diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/use_get_dashboard_panels.test.ts b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/use_get_dashboard_panels.test.ts new file mode 100644 index 0000000000000..b8ca0cd27aa01 --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/use_get_dashboard_panels.test.ts @@ -0,0 +1,220 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public'; +import type { PresentationContainer } from '@kbn/presentation-containers'; +import type { Action } from '@kbn/ui-actions-plugin/public'; +import { type BaseVisType, VisGroups, VisTypeAlias } from '@kbn/visualizations-plugin/public'; +import { COMMON_EMBEDDABLE_GROUPING } from '@kbn/embeddable-plugin/public'; +import { useGetDashboardPanels } from './use_get_dashboard_panels'; +import { pluginServices } from '../../../services/plugin_services'; + +const mockApi = { addNewPanel: jest.fn() } as unknown as jest.Mocked; + +describe('Get dashboard panels hook', () => { + const defaultHookProps: Parameters[0] = { + api: mockApi, + createNewVisType: jest.fn(), + }; + + type PluginServices = ReturnType; + + let compatibleTriggerActionsRequestSpy: jest.SpyInstance< + ReturnType> + >; + + let dashboardVisualizationGroupGetterSpy: jest.SpyInstance< + ReturnType + >; + + let dashboardVisualizationAliasesGetterSpy: jest.SpyInstance< + ReturnType + >; + + beforeAll(() => { + const _pluginServices = pluginServices.getServices(); + + compatibleTriggerActionsRequestSpy = jest.spyOn( + _pluginServices.uiActions, + 'getTriggerCompatibleActions' + ); + + dashboardVisualizationGroupGetterSpy = jest.spyOn(_pluginServices.visualizations, 'getByGroup'); + + dashboardVisualizationAliasesGetterSpy = jest.spyOn( + _pluginServices.visualizations, + 'getAliases' + ); + }); + + beforeEach(() => { + compatibleTriggerActionsRequestSpy.mockResolvedValue([]); + dashboardVisualizationGroupGetterSpy.mockReturnValue([]); + dashboardVisualizationAliasesGetterSpy.mockReturnValue([]); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + jest.resetAllMocks(); + }); + + describe('useGetDashboardPanels', () => { + it('hook return value is callable', () => { + const { result } = renderHook(() => useGetDashboardPanels(defaultHookProps)); + expect(result.current).toBeInstanceOf(Function); + }); + + it('returns a callable method that yields a cached result if invoked after a prior resolution', async () => { + const { result } = renderHook(() => useGetDashboardPanels(defaultHookProps)); + expect(result.current).toBeInstanceOf(Function); + + const firstInvocationResult = await result.current(jest.fn()); + + expect(compatibleTriggerActionsRequestSpy).toHaveBeenCalledWith(ADD_PANEL_TRIGGER, { + embeddable: expect.objectContaining(mockApi), + }); + + const secondInvocationResult = await result.current(jest.fn()); + + expect(firstInvocationResult).toStrictEqual(secondInvocationResult); + + expect(compatibleTriggerActionsRequestSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('augmenting ui action group items with dashboard visualization types', () => { + it.each([ + ['visualizations', VisGroups.PROMOTED], + [COMMON_EMBEDDABLE_GROUPING.legacy.id, VisGroups.LEGACY], + [COMMON_EMBEDDABLE_GROUPING.annotation.id, VisGroups.TOOLS], + ])( + 'includes in the ui action %s group, %s dashboard visualization group types', + async (uiActionGroupId, dashboardVisualizationGroupId) => { + const mockVisualizationsUiAction: Action = { + id: `some-${uiActionGroupId}-action`, + type: '', + order: 10, + grouping: [ + { + id: uiActionGroupId, + order: 1000, + getDisplayName: jest.fn(), + getIconType: jest.fn(), + }, + ], + getDisplayName: jest.fn(() => `Some ${uiActionGroupId} visualization Action`), + getIconType: jest.fn(), + execute: jest.fn(), + isCompatible: jest.fn(() => Promise.resolve(true)), + }; + + const mockDashboardVisualizationType = { + name: dashboardVisualizationGroupId, + title: dashboardVisualizationGroupId, + order: 0, + description: `This is a dummy representation of a ${dashboardVisualizationGroupId} visualization.`, + icon: 'empty', + stage: 'production', + isDeprecated: false, + group: dashboardVisualizationGroupId, + titleInWizard: `Custom ${dashboardVisualizationGroupId} visualization`, + } as BaseVisType; + + compatibleTriggerActionsRequestSpy.mockResolvedValue([mockVisualizationsUiAction]); + + dashboardVisualizationGroupGetterSpy.mockImplementation((group) => { + if (group !== dashboardVisualizationGroupId) return []; + + return [mockDashboardVisualizationType]; + }); + + const { result } = renderHook(() => useGetDashboardPanels(defaultHookProps)); + expect(result.current).toBeInstanceOf(Function); + + expect(await result.current(jest.fn())).toStrictEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: uiActionGroupId, + 'data-test-subj': `dashboardEditorMenu-${uiActionGroupId}Group`, + items: expect.arrayContaining([ + expect.objectContaining({ + // @ts-expect-error ignore passing the required context in this test + 'data-test-subj': `create-action-${mockVisualizationsUiAction.getDisplayName()}`, + }), + expect.objectContaining({ + 'data-test-subj': `visType-${mockDashboardVisualizationType.name}`, + }), + ]), + }), + ]) + ); + } + ); + + it('includes in the ui action visualization group dashboard visualization alias types', async () => { + const mockVisualizationsUiAction: Action = { + id: 'some-vis-action', + type: '', + order: 10, + grouping: [ + { + id: 'visualizations', + order: 1000, + getDisplayName: jest.fn(), + getIconType: jest.fn(), + }, + ], + getDisplayName: jest.fn(() => 'Some visualization Action'), + getIconType: jest.fn(), + execute: jest.fn(), + isCompatible: jest.fn(() => Promise.resolve(true)), + }; + + const mockedAliasVisualizationType: VisTypeAlias = { + name: 'alias visualization', + title: 'Alias Visualization', + order: 0, + description: 'This is a dummy representation of aan aliased visualization.', + icon: 'empty', + stage: 'production', + isDeprecated: false, + }; + + compatibleTriggerActionsRequestSpy.mockResolvedValue([mockVisualizationsUiAction]); + + dashboardVisualizationAliasesGetterSpy.mockReturnValue([mockedAliasVisualizationType]); + + const { result } = renderHook(() => useGetDashboardPanels(defaultHookProps)); + expect(result.current).toBeInstanceOf(Function); + + expect(await result.current(jest.fn())).toStrictEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: mockVisualizationsUiAction.grouping![0].id, + 'data-test-subj': `dashboardEditorMenu-${ + mockVisualizationsUiAction.grouping![0].id + }Group`, + items: expect.arrayContaining([ + expect.objectContaining({ + // @ts-expect-error ignore passing the required context in this test + 'data-test-subj': `create-action-${mockVisualizationsUiAction.getDisplayName()}`, + }), + expect.objectContaining({ + 'data-test-subj': `visType-${mockedAliasVisualizationType.name}`, + }), + ]), + }), + ]) + ); + }); + }); +}); diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/use_get_dashboard_panels.ts b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/use_get_dashboard_panels.ts new file mode 100644 index 0000000000000..13f7f7ff12dd0 --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/use_get_dashboard_panels.ts @@ -0,0 +1,220 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { useMemo, useRef, useCallback } from 'react'; +import type { IconType } from '@elastic/eui'; +import { ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public'; +import { type Subscription, AsyncSubject, from, defer, map, lastValueFrom } from 'rxjs'; +import { EmbeddableFactory, COMMON_EMBEDDABLE_GROUPING } from '@kbn/embeddable-plugin/public'; +import { PresentationContainer } from '@kbn/presentation-containers'; +import { type BaseVisType, VisGroups, type VisTypeAlias } from '@kbn/visualizations-plugin/public'; + +import { pluginServices } from '../../../services/plugin_services'; +import { + getAddPanelActionMenuItemsGroup, + type PanelSelectionMenuItem, + type GroupedAddPanelActions, +} from './add_panel_action_menu_items'; + +interface UseGetDashboardPanelsArgs { + api: PresentationContainer; + createNewVisType: (visType: BaseVisType | VisTypeAlias) => () => void; +} + +export interface FactoryGroup { + id: string; + appName: string; + icon?: IconType; + factories: EmbeddableFactory[]; + order: number; +} + +const sortGroupPanelsByOrder = (panelGroups: T[]): T[] => { + return panelGroups.sort( + // larger number sorted to the top + (panelGroupA, panelGroupB) => panelGroupB.order - panelGroupA.order + ); +}; + +export const useGetDashboardPanels = ({ api, createNewVisType }: UseGetDashboardPanelsArgs) => { + const panelsComputeResultCache = useRef(new AsyncSubject()); + const panelsComputeSubscription = useRef(null); + + const { + uiActions, + visualizations: { getAliases: getVisTypeAliases, getByGroup: getVisTypesByGroup }, + } = pluginServices.getServices(); + + const getSortedVisTypesByGroup = (group: VisGroups) => + getVisTypesByGroup(group) + .sort((a: BaseVisType | VisTypeAlias, b: BaseVisType | VisTypeAlias) => { + const labelA = 'titleInWizard' in a ? a.titleInWizard || a.title : a.title; + const labelB = 'titleInWizard' in b ? b.titleInWizard || a.title : a.title; + if (labelA < labelB) { + return -1; + } + if (labelA > labelB) { + return 1; + } + return 0; + }) + .filter(({ disableCreate }: BaseVisType) => !disableCreate); + + const promotedVisTypes = getSortedVisTypesByGroup(VisGroups.PROMOTED); + const toolVisTypes = getSortedVisTypesByGroup(VisGroups.TOOLS); + const legacyVisTypes = getSortedVisTypesByGroup(VisGroups.LEGACY); + + const visTypeAliases = getVisTypeAliases() + .sort(({ promotion: a = false }: VisTypeAlias, { promotion: b = false }: VisTypeAlias) => + a === b ? 0 : a ? -1 : 1 + ) + .filter(({ disableCreate }: VisTypeAlias) => !disableCreate); + + const augmentedCreateNewVisType = useCallback( + (visType: Parameters[0], cb: () => void) => { + const visClickHandler = createNewVisType(visType); + return () => { + visClickHandler(); + cb(); + }; + }, + [createNewVisType] + ); + + const getVisTypeMenuItem = useCallback( + (onClickCb: () => void, visType: BaseVisType): PanelSelectionMenuItem => { + const { + name, + title, + titleInWizard, + description, + icon = 'empty', + isDeprecated, + order, + } = visType; + return { + id: name, + name: titleInWizard || title, + isDeprecated, + icon, + onClick: augmentedCreateNewVisType(visType, onClickCb), + 'data-test-subj': `visType-${name}`, + description, + order, + }; + }, + [augmentedCreateNewVisType] + ); + + const getVisTypeAliasMenuItem = useCallback( + (onClickCb: () => void, visTypeAlias: VisTypeAlias): PanelSelectionMenuItem => { + const { name, title, description, icon = 'empty', order } = visTypeAlias; + + return { + id: name, + name: title, + icon, + onClick: augmentedCreateNewVisType(visTypeAlias, onClickCb), + 'data-test-subj': `visType-${name}`, + description, + order: order ?? 0, + }; + }, + [augmentedCreateNewVisType] + ); + + const addPanelAction$ = useMemo( + () => + defer(() => { + return from( + uiActions?.getTriggerCompatibleActions?.(ADD_PANEL_TRIGGER, { + embeddable: api, + }) ?? [] + ); + }), + [api, uiActions] + ); + + const computeAvailablePanels = useCallback( + (onPanelSelected: () => void) => { + if (!panelsComputeSubscription.current) { + panelsComputeSubscription.current = addPanelAction$ + .pipe( + map((addPanelActions) => + getAddPanelActionMenuItemsGroup(api, addPanelActions, onPanelSelected) + ), + map((groupedAddPanelAction) => { + return sortGroupPanelsByOrder( + Object.values(groupedAddPanelAction) + ).map((panelGroup) => { + switch (panelGroup.id) { + case 'visualizations': { + return { + ...panelGroup, + items: sortGroupPanelsByOrder( + (panelGroup.items ?? []).concat( + // TODO: actually add grouping to vis type alias so we wouldn't randomly display an unintended item + visTypeAliases.map(getVisTypeAliasMenuItem.bind(null, onPanelSelected)), + promotedVisTypes.map(getVisTypeMenuItem.bind(null, onPanelSelected)) + ) + ), + }; + } + case COMMON_EMBEDDABLE_GROUPING.legacy.id: { + return { + ...panelGroup, + items: sortGroupPanelsByOrder( + (panelGroup.items ?? []).concat( + legacyVisTypes.map(getVisTypeMenuItem.bind(null, onPanelSelected)) + ) + ), + }; + } + case COMMON_EMBEDDABLE_GROUPING.annotation.id: { + return { + ...panelGroup, + items: sortGroupPanelsByOrder( + (panelGroup.items ?? []).concat( + toolVisTypes.map(getVisTypeMenuItem.bind(null, onPanelSelected)) + ) + ), + }; + } + default: { + return { + ...panelGroup, + items: sortGroupPanelsByOrder(panelGroup.items), + }; + } + } + }); + }) + ) + .subscribe(panelsComputeResultCache.current); + } + }, + [ + api, + addPanelAction$, + getVisTypeMenuItem, + getVisTypeAliasMenuItem, + toolVisTypes, + legacyVisTypes, + promotedVisTypes, + visTypeAliases, + ] + ); + + return useCallback( + (...args: Parameters) => { + computeAvailablePanels(...args); + return lastValueFrom(panelsComputeResultCache.current.asObservable()); + }, + [computeAvailablePanels] + ); +}; diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.test.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.test.tsx index c2b51a5e0eda5..8310019f91b40 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.test.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.test.tsx @@ -6,134 +6,53 @@ * Side Public License, v 1. */ -import { EmbeddableFactory } from '@kbn/embeddable-plugin/public'; +import React, { ComponentProps } from 'react'; +import { render } from '@testing-library/react'; import { PresentationContainer } from '@kbn/presentation-containers'; -import { GroupedAddPanelActions } from './add_panel_action_menu_items'; -import { - FactoryGroup, - mergeGroupedItemsProvider, - getEmbeddableFactoryMenuItemProvider, -} from './editor_menu'; +import { EditorMenu } from './editor_menu'; +import { DashboardAPIContext } from '../dashboard_app'; +import { buildMockDashboard } from '../../mocks'; -describe('mergeGroupedItemsProvider', () => { - const mockApi = { addNewPanel: jest.fn() } as unknown as jest.Mocked; - const closePopoverSpy = jest.fn(); +import { pluginServices } from '../../services/plugin_services'; - const getEmbeddableFactoryMenuItem = getEmbeddableFactoryMenuItemProvider( - mockApi, - closePopoverSpy - ); +jest.mock('../../services/plugin_services', () => { + const module = jest.requireActual('../../services/plugin_services'); - const mockFactory = { - id: 'factory1', - type: 'mockFactory', - getDisplayName: () => 'Factory 1', - getDescription: () => 'Factory 1 description', - getIconType: () => 'icon1', - } as unknown as EmbeddableFactory; + const _pluginServices = (module.pluginServices as typeof pluginServices).getServices(); - const factoryGroupMap = { - group1: { - id: 'panel1', - appName: 'App 1', - icon: 'icon1', - order: 10, - factories: [mockFactory], - }, - } as unknown as Record; + jest + .spyOn(_pluginServices.embeddable, 'getEmbeddableFactories') + .mockReturnValue(new Map().values()); + jest.spyOn(_pluginServices.uiActions, 'getTriggerCompatibleActions').mockResolvedValue([]); + jest.spyOn(_pluginServices.visualizations, 'getByGroup').mockReturnValue([]); + jest.spyOn(_pluginServices.visualizations, 'getAliases').mockReturnValue([]); - const groupedAddPanelAction = { - group1: { - id: 'panel2', - title: 'Panel 2', - icon: 'icon2', - order: 10, - items: [ - { - id: 'addPanelActionId', - order: 0, - }, - ], + return { + ...module, + pluginServices: { + ...module.pluginServices, + getServices: jest.fn().mockReturnValue(_pluginServices), }, - } as unknown as Record; - - it('should merge factoryGroupMap and groupedAddPanelAction correctly', () => { - const groupedPanels = mergeGroupedItemsProvider(getEmbeddableFactoryMenuItem)( - factoryGroupMap, - groupedAddPanelAction - ); - - expect(groupedPanels).toEqual([ - { - id: 'panel1', - title: 'App 1', - items: [ - { - icon: 'icon1', - name: 'Factory 1', - id: 'mockFactory', - description: 'Factory 1 description', - 'data-test-subj': 'createNew-mockFactory', - onClick: expect.any(Function), - order: 0, - }, - { - id: 'addPanelActionId', - order: 0, - }, - ], - 'data-test-subj': 'dashboardEditorMenu-group1Group', - order: 10, - }, - ]); - }); - - it('should handle missing factoryGroup correctly', () => { - const groupedPanels = mergeGroupedItemsProvider(getEmbeddableFactoryMenuItem)( - {}, - groupedAddPanelAction - ); - - expect(groupedPanels).toEqual([ - { - id: 'panel2', - icon: 'icon2', - title: 'Panel 2', - items: [ - { - id: 'addPanelActionId', - order: 0, - }, - ], - order: 10, - }, - ]); - }); - - it('should handle missing groupedAddPanelAction correctly', () => { - const groupedPanels = mergeGroupedItemsProvider(getEmbeddableFactoryMenuItem)( - factoryGroupMap, - {} - ); + }; +}); - expect(groupedPanels).toEqual([ - { - id: 'panel1', - title: 'App 1', - items: [ - { - icon: 'icon1', - id: 'mockFactory', - name: 'Factory 1', - description: 'Factory 1 description', - 'data-test-subj': 'createNew-mockFactory', - onClick: expect.any(Function), - order: 0, - }, - ], - order: 10, - 'data-test-subj': 'dashboardEditorMenu-group1Group', +const mockApi = { addNewPanel: jest.fn() } as unknown as jest.Mocked; + +describe('editor menu', () => { + const defaultProps: ComponentProps = { + api: mockApi, + createNewVisType: jest.fn(), + }; + + it('renders without crashing', async () => { + render(, { + wrapper: ({ children }) => { + return ( + + {children} + + ); }, - ]); + }); }); }); diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx index ba7811dfec360..c7902dd632f95 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx @@ -8,354 +8,79 @@ import './editor_menu.scss'; -import React, { useEffect, useMemo, useState, useRef } from 'react'; -import { type IconType } from '@elastic/eui'; +import React, { useEffect, useCallback, type ComponentProps } from 'react'; + import { i18n } from '@kbn/i18n'; -import { type Action, ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public'; +import { toMountPoint } from '@kbn/react-kibana-mount'; import { ToolbarButton } from '@kbn/shared-ux-button-toolbar'; -import { PresentationContainer } from '@kbn/presentation-containers'; -import { type BaseVisType, VisGroups, type VisTypeAlias } from '@kbn/visualizations-plugin/public'; -import { EmbeddableFactory, COMMON_EMBEDDABLE_GROUPING } from '@kbn/embeddable-plugin/public'; + +import { useGetDashboardPanels, DashboardPanelSelectionListFlyout } from './add_new_panel'; import { pluginServices } from '../../services/plugin_services'; -import { - getAddPanelActionMenuItemsGroup, - type PanelSelectionMenuItem, - type GroupedAddPanelActions, -} from './add_panel_action_menu_items'; -import { openDashboardPanelSelectionFlyout } from './open_dashboard_panel_selection_flyout'; -import type { DashboardServices } from '../../services/types'; import { useDashboardAPI } from '../dashboard_app'; -export interface FactoryGroup { - id: string; - appName: string; - icon?: IconType; - factories: EmbeddableFactory[]; - order: number; -} - -interface UnwrappedEmbeddableFactory { - factory: EmbeddableFactory; - isEditable: boolean; -} - -export type GetEmbeddableFactoryMenuItem = ReturnType; - -export const getEmbeddableFactoryMenuItemProvider = - (api: PresentationContainer, closePopover: () => void) => - (factory: EmbeddableFactory): PanelSelectionMenuItem => { - const icon = factory?.getIconType ? factory.getIconType() : 'empty'; - - return { - id: factory.type, - name: factory.getDisplayName(), - icon, - description: factory.getDescription?.(), - onClick: async () => { - closePopover(); - api.addNewPanel({ panelType: factory.type }, true); - }, - 'data-test-subj': `createNew-${factory.type}`, - order: factory.order ?? 0, - }; - }; - -const sortGroupPanelsByOrder = (panelGroups: T[]): T[] => { - return panelGroups.sort( - // larger number sorted to the top - (panelGroupA, panelGroupB) => panelGroupB.order - panelGroupA.order - ); -}; - -export const mergeGroupedItemsProvider = - (getEmbeddableFactoryMenuItem: GetEmbeddableFactoryMenuItem) => - ( - factoryGroupMap: Record, - groupedAddPanelAction: Record - ) => { - const panelGroups: GroupedAddPanelActions[] = []; - - new Set(Object.keys(factoryGroupMap).concat(Object.keys(groupedAddPanelAction))).forEach( - (groupId) => { - const dataTestSubj = `dashboardEditorMenu-${groupId}Group`; - - const factoryGroup = factoryGroupMap[groupId]; - const addPanelGroup = groupedAddPanelAction[groupId]; - - if (factoryGroup && addPanelGroup) { - panelGroups.push({ - id: factoryGroup.id, - title: factoryGroup.appName, - 'data-test-subj': dataTestSubj, - order: factoryGroup.order, - items: [ - ...factoryGroup.factories.map(getEmbeddableFactoryMenuItem), - ...(addPanelGroup?.items ?? []), - ], - }); - } else if (factoryGroup) { - panelGroups.push({ - id: factoryGroup.id, - title: factoryGroup.appName, - 'data-test-subj': dataTestSubj, - order: factoryGroup.order, - items: factoryGroup.factories.map(getEmbeddableFactoryMenuItem), - }); - } else if (addPanelGroup) { - panelGroups.push(addPanelGroup); - } - } - ); - - return panelGroups; - }; - -interface EditorMenuProps { - api: PresentationContainer; +interface EditorMenuProps + extends Pick[0], 'api' | 'createNewVisType'> { isDisabled?: boolean; - /** Handler for creating new visualization of a specified type */ - createNewVisType: (visType: BaseVisType | VisTypeAlias) => () => void; } export const EditorMenu = ({ createNewVisType, isDisabled, api }: EditorMenuProps) => { - const isMounted = useRef(false); - const flyoutRef = useRef>(); - const dashboard = useDashboardAPI(); - - useEffect(() => { - isMounted.current = true; - - return () => { - isMounted.current = false; - flyoutRef.current?.close(); - }; - }, []); + const dashboardAPI = useDashboardAPI(); const { - embeddable, - visualizations: { getAliases: getVisTypeAliases, getByGroup: getVisTypesByGroup }, - uiActions, + overlays, + analytics, + settings: { i18n: i18nStart, theme }, } = pluginServices.getServices(); - const [unwrappedEmbeddableFactories, setUnwrappedEmbeddableFactories] = useState< - UnwrappedEmbeddableFactory[] - >([]); - - const [addPanelActions, setAddPanelActions] = useState> | undefined>( - undefined - ); - - const embeddableFactories = useMemo( - () => Array.from(embeddable.getEmbeddableFactories()), - [embeddable] - ); - - useEffect(() => { - Promise.all( - embeddableFactories.map>(async (factory) => ({ - factory, - isEditable: await factory.isEditable(), - })) - ).then((factories) => { - setUnwrappedEmbeddableFactories(factories); - }); - }, [embeddableFactories]); - - const getSortedVisTypesByGroup = (group: VisGroups) => - getVisTypesByGroup(group) - .sort((a: BaseVisType | VisTypeAlias, b: BaseVisType | VisTypeAlias) => { - const labelA = 'titleInWizard' in a ? a.titleInWizard || a.title : a.title; - const labelB = 'titleInWizard' in b ? b.titleInWizard || a.title : a.title; - if (labelA < labelB) { - return -1; - } - if (labelA > labelB) { - return 1; - } - return 0; - }) - .filter(({ disableCreate }: BaseVisType) => !disableCreate); - - const promotedVisTypes = getSortedVisTypesByGroup(VisGroups.PROMOTED); - const toolVisTypes = getSortedVisTypesByGroup(VisGroups.TOOLS); - const legacyVisTypes = getSortedVisTypesByGroup(VisGroups.LEGACY); - - const visTypeAliases = getVisTypeAliases() - .sort(({ promotion: a = false }: VisTypeAlias, { promotion: b = false }: VisTypeAlias) => - a === b ? 0 : a ? -1 : 1 - ) - .filter(({ disableCreate }: VisTypeAlias) => !disableCreate); - - const factories = unwrappedEmbeddableFactories.filter( - ({ isEditable, factory: { type, canCreateNew, isContainerType } }) => - isEditable && !isContainerType && canCreateNew() && type !== 'visualization' - ); - - const factoryGroupMap: Record = {}; - - // Retrieve ADD_PANEL_TRIGGER actions - useEffect(() => { - async function loadPanelActions() { - const registeredActions = await uiActions?.getTriggerCompatibleActions?.(ADD_PANEL_TRIGGER, { - embeddable: api, - }); - - if (isMounted.current) { - setAddPanelActions(registeredActions); - } - } - loadPanelActions(); - }, [uiActions, api]); - - factories.forEach(({ factory }) => { - const { grouping } = factory; - - if (grouping) { - grouping.forEach((group) => { - if (factoryGroupMap[group.id]) { - factoryGroupMap[group.id].factories.push(factory); - } else { - factoryGroupMap[group.id] = { - id: group.id, - appName: group.getDisplayName - ? group.getDisplayName({ embeddable: dashboard }) - : group.id, - icon: group.getIconType?.({ embeddable: dashboard }), - factories: [factory], - order: group.order ?? 0, - }; - } - }); - } else { - const fallbackGroup = COMMON_EMBEDDABLE_GROUPING.other; - - if (!factoryGroupMap[fallbackGroup.id]) { - factoryGroupMap[fallbackGroup.id] = { - id: fallbackGroup.id, - appName: fallbackGroup.getDisplayName - ? fallbackGroup.getDisplayName({ embeddable: dashboard }) - : fallbackGroup.id, - icon: fallbackGroup.getIconType?.({ embeddable: dashboard }) || 'empty', - factories: [], - order: fallbackGroup.order ?? 0, - }; - } - - factoryGroupMap[fallbackGroup.id].factories.push(factory); - } + const fetchDashboardPanels = useGetDashboardPanels({ + api, + createNewVisType, }); - const augmentedCreateNewVisType = ( - visType: Parameters[0], - cb: () => void - ) => { - const visClickHandler = createNewVisType(visType); + useEffect(() => { + // ensure opened dashboard is closed if a navigation event happens; return () => { - visClickHandler(); - cb(); + dashboardAPI.clearOverlays(); }; - }; - - const getVisTypeMenuItem = ( - onClickCb: () => void, - visType: BaseVisType - ): PanelSelectionMenuItem => { - const { - name, - title, - titleInWizard, - description, - icon = 'empty', - isDeprecated, - order, - } = visType; - return { - id: name, - name: titleInWizard || title, - isDeprecated, - icon, - onClick: augmentedCreateNewVisType(visType, onClickCb), - 'data-test-subj': `visType-${name}`, - description, - order, - }; - }; - - const getVisTypeAliasMenuItem = ( - onClickCb: () => void, - visTypeAlias: VisTypeAlias - ): PanelSelectionMenuItem => { - const { name, title, description, icon = 'empty', order } = visTypeAlias; - - return { - id: name, - name: title, - icon, - onClick: augmentedCreateNewVisType(visTypeAlias, onClickCb), - 'data-test-subj': `visType-${name}`, - description, - order: order ?? 0, - }; - }; - - const getEditorMenuPanels = (closeFlyout: () => void): GroupedAddPanelActions[] => { - const getEmbeddableFactoryMenuItem = getEmbeddableFactoryMenuItemProvider(api, closeFlyout); - - const groupedAddPanelAction = getAddPanelActionMenuItemsGroup( - api, - addPanelActions, - closeFlyout - ); - - const initialPanelGroups = mergeGroupedItemsProvider(getEmbeddableFactoryMenuItem)( - factoryGroupMap, - groupedAddPanelAction - ); - - // enhance panel groups - return sortGroupPanelsByOrder(initialPanelGroups).map((panelGroup) => { - switch (panelGroup.id) { - case 'visualizations': { - return { - ...panelGroup, - items: sortGroupPanelsByOrder( - (panelGroup.items ?? []).concat( - // TODO: actually add grouping to vis type alias so we wouldn't randomly display an unintended item - visTypeAliases.map(getVisTypeAliasMenuItem.bind(null, closeFlyout)), - promotedVisTypes.map(getVisTypeMenuItem.bind(null, closeFlyout)) - ) - ), - }; - } - case COMMON_EMBEDDABLE_GROUPING.legacy.id: { - return { - ...panelGroup, - items: sortGroupPanelsByOrder( - (panelGroup.items ?? []).concat( - legacyVisTypes.map(getVisTypeMenuItem.bind(null, closeFlyout)) - ) - ), - }; - } - case COMMON_EMBEDDABLE_GROUPING.annotation.id: { - return { - ...panelGroup, - items: sortGroupPanelsByOrder( - (panelGroup.items ?? []).concat( - toolVisTypes.map(getVisTypeMenuItem.bind(null, closeFlyout)) - ) - ), - }; - } - default: { - return { - ...panelGroup, - items: sortGroupPanelsByOrder(panelGroup.items), - }; - } - } - }); - }; + }, [dashboardAPI]); + + const openDashboardPanelSelectionFlyout = useCallback( + function openDashboardPanelSelectionFlyout() { + const flyoutPanelPaddingSize: ComponentProps< + typeof DashboardPanelSelectionListFlyout + >['paddingSize'] = 'l'; + + const mount = toMountPoint( + React.createElement(function () { + return ( + + ); + }), + { analytics, theme, i18n: i18nStart } + ); + + dashboardAPI.openOverlay( + overlays.openFlyout(mount, { + size: 'm', + maxWidth: 500, + paddingSize: flyoutPanelPaddingSize, + 'aria-labelledby': 'addPanelsFlyout', + 'data-test-subj': 'dashboardPanelSelectionFlyout', + onClose(overlayRef) { + dashboardAPI.clearOverlays(); + overlayRef.close(); + }, + }) + ); + }, + [analytics, theme, i18nStart, dashboardAPI, overlays, fetchDashboardPanels] + ); return ( { - flyoutRef.current = openDashboardPanelSelectionFlyout({ - getPanels: getEditorMenuPanels, - }); - }} + onClick={openDashboardPanelSelectionFlyout} size="s" /> ); diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/open_dashboard_panel_selection_flyout.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/open_dashboard_panel_selection_flyout.tsx deleted file mode 100644 index 8bd8dffc67c97..0000000000000 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/open_dashboard_panel_selection_flyout.tsx +++ /dev/null @@ -1,255 +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 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 or the Server - * Side Public License, v 1. - */ - -import React, { useEffect, useState, useRef } from 'react'; -import { toMountPoint } from '@kbn/react-kibana-mount'; -import { i18n as i18nFn } from '@kbn/i18n'; -import orderBy from 'lodash/orderBy'; -import { - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiForm, - EuiBadge, - EuiFormRow, - EuiTitle, - EuiFieldSearch, - useEuiTheme, - type EuiFlyoutProps, - EuiListGroup, - EuiListGroupItem, - EuiToolTip, - EuiText, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { pluginServices } from '../../services/plugin_services'; -import type { DashboardServices } from '../../services/types'; -import type { GroupedAddPanelActions, PanelSelectionMenuItem } from './add_panel_action_menu_items'; - -interface OpenDashboardPanelSelectionFlyoutArgs { - getPanels: (closePopover: () => void) => GroupedAddPanelActions[]; - flyoutPanelPaddingSize?: Exclude; -} - -interface Props extends Pick { - /** Handler to close flyout */ - close: () => void; - /** Padding for flyout */ - paddingSize: Exclude; -} - -export function openDashboardPanelSelectionFlyout({ - getPanels, - flyoutPanelPaddingSize = 'l', -}: OpenDashboardPanelSelectionFlyoutArgs) { - const { - overlays, - analytics, - settings: { i18n, theme }, - } = pluginServices.getServices(); - // eslint-disable-next-line prefer-const - let flyoutRef: ReturnType; - - const mount = toMountPoint( - React.createElement(function () { - const closeFlyout = () => flyoutRef.close(); - return ( - - ); - }), - { analytics, theme, i18n } - ); - - flyoutRef = overlays.openFlyout(mount, { - size: 'm', - maxWidth: 500, - paddingSize: flyoutPanelPaddingSize, - 'aria-labelledby': 'addPanelsFlyout', - 'data-test-subj': 'dashboardPanelSelectionFlyout', - }); - - return flyoutRef; -} - -export const DashboardPanelSelectionListFlyout: React.FC = ({ - close, - getPanels, - paddingSize, -}) => { - const { euiTheme } = useEuiTheme(); - const panels = useRef(getPanels(close)); - const [searchTerm, setSearchTerm] = useState(''); - const [panelsSearchResult, setPanelsSearchResult] = useState( - panels.current - ); - - useEffect(() => { - if (!searchTerm) { - return setPanelsSearchResult(panels.current); - } - - const q = searchTerm.toLowerCase(); - - setPanelsSearchResult( - orderBy( - panels.current.map((panel) => { - const groupSearchMatch = panel.title.toLowerCase().includes(q); - - const [groupSearchMatchAgg, items] = panel.items.reduce( - (acc, cur) => { - const searchMatch = cur.name.toLowerCase().includes(q); - - acc[0] = acc[0] || searchMatch; - acc[1].push({ - ...cur, - isDisabled: !(groupSearchMatch || searchMatch), - }); - - return acc; - }, - [groupSearchMatch, [] as PanelSelectionMenuItem[]] - ); - - return { - ...panel, - isDisabled: !groupSearchMatchAgg, - items, - }; - }), - ['isDisabled'] - ) - ); - }, [searchTerm]); - - return ( - <> - - -

- -

-
-
- - - - - - { - setSearchTerm(e.target.value); - }} - aria-label={i18nFn.translate( - 'dashboard.editorMenu.addPanelFlyout.searchLabelText', - { defaultMessage: 'search field for panels' } - )} - className="nsPanelSelectionFlyout__searchInput" - data-test-subj="dashboardPanelSelectionFlyout__searchInput" - /> - - - - - - {panelsSearchResult.some(({ isDisabled }) => !isDisabled) ? ( - panelsSearchResult.map( - ({ id, title, items, isDisabled, ['data-test-subj']: dataTestSubj }) => - !isDisabled ? ( - - - {typeof title === 'string' ?

{title}

: title} -
- - {items?.map((item, idx) => { - return ( - - {!item.isDeprecated ? ( - {item.name} - ) : ( - - - {item.name} - - - - - - - - )} - - } - onClick={item?.onClick} - iconType={item.icon} - data-test-subj={item['data-test-subj']} - isDisabled={item.isDisabled} - /> - ); - })} - -
- ) : null - ) - ) : ( - - - - )} -
-
-
-
- - - - - - - - - - - ); -}; diff --git a/test/functional/services/dashboard/add_panel.ts b/test/functional/services/dashboard/add_panel.ts index 16b283f2b5c53..a279b33c8bc23 100644 --- a/test/functional/services/dashboard/add_panel.ts +++ b/test/functional/services/dashboard/add_panel.ts @@ -53,6 +53,9 @@ export class DashboardAddPanelService extends FtrService { this.log.debug('DashboardAddPanel.clickEditorMenuButton'); await this.testSubjects.click('dashboardEditorMenuButton'); await this.testSubjects.existOrFail('dashboardPanelSelectionFlyout'); + await this.retry.try(async () => { + return await this.testSubjects.exists('dashboardPanelSelectionList'); + }); } async expectEditorMenuClosed() { diff --git a/x-pack/plugins/alerting/server/rule_type_registry.test.ts b/x-pack/plugins/alerting/server/rule_type_registry.test.ts index 3ee3551a301d5..e678228660e51 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.test.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.test.ts @@ -564,6 +564,39 @@ describe('Create Lifecycle', () => { }); }); + test('injects custom cost for certain rule types', () => { + const ruleType: RuleType = { + id: 'siem.indicatorRule', + name: 'Test', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + executor: jest.fn(), + category: 'test', + producer: 'alerts', + ruleTaskTimeout: '20m', + validate: { + params: { validate: (params) => params }, + }, + }; + const registry = new RuleTypeRegistry(ruleTypeRegistryParams); + registry.register(ruleType); + expect(taskManager.registerTaskDefinitions).toHaveBeenCalledTimes(1); + expect(taskManager.registerTaskDefinitions.mock.calls[0][0]).toMatchObject({ + 'alerting:siem.indicatorRule': { + timeout: '20m', + title: 'Test', + cost: 10, + }, + }); + }); + test('shallow clones the given rule type', () => { const ruleType: RuleType = { id: 'test', diff --git a/x-pack/plugins/alerting/server/rule_type_registry.ts b/x-pack/plugins/alerting/server/rule_type_registry.ts index d1ffe59df3b6f..bc7a10d767ff0 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.ts @@ -14,6 +14,7 @@ import { Logger } from '@kbn/core/server'; import { LicensingPluginSetup } from '@kbn/licensing-plugin/server'; import { RunContext, TaskManagerSetupContract } from '@kbn/task-manager-plugin/server'; import { stateSchemaByVersion } from '@kbn/alerting-state-types'; +import { TaskCost } from '@kbn/task-manager-plugin/server/task'; import { TaskRunnerFactory } from './task_runner'; import { RuleType, @@ -40,6 +41,9 @@ import { AlertsService } from './alerts_service/alerts_service'; import { getRuleTypeIdValidLegacyConsumers } from './rule_type_registry_deprecated_consumers'; import { AlertingConfig } from './config'; +const RULE_TYPES_WITH_CUSTOM_COST: Record = { + 'siem.indicatorRule': TaskCost.ExtraLarge, +}; export interface ConstructorOptions { config: AlertingConfig; logger: Logger; @@ -289,6 +293,8 @@ export class RuleTypeRegistry { normalizedRuleType as unknown as UntypedNormalizedRuleType ); + const taskCost: TaskCost | undefined = RULE_TYPES_WITH_CUSTOM_COST[ruleType.id]; + this.taskManager.registerTaskDefinitions({ [`alerting:${ruleType.id}`]: { title: ruleType.name, @@ -310,6 +316,7 @@ export class RuleTypeRegistry { spaceId: schema.string(), consumer: schema.maybe(schema.string()), }), + ...(taskCost ? { cost: taskCost } : {}), }, }); diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encryption_key_rotation_service.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encryption_key_rotation_service.ts index d8fa12a3e4973..80275ce3a91e4 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encryption_key_rotation_service.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encryption_key_rotation_service.ts @@ -205,7 +205,7 @@ export class EncryptionKeyRotationService { } this.options.logger.info( - `Encryption key rotation is completed. ${result.successful} objects out ouf ${result.total} were successfully re-encrypted with the primary encryption key and ${result.failed} objects failed.` + `Encryption key rotation is completed. ${result.successful} objects out of ${result.total} were successfully re-encrypted with the primary encryption key and ${result.failed} objects failed.` ); return result; diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts index 7aecb03868fa8..e7e0a7b482c03 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts @@ -110,22 +110,21 @@ export class EncryptedSavedObjectsPlugin getStartServices: core.getStartServices, }); - // In the serverless environment, the encryption keys for saved objects is managed internally and never - // exposed to users and administrators, eliminating the need for any public Encrypted Saved Objects HTTP APIs - if (this.initializerContext.env.packageInfo.buildFlavor !== 'serverless') { - defineRoutes({ - router: core.http.createRouter(), - logger: this.initializerContext.logger.get('routes'), - encryptionKeyRotationService: Object.freeze( - new EncryptionKeyRotationService({ - logger: this.logger.get('key-rotation-service'), - service, - getStartServices: core.getStartServices, - }) - ), - config, - }); - } + // Expose the key rotation route for both stateful and serverless environments + // The endpoint requires admin privileges, and is internal only in serverless + defineRoutes({ + router: core.http.createRouter(), + logger: this.initializerContext.logger.get('routes'), + encryptionKeyRotationService: Object.freeze( + new EncryptionKeyRotationService({ + logger: this.logger.get('key-rotation-service'), + service, + getStartServices: core.getStartServices, + }) + ), + config, + buildFlavor: this.initializerContext.env.packageInfo.buildFlavor, + }); return { canEncrypt, diff --git a/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts b/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts index 822427687759f..e6263521f690d 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { BuildFlavor } from '@kbn/config'; import { httpServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; import type { ConfigType } from '../config'; @@ -17,5 +18,6 @@ export const routeDefinitionParamsMock = { logger: loggingSystemMock.create().get(), config: ConfigSchema.validate(config) as ConfigType, encryptionKeyRotationService: encryptionKeyRotationServiceMock.create(), + buildFlavor: 'traditional' as BuildFlavor, }), }; diff --git a/x-pack/plugins/encrypted_saved_objects/server/routes/index.ts b/x-pack/plugins/encrypted_saved_objects/server/routes/index.ts index 28f8dde589c75..14d1933e8b765 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/routes/index.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/routes/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { BuildFlavor } from '@kbn/config'; import type { IRouter, Logger } from '@kbn/core/server'; import type { PublicMethodsOf } from '@kbn/utility-types'; @@ -20,6 +21,7 @@ export interface RouteDefinitionParams { logger: Logger; config: ConfigType; encryptionKeyRotationService: PublicMethodsOf; + buildFlavor: BuildFlavor; } export function defineRoutes(params: RouteDefinitionParams) { diff --git a/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.ts b/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.ts index c9c452cf9a031..e26efbb2a93b3 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.ts @@ -23,6 +23,7 @@ export function defineKeyRotationRoutes({ router, logger, config, + buildFlavor, }: RouteDefinitionParams) { let rotationInProgress = false; router.post( @@ -41,6 +42,7 @@ export function defineKeyRotationRoutes({ options: { tags: ['access:rotateEncryptionKey', 'oas-tag:saved objects'], description: `Rotate a key for encrypted saved objects`, + access: buildFlavor === 'serverless' ? 'internal' : undefined, }, }, async (context, request, response) => { diff --git a/x-pack/plugins/encrypted_saved_objects/tsconfig.json b/x-pack/plugins/encrypted_saved_objects/tsconfig.json index 83cdcd6225850..d2115146a4a42 100644 --- a/x-pack/plugins/encrypted_saved_objects/tsconfig.json +++ b/x-pack/plugins/encrypted_saved_objects/tsconfig.json @@ -14,6 +14,7 @@ "@kbn/core-saved-objects-api-server-mocks", "@kbn/core-security-common", "@kbn/test-jest-helpers", + "@kbn/config", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_error.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_error.tsx new file mode 100644 index 0000000000000..ddfa1e32b0ba4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_error.tsx @@ -0,0 +1,197 @@ +/* + * 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 { useEffect, useState } from 'react'; + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + InferenceServiceSettings, + MappingProperty, + MappingPropertyBase, + MappingTypeMapping, +} from '@elastic/elasticsearch/lib/api/types'; + +import { EuiButton, EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { LocalInferenceServiceSettings } from '@kbn/ml-trained-models-utils/src/constants/trained_models'; + +import { KibanaLogic } from '../../../shared/kibana'; +import { mappingsWithPropsApiLogic } from '../../api/mappings/mappings_logic'; + +export interface IndexErrorProps { + indexName: string; +} + +interface SemanticTextProperty extends MappingPropertyBase { + inference_id: string; + type: 'semantic_text'; +} + +const parseMapping = (mappings: MappingTypeMapping) => { + const fields = mappings.properties; + if (!fields) { + return []; + } + return getSemanticTextFields(fields, ''); +}; + +const getSemanticTextFields = ( + fields: Record, + path: string +): Array<{ path: string; source: SemanticTextProperty }> => { + return Object.entries(fields).flatMap(([key, value]) => { + const currentPath: string = path ? `${path}.${key}` : key; + const currentField: Array<{ path: string; source: SemanticTextProperty }> = + // @ts-expect-error because semantic_text type isn't incorporated in API type yet + value.type === 'semantic_text' ? [{ path: currentPath, source: value }] : []; + if (hasProperties(value)) { + const childSemanticTextFields: Array<{ path: string; source: SemanticTextProperty }> = + value.properties ? getSemanticTextFields(value.properties, currentPath) : []; + return [...currentField, ...childSemanticTextFields]; + } + return currentField; + }); +}; + +function hasProperties(field: MappingProperty): field is MappingPropertyBase { + return !!(field as MappingPropertyBase).properties; +} + +function isLocalModel(model: InferenceServiceSettings): model is LocalInferenceServiceSettings { + return Boolean((model as LocalInferenceServiceSettings).service_settings.model_id); +} + +export const IndexError: React.FC = ({ indexName }) => { + const { makeRequest: makeMappingRequest } = useActions(mappingsWithPropsApiLogic(indexName)); + const { data } = useValues(mappingsWithPropsApiLogic(indexName)); + const { ml } = useValues(KibanaLogic); + const [errors, setErrors] = useState< + Array<{ error: string; field: { path: string; source: SemanticTextProperty } }> + >([]); + + const [showErrors, setShowErrors] = useState(false); + + useEffect(() => { + makeMappingRequest({ indexName }); + }, [indexName]); + + useEffect(() => { + const mappings = data?.mappings; + if (!mappings || !ml) { + return; + } + + const semanticTextFields = parseMapping(mappings); + const fetchErrors = async () => { + const trainedModelStats = await ml?.mlApi?.trainedModels.getTrainedModelStats(); + const endpoints = await ml?.mlApi?.inferenceModels.getAllInferenceEndpoints(); + if (!trainedModelStats || !endpoints) { + return []; + } + + const semanticTextFieldsWithErrors = semanticTextFields + .map((field) => { + const model = endpoints.endpoints.find( + (endpoint) => endpoint.model_id === field.source.inference_id + ); + if (!model) { + return { + error: i18n.translate( + 'xpack.enterpriseSearch.indexOverview.indexErrors.missingModelError', + { + defaultMessage: 'Model not found for inference endpoint {inferenceId}', + values: { + inferenceId: field.source.inference_id as string, + }, + } + ), + field, + }; + } + if (isLocalModel(model)) { + const modelId = model.service_settings.model_id; + const modelStats = trainedModelStats?.trained_model_stats.find( + (value) => value.model_id === modelId + ); + if (!modelStats || modelStats.deployment_stats?.state !== 'started') { + return { + error: i18n.translate( + 'xpack.enterpriseSearch.indexOverview.indexErrors.missingModelError', + { + defaultMessage: + 'Model {modelId} for inference endpoint {inferenceId} in field {fieldName} has not been started', + values: { + fieldName: field.path, + inferenceId: field.source.inference_id as string, + modelId, + }, + } + ), + field, + }; + } + } + return { error: '', field }; + }) + .filter((value) => !!value.error); + setErrors(semanticTextFieldsWithErrors); + }; + + if (semanticTextFields.length) { + fetchErrors(); + } + }, [data]); + return errors.length > 0 ? ( + + {showErrors && ( + <> +

+ {i18n.translate('xpack.enterpriseSearch.indexOverview.indexErrors.body', { + defaultMessage: 'Found errors in the following fields:', + })} + {errors.map(({ field, error }) => ( +

  • + {field.path}: {error} +
  • + ))} +

    + setShowErrors(false)} + > + {i18n.translate('xpack.enterpriseSearch.indexOverview.indexErrors.hideErrorsLabel', { + defaultMessage: 'Hide full error', + })} + + + )} + {!showErrors && ( + setShowErrors(true)} + > + {i18n.translate('xpack.enterpriseSearch.indexOverview.indexErrors.showErrorsLabel', { + defaultMessage: 'Show full error', + })} + + )} +
    + ) : null; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/search_index.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/search_index.tsx index 7c13a3f524ccf..538cc1c575fc9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/search_index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/search_index.tsx @@ -40,6 +40,7 @@ import { CrawlerConfiguration } from './crawler/crawler_configuration/crawler_co import { SearchIndexDomainManagement } from './crawler/domain_management/domain_management'; import { NoConnectorRecord } from './crawler/no_connector_record'; import { SearchIndexDocuments } from './documents'; +import { IndexError } from './index_error'; import { SearchIndexIndexMappings } from './index_mappings'; import { IndexNameLogic } from './index_name_logic'; import { IndexViewLogic } from './index_view_logic'; @@ -239,6 +240,7 @@ export const SearchIndex: React.FC = () => { rightSideItems: getHeaderActions(index), }} > + {

    @@ -245,7 +245,7 @@ export const SemanticSearchGuide: React.FC = () => {

    @@ -293,7 +293,7 @@ export const SemanticSearchGuide: React.FC = () => {

    semantic_text }} />

    diff --git a/x-pack/plugins/fleet/common/types/models/agent_policy.ts b/x-pack/plugins/fleet/common/types/models/agent_policy.ts index bc70714b79158..1468d2ac5b11e 100644 --- a/x-pack/plugins/fleet/common/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/agent_policy.ts @@ -193,6 +193,10 @@ export interface FleetServerPolicy { * The coordinator index of the policy */ coordinator_idx: number; + /** + * The namespaces of the policy + */ + namespaces?: string[]; /** * The opaque payload. */ diff --git a/x-pack/plugins/fleet/server/services/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policy.test.ts index dd63d09c9d7c5..a5dbbc6b233b3 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts @@ -1230,6 +1230,7 @@ describe('Agent policy', () => { mockedGetFullAgentPolicy.mockResolvedValue({ id: 'policy123', revision: 1, + namespaces: ['mySpace'], inputs: [ { id: 'input-123', @@ -1282,10 +1283,16 @@ describe('Agent policy', () => { }), expect.objectContaining({ '@timestamp': expect.anything(), - data: { id: 'policy123', inputs: [{ id: 'input-123' }], revision: 1 }, + data: { + id: 'policy123', + inputs: [{ id: 'input-123' }], + revision: 1, + namespaces: ['mySpace'], + }, default_fleet_server: false, policy_id: 'policy123', revision_idx: 1, + namespaces: ['mySpace'], }), ], refresh: 'wait_for', diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 36a62af378c31..d243ef8b60e16 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -1137,6 +1137,7 @@ class AgentPolicyService { '@timestamp': new Date().toISOString(), revision_idx: fullPolicy.revision, coordinator_idx: 0, + namespaces: fullPolicy.namespaces, data: fullPolicy as unknown as FleetServerPolicy['data'], policy_id: fullPolicy.id, default_fleet_server: policy.is_default_fleet_server === true, diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index ba615678c334e..df37dd9707b04 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -648,7 +648,13 @@ export class LensPlugin { ); // Displays the add ESQL panel in the dashboard add Panel menu - const createESQLPanelAction = new CreateESQLPanelAction(startDependencies, core); + const createESQLPanelAction = new CreateESQLPanelAction(startDependencies, core, async () => { + if (!this.editorFrameService) { + await this.initDependenciesForApi(); + } + + return this.editorFrameService!; + }); startDependencies.uiActions.addTriggerAction(ADD_PANEL_TRIGGER, createESQLPanelAction); const discoverLocator = startDependencies.share?.url.locators.get('DISCOVER_APP_LOCATOR'); diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.test.tsx b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.test.tsx index 97b59a93829e3..63844b1d6d3ea 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.test.tsx +++ b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.test.tsx @@ -6,9 +6,10 @@ */ import type { CoreStart } from '@kbn/core/public'; import { coreMock } from '@kbn/core/public/mocks'; +import { getMockPresentationContainer } from '@kbn/presentation-containers/mocks'; import type { LensPluginStartDependencies } from '../../plugin'; +import type { EditorFrameService } from '../../editor_frame_service'; import { createMockStartDependencies } from '../../editor_frame_service/mocks'; -import { getMockPresentationContainer } from '@kbn/presentation-containers/mocks'; import { CreateESQLPanelAction } from './create_action'; describe('create Lens panel action', () => { @@ -16,9 +17,22 @@ describe('create Lens panel action', () => { const mockStartDependencies = createMockStartDependencies() as unknown as LensPluginStartDependencies; const mockPresentationContainer = getMockPresentationContainer(); + + const mockEditorFrameService = { + loadVisualizations: jest.fn(), + loadDatasources: jest.fn(), + } as unknown as EditorFrameService; + + const mockGetEditorFrameService = jest.fn(() => Promise.resolve(mockEditorFrameService)); + describe('compatibility check', () => { it('is incompatible if ui setting for ES|QL is off', async () => { - const configurablePanelAction = new CreateESQLPanelAction(mockStartDependencies, core); + const configurablePanelAction = new CreateESQLPanelAction( + mockStartDependencies, + core, + mockGetEditorFrameService + ); + const isCompatible = await configurablePanelAction.isCompatible({ embeddable: mockPresentationContainer, }); @@ -36,7 +50,13 @@ describe('create Lens panel action', () => { }, }, } as CoreStart; - const createESQLAction = new CreateESQLPanelAction(mockStartDependencies, updatedCore); + + const createESQLAction = new CreateESQLPanelAction( + mockStartDependencies, + updatedCore, + mockGetEditorFrameService + ); + const isCompatible = await createESQLAction.isCompatible({ embeddable: mockPresentationContainer, }); diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.tsx b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.tsx index f1d58f9702fb4..6fb9310158082 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.tsx +++ b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.tsx @@ -11,6 +11,7 @@ import { EmbeddableApiContext } from '@kbn/presentation-publishing'; import { apiIsPresentationContainer } from '@kbn/presentation-containers'; import { COMMON_VISUALIZATION_GROUPING } from '@kbn/visualizations-plugin/public'; import type { LensPluginStartDependencies } from '../../plugin'; +import type { EditorFrameService } from '../../editor_frame_service'; const ACTION_CREATE_ESQL_CHART = 'ACTION_CREATE_ESQL_CHART'; @@ -25,7 +26,8 @@ export class CreateESQLPanelAction implements Action { constructor( protected readonly startDependencies: LensPluginStartDependencies, - protected readonly core: CoreStart + protected readonly core: CoreStart, + protected readonly getEditorFrameService: () => Promise ) {} public getDisplayName(): string { @@ -41,18 +43,21 @@ export class CreateESQLPanelAction implements Action { public async isCompatible({ embeddable }: EmbeddableApiContext) { if (!apiIsPresentationContainer(embeddable)) return false; - // compatible only when ES|QL advanced setting is enabled const { isCreateActionCompatible } = await getAsyncHelpers(); + return isCreateActionCompatible(this.core); } public async execute({ embeddable }: EmbeddableApiContext) { if (!apiIsPresentationContainer(embeddable)) throw new IncompatibleActionError(); const { executeCreateAction } = await getAsyncHelpers(); + const editorFrameService = await this.getEditorFrameService(); + executeCreateAction({ deps: this.startDependencies, core: this.core, api: embeddable, + editorFrameService, }); } } diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts index 65ac6ef69aee8..8768bc721480d 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts +++ b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts @@ -21,6 +21,7 @@ import { suggestionsApi } from '../../lens_suggestions_api'; import { generateId } from '../../id_generator'; import { executeEditAction } from './edit_action_helpers'; import { Embeddable } from '../../embeddable'; +import type { EditorFrameService } from '../../editor_frame_service'; // datasourceMap and visualizationMap setters/getters export const [getVisualizationMap, setVisualizationMap] = createGetterSetter< @@ -31,7 +32,7 @@ export const [getDatasourceMap, setDatasourceMap] = createGetterSetter< Record> >('DatasourceMap', false); -export function isCreateActionCompatible(core: CoreStart) { +export async function isCreateActionCompatible(core: CoreStart) { return core.uiSettings.get(ENABLE_ESQL); } @@ -39,13 +40,13 @@ export async function executeCreateAction({ deps, core, api, + editorFrameService, }: { deps: LensPluginStartDependencies; core: CoreStart; api: PresentationContainer; + editorFrameService: EditorFrameService; }) { - const isCompatibleAction = isCreateActionCompatible(core); - const getFallbackDataView = async () => { const indexName = await getIndexForESQLQuery({ dataViews: deps.dataViews }); if (!indexName) return null; @@ -53,13 +54,33 @@ export async function executeCreateAction({ return dataView; }; - const dataView = await getFallbackDataView(); + const [isCompatibleAction, dataView] = await Promise.all([ + isCreateActionCompatible(core), + getFallbackDataView(), + ]); if (!isCompatibleAction || !dataView) { throw new IncompatibleActionError(); } - const visualizationMap = getVisualizationMap(); - const datasourceMap = getDatasourceMap(); + + let visualizationMap = getVisualizationMap(); + let datasourceMap = getDatasourceMap(); + + if (!visualizationMap || !datasourceMap) { + [visualizationMap, datasourceMap] = await Promise.all([ + editorFrameService.loadVisualizations(), + editorFrameService.loadDatasources(), + ]); + + if (!visualizationMap && !datasourceMap) { + throw new IncompatibleActionError(); + } + + // persist for retrieval elsewhere + setDatasourceMap(datasourceMap); + setVisualizationMap(visualizationMap); + } + const defaultIndex = dataView.getIndexPattern(); const defaultEsqlQuery = { diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/inference_models.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/inference_models.ts index 2895d1fed4336..736f4f275bb36 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/inference_models.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/inference_models.ts @@ -31,5 +31,18 @@ export function inferenceModelsApiProvider(httpService: HttpService) { }); return result; }, + /** + * Gets all inference endpoints + */ + async getAllInferenceEndpoints() { + const result = await httpService.http<{ + endpoints: estypes.InferenceModelConfigContainer[]; + }>({ + path: `${ML_INTERNAL_BASE_PATH}/_inference/all`, + method: 'GET', + version: '1', + }); + return result; + }, }; } diff --git a/x-pack/plugins/ml/server/routes/inference_models.ts b/x-pack/plugins/ml/server/routes/inference_models.ts index 29f687ede932d..cb12d87e2b6fc 100644 --- a/x-pack/plugins/ml/server/routes/inference_models.ts +++ b/x-pack/plugins/ml/server/routes/inference_models.ts @@ -7,6 +7,7 @@ import type { CloudSetup } from '@kbn/cloud-plugin/server'; import { schema } from '@kbn/config-schema'; import type { InferenceModelConfig, InferenceTaskType } from '@elastic/elasticsearch/lib/api/types'; +import type { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils'; import type { RouteInitialization } from '../types'; import { createInferenceSchema } from './schemas/inference_schema'; import { modelsProvider } from '../models/model_management'; @@ -63,4 +64,40 @@ export function inferenceModelRoutes( } ) ); + /** + * @apiGroup TrainedModels + * + * @api {put} /internal/ml/_inference/:taskType/:inferenceId Create Inference Endpoint + * @apiName CreateInferenceEndpoint + * @apiDescription Create Inference Endpoint + */ + router.versioned + .get({ + path: `${ML_INTERNAL_BASE_PATH}/_inference/all`, + access: 'internal', + options: { + tags: ['access:ml:canGetTrainedModels'], + }, + }) + .addVersion( + { + version: '1', + validate: {}, + }, + routeGuard.fullLicenseAPIGuard(async ({ client, response }) => { + try { + const body = await client.asCurrentUser.transport.request<{ + models: InferenceAPIConfigResponse[]; + }>({ + method: 'GET', + path: `/_inference/_all`, + }); + return response.ok({ + body, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); } diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/delete_ingest_pipeline.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/delete_ingest_pipeline.ts index c1516e342ac1d..f4c46d8447d8f 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/delete_ingest_pipeline.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/delete_ingest_pipeline.ts @@ -24,7 +24,7 @@ export async function deleteHistoryIngestPipeline( esClient.ingest.deletePipeline({ id: historyPipelineId }, { ignore: [404] }) ); } catch (e) { - logger.error(`Unable to delete history ingest pipeline [${definition.id}].`); + logger.error(`Unable to delete history ingest pipeline [${definition.id}]: ${e}`); throw e; } } @@ -40,7 +40,7 @@ export async function deleteLatestIngestPipeline( esClient.ingest.deletePipeline({ id: latestPipelineId }, { ignore: [404] }) ); } catch (e) { - logger.error(`Unable to delete latest ingest pipeline [${definition.id}].`); + logger.error(`Unable to delete latest ingest pipeline [${definition.id}]: ${e}`); throw e; } } diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/fixtures/entity_definition_with_backfill.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/fixtures/entity_definition_with_backfill.ts index 6d4026973ca38..66a79825fbfb0 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/fixtures/entity_definition_with_backfill.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/fixtures/entity_definition_with_backfill.ts @@ -7,7 +7,7 @@ import { entityDefinitionSchema } from '@kbn/entities-schema'; export const entityDefinitionWithBackfill = entityDefinitionSchema.parse({ - id: 'admin-console-services', + id: 'admin-console-services-backfill', version: '999.999.999', name: 'Services for Admin Console', type: 'service', diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/fixtures/index.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/fixtures/index.ts new file mode 100644 index 0000000000000..eae0e8e8afc9a --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/fixtures/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { entityDefinition } from './entity_definition'; +export { entityDefinitionWithBackfill } from './entity_definition_with_backfill'; diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/install_entity_definition.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/install_entity_definition.ts index 875242f73d751..a58019bf236ae 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/install_entity_definition.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/install_entity_definition.ts @@ -36,8 +36,8 @@ import { import { uninstallEntityDefinition } from './uninstall_entity_definition'; import { isBackfillEnabled } from './helpers/is_backfill_enabled'; import { deleteTemplate, upsertTemplate } from '../manage_index_templates'; -import { getEntitiesLatestIndexTemplateConfig } from '../../templates/entities_latest_template'; -import { getEntitiesHistoryIndexTemplateConfig } from '../../templates/entities_history_template'; +import { getEntitiesLatestIndexTemplateConfig } from './templates/entities_latest_template'; +import { getEntitiesHistoryIndexTemplateConfig } from './templates/entities_history_template'; export interface InstallDefinitionParams { esClient: ElasticsearchClient; @@ -111,7 +111,7 @@ export async function installEntityDefinition({ return entityDefinition; } catch (e) { - logger.error(`Failed to install entity definition ${definition.id}`, e); + logger.error(`Failed to install entity definition ${definition.id}: ${e}`); // Clean up anything that was successful. if (installState.definition) { await deleteEntityDefinition(soClient, definition, logger); diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/read_entity_definition.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/read_entity_definition.ts index 91194140101b7..37a0a48b92b5a 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/read_entity_definition.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/read_entity_definition.ts @@ -30,7 +30,7 @@ export async function readEntityDefinition( try { return entityDefinitionSchema.parse(response.saved_objects[0].attributes); } catch (e) { - logger.error(`Unable to parse entity definition with [${id}]`); + logger.error(`Unable to parse entity definition with [${id}]: ${e}`); throw e; } } diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/stop_and_delete_transform.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/stop_and_delete_transform.ts index d49165be22106..17ffeb44affc1 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/stop_and_delete_transform.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/stop_and_delete_transform.ts @@ -38,7 +38,7 @@ export async function stopAndDeleteHistoryTransform( { logger } ); } catch (e) { - logger.error(`Cannot stop or delete history transform [${definition.id}]`); + logger.error(`Cannot stop or delete history transform [${definition.id}]: ${e}`); throw e; } } @@ -67,7 +67,7 @@ export async function stopAndDeleteHistoryBackfillTransform( { logger } ); } catch (e) { - logger.error(`Cannot stop or delete history backfill transform [${definition.id}]`); + logger.error(`Cannot stop or delete history backfill transform [${definition.id}]: ${e}`); throw e; } } diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/__snapshots__/entities_history_template.test.ts.snap b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/__snapshots__/entities_history_template.test.ts.snap new file mode 100644 index 0000000000000..94af9f3307f04 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/__snapshots__/entities_history_template.test.ts.snap @@ -0,0 +1,78 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getEntitiesHistoryIndexTemplateConfig(definitionId) should generate a valid index template 1`] = ` +Object { + "_meta": Object { + "description": "Index template for indices managed by the Elastic Entity Model's entity discovery framework for the history dataset", + "ecs_version": "8.0.0", + "managed": true, + "managed_by": "elastic_entity_model", + }, + "composed_of": Array [ + "entities_v1_history_base", + "entities_v1_entity", + "entities_v1_event", + "admin-console-services@platform", + "admin-console-services-history@platform", + "admin-console-services@custom", + "admin-console-services-history@custom", + ], + "ignore_missing_component_templates": Array [ + "admin-console-services@platform", + "admin-console-services-history@platform", + "admin-console-services@custom", + "admin-console-services-history@custom", + ], + "index_patterns": Array [ + ".entities.v1.history.admin-console-services.*", + ], + "name": "entities_v1_history_admin-console-services_index_template", + "priority": 200, + "template": Object { + "mappings": Object { + "_meta": Object { + "version": "1.6.0", + }, + "date_detection": false, + "dynamic_templates": Array [ + Object { + "strings_as_keyword": Object { + "mapping": Object { + "fields": Object { + "text": Object { + "type": "text", + }, + }, + "ignore_above": 1024, + "type": "keyword", + }, + "match_mapping_type": "string", + }, + }, + Object { + "entity_metrics": Object { + "mapping": Object { + "type": "{dynamic_type}", + }, + "match_mapping_type": Array [ + "long", + "double", + ], + "path_match": "entity.metrics.*", + }, + }, + ], + }, + "settings": Object { + "index": Object { + "codec": "best_compression", + "mapping": Object { + "total_fields": Object { + "limit": 2000, + }, + }, + }, + }, + }, +} +`; diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/__snapshots__/entities_latest_template.test.ts.snap b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/__snapshots__/entities_latest_template.test.ts.snap new file mode 100644 index 0000000000000..b4247098d9498 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/__snapshots__/entities_latest_template.test.ts.snap @@ -0,0 +1,78 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getEntitiesLatestIndexTemplateConfig(definitionId) should generate a valid index template 1`] = ` +Object { + "_meta": Object { + "description": "Index template for indices managed by the Elastic Entity Model's entity discovery framework for the latest dataset", + "ecs_version": "8.0.0", + "managed": true, + "managed_by": "elastic_entity_model", + }, + "composed_of": Array [ + "entities_v1_latest_base", + "entities_v1_entity", + "entities_v1_event", + "admin-console-services@platform", + "admin-console-services-latest@platform", + "admin-console-services@custom", + "admin-console-services-latest@custom", + ], + "ignore_missing_component_templates": Array [ + "admin-console-services@platform", + "admin-console-services-latest@platform", + "admin-console-services@custom", + "admin-console-services-latest@custom", + ], + "index_patterns": Array [ + ".entities.v1.latest.admin-console-services", + ], + "name": "entities_v1_latest_admin-console-services_index_template", + "priority": 200, + "template": Object { + "mappings": Object { + "_meta": Object { + "version": "1.6.0", + }, + "date_detection": false, + "dynamic_templates": Array [ + Object { + "strings_as_keyword": Object { + "mapping": Object { + "fields": Object { + "text": Object { + "type": "text", + }, + }, + "ignore_above": 1024, + "type": "keyword", + }, + "match_mapping_type": "string", + }, + }, + Object { + "entity_metrics": Object { + "mapping": Object { + "type": "{dynamic_type}", + }, + "match_mapping_type": Array [ + "long", + "double", + ], + "path_match": "entity.metrics.*", + }, + }, + ], + }, + "settings": Object { + "index": Object { + "codec": "best_compression", + "mapping": Object { + "total_fields": Object { + "limit": 2000, + }, + }, + }, + }, + }, +} +`; diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_history_template.test.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_history_template.test.ts new file mode 100644 index 0000000000000..11aad78741020 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_history_template.test.ts @@ -0,0 +1,16 @@ +/* + * 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 { entityDefinition } from '../helpers/fixtures/entity_definition'; +import { getEntitiesHistoryIndexTemplateConfig } from './entities_history_template'; + +describe('getEntitiesHistoryIndexTemplateConfig(definitionId)', () => { + it('should generate a valid index template', () => { + const template = getEntitiesHistoryIndexTemplateConfig(entityDefinition.id); + expect(template).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/observability_solution/entity_manager/server/templates/entities_history_template.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_history_template.ts similarity index 87% rename from x-pack/plugins/observability_solution/entity_manager/server/templates/entities_history_template.ts rename to x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_history_template.ts index 63d589bfaa754..a0fb4b032a6e1 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/templates/entities_history_template.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_history_template.ts @@ -6,14 +6,14 @@ */ import { IndicesPutIndexTemplateRequest } from '@elastic/elasticsearch/lib/api/types'; -import { getEntityHistoryIndexTemplateV1 } from '../../common/helpers'; +import { getEntityHistoryIndexTemplateV1 } from '../../../../common/helpers'; import { ENTITY_ENTITY_COMPONENT_TEMPLATE_V1, ENTITY_EVENT_COMPONENT_TEMPLATE_V1, ENTITY_HISTORY_BASE_COMPONENT_TEMPLATE_V1, ENTITY_HISTORY_INDEX_PREFIX_V1, -} from '../../common/constants_entities'; -import { getCustomHistoryTemplateComponents } from './components/helpers'; +} from '../../../../common/constants_entities'; +import { getCustomHistoryTemplateComponents } from '../../../templates/components/helpers'; export const getEntitiesHistoryIndexTemplateConfig = ( definitionId: string @@ -33,7 +33,7 @@ export const getEntitiesHistoryIndexTemplateConfig = ( ENTITY_EVENT_COMPONENT_TEMPLATE_V1, ...getCustomHistoryTemplateComponents(definitionId), ], - index_patterns: [`${ENTITY_HISTORY_INDEX_PREFIX_V1}.*`], + index_patterns: [`${ENTITY_HISTORY_INDEX_PREFIX_V1}.${definitionId}.*`], priority: 200, template: { mappings: { diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_latest_template.test.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_latest_template.test.ts new file mode 100644 index 0000000000000..72583d941492c --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_latest_template.test.ts @@ -0,0 +1,16 @@ +/* + * 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 { entityDefinition } from '../helpers/fixtures/entity_definition'; +import { getEntitiesLatestIndexTemplateConfig } from './entities_latest_template'; + +describe('getEntitiesLatestIndexTemplateConfig(definitionId)', () => { + it('should generate a valid index template', () => { + const template = getEntitiesLatestIndexTemplateConfig(entityDefinition.id); + expect(template).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/observability_solution/entity_manager/server/templates/entities_latest_template.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_latest_template.ts similarity index 87% rename from x-pack/plugins/observability_solution/entity_manager/server/templates/entities_latest_template.ts rename to x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_latest_template.ts index 3ad09e7257a1a..466346f86b44d 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/templates/entities_latest_template.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_latest_template.ts @@ -6,14 +6,14 @@ */ import { IndicesPutIndexTemplateRequest } from '@elastic/elasticsearch/lib/api/types'; -import { getEntityLatestIndexTemplateV1 } from '../../common/helpers'; +import { getEntityLatestIndexTemplateV1 } from '../../../../common/helpers'; import { ENTITY_ENTITY_COMPONENT_TEMPLATE_V1, ENTITY_EVENT_COMPONENT_TEMPLATE_V1, ENTITY_LATEST_BASE_COMPONENT_TEMPLATE_V1, ENTITY_LATEST_INDEX_PREFIX_V1, -} from '../../common/constants_entities'; -import { getCustomLatestTemplateComponents } from './components/helpers'; +} from '../../../../common/constants_entities'; +import { getCustomLatestTemplateComponents } from '../../../templates/components/helpers'; export const getEntitiesLatestIndexTemplateConfig = ( definitionId: string @@ -33,8 +33,8 @@ export const getEntitiesLatestIndexTemplateConfig = ( ENTITY_EVENT_COMPONENT_TEMPLATE_V1, ...getCustomLatestTemplateComponents(definitionId), ], - index_patterns: [`${ENTITY_LATEST_INDEX_PREFIX_V1}.*`], - priority: 1, + index_patterns: [`${ENTITY_LATEST_INDEX_PREFIX_V1}.${definitionId}`], + priority: 200, template: { mappings: { _meta: { diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap index 4ecdd0c3ab024..551b9761341d2 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap @@ -9,7 +9,7 @@ Object { "defer_validation": true, "dest": Object { "index": ".entities.v1.history.noop", - "pipeline": "entities-v1-history-admin-console-services", + "pipeline": "entities-v1-history-admin-console-services-backfill", }, "frequency": "5m", "pivot": Object { @@ -143,7 +143,7 @@ Object { "field": "@timestamp", }, }, - "transform_id": "entities-v1-history-backfill-admin-console-services", + "transform_id": "entities-v1-history-backfill-admin-console-services-backfill", } `; diff --git a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml new file mode 100644 index 0000000000000..22979d62e0933 --- /dev/null +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml @@ -0,0 +1,938 @@ +openapi: 3.0.3 +info: + description: Interact with and manage endpoints running the Elastic Defend integration. + title: Security Solution Endpoint Management API (Elastic Cloud and self-hosted) + version: '2023-10-31' +servers: + - url: 'http://{kibana_host}:{port}' + variables: + kibana_host: + default: localhost + port: + default: '5601' +paths: + /api/endpoint/action: + get: + operationId: EndpointGetActionsList + parameters: + - in: query + name: query + required: true + schema: + $ref: '#/components/schemas/EndpointActionListRequestQuery' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Actions List schema + '/api/endpoint/action_log/{agent_id}': + get: + operationId: EndpointGetActionAuditLog + parameters: + - in: query + name: query + required: true + schema: + $ref: '#/components/schemas/AuditLogRequestQuery' + - in: path + name: query + required: true + schema: + $ref: '#/components/schemas/AuditLogRequestParams' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get action audit log schema + /api/endpoint/action_status: + get: + operationId: EndpointGetActionsStatus + parameters: + - in: query + name: query + required: true + schema: + type: object + properties: + agent_ids: + $ref: '#/components/schemas/AgentIds' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Actions status schema + '/api/endpoint/action/{action_id}': + get: + operationId: EndpointGetActionsDetails + parameters: + - in: path + name: query + required: true + schema: + $ref: '#/components/schemas/DetailsRequestParams' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Action details schema + '/api/endpoint/action/{action_id}/file/{file_id}/download`': + get: + operationId: EndpointFileDownload + parameters: + - in: path + name: query + required: true + schema: + $ref: '#/components/schemas/FileDownloadRequestParams' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: File Download schema + '/api/endpoint/action/{action_id}/file/{file_id}`': + get: + operationId: EndpointFileInfo + parameters: + - in: path + name: query + required: true + schema: + $ref: '#/components/schemas/FileInfoRequestParams' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: File Info schema + /api/endpoint/action/execute: + post: + operationId: EndpointExecuteAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ExecuteActionRequestBody' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Execute Action + /api/endpoint/action/get_file: + post: + operationId: EndpointGetFileAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/GetFileActionRequestBody' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get File Action + /api/endpoint/action/isolate: + post: + operationId: EndpointIsolateHostAction + requestBody: + content: + application/json: + schema: + type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Isolate host Action + /api/endpoint/action/kill_process: + post: + operationId: EndpointKillProcessAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProcessActionSchemas' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Kill process Action + /api/endpoint/action/running_procs: + post: + operationId: EndpointGetRunningProcessesAction + requestBody: + content: + application/json: + schema: + type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Running Processes Action + /api/endpoint/action/scan: + post: + operationId: EndpointScanAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ScanActionRequestBody' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Scan Action + /api/endpoint/action/state: + get: + operationId: EndpointGetActionsState + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Action State schema + /api/endpoint/action/suspend_process: + post: + operationId: EndpointSuspendProcessAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProcessActionSchemas' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Suspend process Action + /api/endpoint/action/unisolate: + post: + operationId: EndpointUnisolateHostAction + requestBody: + content: + application/json: + schema: + type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Unisolate host Action + /api/endpoint/action/upload: + post: + operationId: EndpointUploadAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/FileUploadActionRequestBody' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Upload Action + /api/endpoint/isolate: + post: + operationId: EndpointIsolateRedirect + requestBody: + content: + application/json: + schema: + type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + '308': + description: Permanent Redirect + headers: + Location: + description: Permanently redirects to "/api/endpoint/action/isolate" + schema: + example: /api/endpoint/action/isolate + type: string + summary: Permanently redirects to a new location + /api/endpoint/metadata: + get: + operationId: GetEndpointMetadataList + parameters: + - in: query + name: query + required: true + schema: + $ref: '#/components/schemas/ListRequestQuery' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Metadata List schema + '/api/endpoint/metadata/{id}': + get: + operationId: GetEndpointMetadata + parameters: + - in: path + name: query + required: true + schema: + type: object + properties: + id: + type: string + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Metadata schema + /api/endpoint/metadata/transforms: + get: + operationId: GetEndpointMetadataTransform + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Metadata Transform schema + /api/endpoint/policy_response: + get: + operationId: GetPolicyResponse + parameters: + - in: query + name: query + required: true + schema: + type: object + properties: + agentId: + $ref: '#/components/schemas/AgentId' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Policy Response schema + /api/endpoint/policy/summaries: + get: + operationId: GetAgentPolicySummary + parameters: + - in: query + name: query + required: true + schema: + type: object + properties: + package_name: + type: string + policy_id: + nullable: true + type: string + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Agent Policy Summary schema + '/api/endpoint/protection_updates_note/{package_policy_id}': + get: + operationId: GetProtectionUpdatesNote + parameters: + - in: path + name: package_policy_id + required: true + schema: + type: string + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ProtectionUpdatesNoteResponse' + description: OK + summary: Get Protection Updates Note schema + post: + operationId: CreateUpdateProtectionUpdatesNote + parameters: + - in: path + name: package_policy_id + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + properties: + note: + type: string + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ProtectionUpdatesNoteResponse' + description: OK + summary: Create Update Protection Updates Note schema + '/api/endpoint/suggestions/{suggestion_type}': + post: + operationId: GetEndpointSuggestions + parameters: + - in: path + name: suggestion_type + required: true + schema: + enum: + - eventFilters + type: string + requestBody: + content: + application/json: + schema: + type: object + properties: + field: + type: string + fieldMeta: {} + filters: {} + query: + type: string + required: + - parameters + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get suggestions + /api/endpoint/unisolate: + post: + operationId: EndpointUnisolateRedirect + requestBody: + content: + application/json: + schema: + type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + '308': + description: Permanent Redirect + headers: + Location: + description: Permanently redirects to "/api/endpoint/action/unisolate" + schema: + example: /api/endpoint/action/unisolate + type: string + summary: Permanently redirects to a new location +components: + schemas: + AgentId: + description: Agent ID + type: string + AgentIds: + minLength: 1 + oneOf: + - items: + minLength: 1 + type: string + maxItems: 50 + minItems: 1 + type: array + - minLength: 1 + type: string + AlertIds: + description: A list of alerts ids. + items: + $ref: '#/components/schemas/NonEmptyString' + minItems: 1 + type: array + AuditLogRequestParams: + type: object + properties: + agent_id: + $ref: '#/components/schemas/AgentId' + AuditLogRequestQuery: + type: object + properties: + end_date: + $ref: '#/components/schemas/EndDate' + page: + $ref: '#/components/schemas/Page' + page_size: + $ref: '#/components/schemas/PageSize' + start_date: + $ref: '#/components/schemas/StartDate' + CaseIds: + description: Case IDs to be updated (cannot contain empty strings) + items: + minLength: 1 + type: string + minItems: 1 + type: array + Command: + description: The command to be executed (cannot be an empty string) + enum: + - isolate + - unisolate + - kill-process + - suspend-process + - running-processes + - get-file + - execute + - upload + minLength: 1 + type: string + Commands: + items: + $ref: '#/components/schemas/Command' + type: array + Comment: + description: Optional comment + type: string + DetailsRequestParams: + type: object + properties: + action_id: + type: string + EndDate: + description: End date + type: string + EndpointActionListRequestQuery: + type: object + properties: + agentIds: + $ref: '#/components/schemas/AgentIds' + commands: + $ref: '#/components/schemas/Commands' + endDate: + $ref: '#/components/schemas/EndDate' + page: + $ref: '#/components/schemas/Page' + pageSize: + default: 10 + description: Number of items per page + maximum: 10000 + minimum: 1 + type: integer + startDate: + $ref: '#/components/schemas/StartDate' + types: + $ref: '#/components/schemas/Types' + userIds: + $ref: '#/components/schemas/UserIds' + withOutputs: + $ref: '#/components/schemas/WithOutputs' + EndpointIds: + description: List of endpoint IDs (cannot contain empty strings) + items: + minLength: 1 + type: string + minItems: 1 + type: array + ExecuteActionRequestBody: + allOf: + - type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + - type: object + properties: + parameters: + type: object + properties: + command: + $ref: '#/components/schemas/Command' + timeout: + $ref: '#/components/schemas/Timeout' + required: + - command + required: + - parameters + FileDownloadRequestParams: + type: object + properties: + action_id: + type: string + file_id: + type: string + required: + - action_id + - file_id + FileInfoRequestParams: + type: object + properties: + action_id: + type: string + file_id: + type: string + required: + - action_id + - file_id + FileUploadActionRequestBody: + allOf: + - type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + - type: object + properties: + file: + format: binary + type: string + parameters: + type: object + properties: + overwrite: + default: false + type: boolean + required: + - parameters + - file + GetFileActionRequestBody: + allOf: + - type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + - type: object + properties: + parameters: + type: object + properties: + path: + type: string + required: + - path + required: + - parameters + ListRequestQuery: + type: object + properties: + hostStatuses: + items: + enum: + - healthy + - offline + - updating + - inactive + - unenrolled + type: string + type: array + kuery: + nullable: true + type: string + page: + default: 0 + description: Page number + minimum: 0 + type: integer + pageSize: + default: 10 + description: Number of items per page + maximum: 10000 + minimum: 1 + type: integer + sortDirection: + enum: + - asc + - desc + nullable: true + type: string + sortField: + enum: + - enrolled_at + - metadata.host.hostname + - host_status + - metadata.Endpoint.policy.applied.name + - metadata.Endpoint.policy.applied.status + - metadata.host.os.name + - metadata.host.ip + - metadata.agent.version + - last_checkin + type: string + required: + - hostStatuses + NonEmptyString: + description: A string that is not empty and does not contain only whitespace + minLength: 1 + pattern: ^(?! *$).+$ + type: string + Page: + default: 1 + description: Page number + minimum: 1 + type: integer + PageSize: + default: 10 + description: Number of items per page + maximum: 100 + minimum: 1 + type: integer + Parameters: + description: Optional parameters object + type: object + ProcessActionSchemas: + allOf: + - type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + - type: object + properties: + parameters: + oneOf: + - type: object + properties: + pid: + minimum: 1 + type: integer + - type: object + properties: + entity_id: + minLength: 1 + type: string + required: + - parameters + ProtectionUpdatesNoteResponse: + type: object + properties: + note: + type: string + ScanActionRequestBody: + allOf: + - type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + - type: object + properties: + parameters: + type: object + properties: + path: + type: string + required: + - path + required: + - parameters + StartDate: + description: Start date + type: string + SuccessResponse: + type: object + properties: {} + Timeout: + description: The maximum timeout value in milliseconds (optional) + minimum: 1 + type: integer + Type: + enum: + - automated + - manual + type: string + Types: + items: + $ref: '#/components/schemas/Type' + maxLength: 2 + minLength: 1 + type: array + UserIds: + description: User IDs + oneOf: + - items: + minLength: 1 + type: string + minItems: 1 + type: array + - minLength: 1 + type: string + WithOutputs: + description: With Outputs + oneOf: + - items: + minLength: 1 + type: string + minItems: 1 + type: array + - minLength: 1 + type: string + securitySchemes: + BasicAuth: + scheme: basic + type: http +security: + - BasicAuth: [] +tags: ! '' diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml new file mode 100644 index 0000000000000..99627f8bd8a9e --- /dev/null +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml @@ -0,0 +1,866 @@ +openapi: 3.0.3 +info: + description: Interact with and manage endpoints running the Elastic Defend integration. + title: Security Solution Endpoint Management API (Elastic Cloud Serverless) + version: '2023-10-31' +servers: + - url: 'http://{kibana_host}:{port}' + variables: + kibana_host: + default: localhost + port: + default: '5601' +paths: + /api/endpoint/action: + get: + operationId: EndpointGetActionsList + parameters: + - in: query + name: query + required: true + schema: + $ref: '#/components/schemas/EndpointActionListRequestQuery' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Actions List schema + '/api/endpoint/action_log/{agent_id}': + get: + operationId: EndpointGetActionAuditLog + parameters: + - in: query + name: query + required: true + schema: + $ref: '#/components/schemas/AuditLogRequestQuery' + - in: path + name: query + required: true + schema: + $ref: '#/components/schemas/AuditLogRequestParams' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get action audit log schema + /api/endpoint/action_status: + get: + operationId: EndpointGetActionsStatus + parameters: + - in: query + name: query + required: true + schema: + type: object + properties: + agent_ids: + $ref: '#/components/schemas/AgentIds' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Actions status schema + '/api/endpoint/action/{action_id}': + get: + operationId: EndpointGetActionsDetails + parameters: + - in: path + name: query + required: true + schema: + $ref: '#/components/schemas/DetailsRequestParams' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Action details schema + '/api/endpoint/action/{action_id}/file/{file_id}/download`': + get: + operationId: EndpointFileDownload + parameters: + - in: path + name: query + required: true + schema: + $ref: '#/components/schemas/FileDownloadRequestParams' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: File Download schema + '/api/endpoint/action/{action_id}/file/{file_id}`': + get: + operationId: EndpointFileInfo + parameters: + - in: path + name: query + required: true + schema: + $ref: '#/components/schemas/FileInfoRequestParams' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: File Info schema + /api/endpoint/action/execute: + post: + operationId: EndpointExecuteAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ExecuteActionRequestBody' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Execute Action + /api/endpoint/action/get_file: + post: + operationId: EndpointGetFileAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/GetFileActionRequestBody' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get File Action + /api/endpoint/action/isolate: + post: + operationId: EndpointIsolateHostAction + requestBody: + content: + application/json: + schema: + type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Isolate host Action + /api/endpoint/action/kill_process: + post: + operationId: EndpointKillProcessAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProcessActionSchemas' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Kill process Action + /api/endpoint/action/running_procs: + post: + operationId: EndpointGetRunningProcessesAction + requestBody: + content: + application/json: + schema: + type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Running Processes Action + /api/endpoint/action/scan: + post: + operationId: EndpointScanAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ScanActionRequestBody' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Scan Action + /api/endpoint/action/state: + get: + operationId: EndpointGetActionsState + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Action State schema + /api/endpoint/action/suspend_process: + post: + operationId: EndpointSuspendProcessAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProcessActionSchemas' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Suspend process Action + /api/endpoint/action/unisolate: + post: + operationId: EndpointUnisolateHostAction + requestBody: + content: + application/json: + schema: + type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Unisolate host Action + /api/endpoint/action/upload: + post: + operationId: EndpointUploadAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/FileUploadActionRequestBody' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Upload Action + /api/endpoint/metadata: + get: + operationId: GetEndpointMetadataList + parameters: + - in: query + name: query + required: true + schema: + $ref: '#/components/schemas/ListRequestQuery' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Metadata List schema + '/api/endpoint/metadata/{id}': + get: + operationId: GetEndpointMetadata + parameters: + - in: path + name: query + required: true + schema: + type: object + properties: + id: + type: string + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Metadata schema + /api/endpoint/metadata/transforms: + get: + operationId: GetEndpointMetadataTransform + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Metadata Transform schema + /api/endpoint/policy_response: + get: + operationId: GetPolicyResponse + parameters: + - in: query + name: query + required: true + schema: + type: object + properties: + agentId: + $ref: '#/components/schemas/AgentId' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Policy Response schema + /api/endpoint/policy/summaries: + get: + operationId: GetAgentPolicySummary + parameters: + - in: query + name: query + required: true + schema: + type: object + properties: + package_name: + type: string + policy_id: + nullable: true + type: string + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Agent Policy Summary schema + '/api/endpoint/protection_updates_note/{package_policy_id}': + get: + operationId: GetProtectionUpdatesNote + parameters: + - in: path + name: package_policy_id + required: true + schema: + type: string + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ProtectionUpdatesNoteResponse' + description: OK + summary: Get Protection Updates Note schema + post: + operationId: CreateUpdateProtectionUpdatesNote + parameters: + - in: path + name: package_policy_id + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + properties: + note: + type: string + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ProtectionUpdatesNoteResponse' + description: OK + summary: Create Update Protection Updates Note schema + '/api/endpoint/suggestions/{suggestion_type}': + post: + operationId: GetEndpointSuggestions + parameters: + - in: path + name: suggestion_type + required: true + schema: + enum: + - eventFilters + type: string + requestBody: + content: + application/json: + schema: + type: object + properties: + field: + type: string + fieldMeta: {} + filters: {} + query: + type: string + required: + - parameters + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get suggestions +components: + schemas: + AgentId: + description: Agent ID + type: string + AgentIds: + minLength: 1 + oneOf: + - items: + minLength: 1 + type: string + maxItems: 50 + minItems: 1 + type: array + - minLength: 1 + type: string + AlertIds: + description: A list of alerts ids. + items: + $ref: '#/components/schemas/NonEmptyString' + minItems: 1 + type: array + AuditLogRequestParams: + type: object + properties: + agent_id: + $ref: '#/components/schemas/AgentId' + AuditLogRequestQuery: + type: object + properties: + end_date: + $ref: '#/components/schemas/EndDate' + page: + $ref: '#/components/schemas/Page' + page_size: + $ref: '#/components/schemas/PageSize' + start_date: + $ref: '#/components/schemas/StartDate' + CaseIds: + description: Case IDs to be updated (cannot contain empty strings) + items: + minLength: 1 + type: string + minItems: 1 + type: array + Command: + description: The command to be executed (cannot be an empty string) + enum: + - isolate + - unisolate + - kill-process + - suspend-process + - running-processes + - get-file + - execute + - upload + minLength: 1 + type: string + Commands: + items: + $ref: '#/components/schemas/Command' + type: array + Comment: + description: Optional comment + type: string + DetailsRequestParams: + type: object + properties: + action_id: + type: string + EndDate: + description: End date + type: string + EndpointActionListRequestQuery: + type: object + properties: + agentIds: + $ref: '#/components/schemas/AgentIds' + commands: + $ref: '#/components/schemas/Commands' + endDate: + $ref: '#/components/schemas/EndDate' + page: + $ref: '#/components/schemas/Page' + pageSize: + default: 10 + description: Number of items per page + maximum: 10000 + minimum: 1 + type: integer + startDate: + $ref: '#/components/schemas/StartDate' + types: + $ref: '#/components/schemas/Types' + userIds: + $ref: '#/components/schemas/UserIds' + withOutputs: + $ref: '#/components/schemas/WithOutputs' + EndpointIds: + description: List of endpoint IDs (cannot contain empty strings) + items: + minLength: 1 + type: string + minItems: 1 + type: array + ExecuteActionRequestBody: + allOf: + - type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + - type: object + properties: + parameters: + type: object + properties: + command: + $ref: '#/components/schemas/Command' + timeout: + $ref: '#/components/schemas/Timeout' + required: + - command + required: + - parameters + FileDownloadRequestParams: + type: object + properties: + action_id: + type: string + file_id: + type: string + required: + - action_id + - file_id + FileInfoRequestParams: + type: object + properties: + action_id: + type: string + file_id: + type: string + required: + - action_id + - file_id + FileUploadActionRequestBody: + allOf: + - type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + - type: object + properties: + file: + format: binary + type: string + parameters: + type: object + properties: + overwrite: + default: false + type: boolean + required: + - parameters + - file + GetFileActionRequestBody: + allOf: + - type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + - type: object + properties: + parameters: + type: object + properties: + path: + type: string + required: + - path + required: + - parameters + ListRequestQuery: + type: object + properties: + hostStatuses: + items: + enum: + - healthy + - offline + - updating + - inactive + - unenrolled + type: string + type: array + kuery: + nullable: true + type: string + page: + default: 0 + description: Page number + minimum: 0 + type: integer + pageSize: + default: 10 + description: Number of items per page + maximum: 10000 + minimum: 1 + type: integer + sortDirection: + enum: + - asc + - desc + nullable: true + type: string + sortField: + enum: + - enrolled_at + - metadata.host.hostname + - host_status + - metadata.Endpoint.policy.applied.name + - metadata.Endpoint.policy.applied.status + - metadata.host.os.name + - metadata.host.ip + - metadata.agent.version + - last_checkin + type: string + required: + - hostStatuses + NonEmptyString: + description: A string that is not empty and does not contain only whitespace + minLength: 1 + pattern: ^(?! *$).+$ + type: string + Page: + default: 1 + description: Page number + minimum: 1 + type: integer + PageSize: + default: 10 + description: Number of items per page + maximum: 100 + minimum: 1 + type: integer + Parameters: + description: Optional parameters object + type: object + ProcessActionSchemas: + allOf: + - type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + - type: object + properties: + parameters: + oneOf: + - type: object + properties: + pid: + minimum: 1 + type: integer + - type: object + properties: + entity_id: + minLength: 1 + type: string + required: + - parameters + ProtectionUpdatesNoteResponse: + type: object + properties: + note: + type: string + ScanActionRequestBody: + allOf: + - type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + - type: object + properties: + parameters: + type: object + properties: + path: + type: string + required: + - path + required: + - parameters + StartDate: + description: Start date + type: string + SuccessResponse: + type: object + properties: {} + Timeout: + description: The maximum timeout value in milliseconds (optional) + minimum: 1 + type: integer + Type: + enum: + - automated + - manual + type: string + Types: + items: + $ref: '#/components/schemas/Type' + maxLength: 2 + minLength: 1 + type: array + UserIds: + description: User IDs + oneOf: + - items: + minLength: 1 + type: string + minItems: 1 + type: array + - minLength: 1 + type: string + WithOutputs: + description: With Outputs + oneOf: + - items: + minLength: 1 + type: string + minItems: 1 + type: array + - minLength: 1 + type: string + securitySchemes: + BasicAuth: + scheme: basic + type: http +security: + - BasicAuth: [] +tags: ! '' diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index cd7eb82b11a92..98575c1f48c26 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -32,6 +32,7 @@ "openapi:generate:debug": "node --inspect-brk scripts/openapi/generate", "openapi:bundle:detections": "node scripts/openapi/bundle_detections", "openapi:bundle:timeline": "node scripts/openapi/bundle_timeline", - "openapi:bundle:entity-analytics": "node scripts/openapi/bundle_entity_analytics" + "openapi:bundle:entity-analytics": "node scripts/openapi/bundle_entity_analytics", + "openapi:bundle:endpoint-management": "node scripts/openapi/bundle_endpoint_management" } } diff --git a/x-pack/plugins/security_solution/scripts/openapi/bundle_endpoint_management.js b/x-pack/plugins/security_solution/scripts/openapi/bundle_endpoint_management.js new file mode 100644 index 0000000000000..d4d994b993057 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/openapi/bundle_endpoint_management.js @@ -0,0 +1,44 @@ +/* + * 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. + */ + +require('../../../../../src/setup_node_env'); +const { bundle } = require('@kbn/openapi-bundler'); +const { join, resolve } = require('path'); + +const ROOT = resolve(__dirname, '../..'); + +(async () => { + await bundle({ + sourceGlob: join(ROOT, 'common/api/endpoint/**/*.schema.yaml'), + outputFilePath: join( + ROOT, + 'docs/openapi/serverless/security_solution_endpoint_management_api_{version}.bundled.schema.yaml' + ), + options: { + includeLabels: ['serverless'], + specInfo: { + title: 'Security Solution Endpoint Management API (Elastic Cloud Serverless)', + description: 'Interact with and manage endpoints running the Elastic Defend integration.', + }, + }, + }); + + await bundle({ + sourceGlob: join(ROOT, 'common/api/endpoint/**/*.schema.yaml'), + outputFilePath: join( + ROOT, + 'docs/openapi/ess/security_solution_endpoint_management_api_{version}.bundled.schema.yaml' + ), + options: { + includeLabels: ['ess'], + specInfo: { + title: 'Security Solution Endpoint Management API (Elastic Cloud and self-hosted)', + description: 'Interact with and manage endpoints running the Elastic Defend integration.', + }, + }, + }); +})(); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/async_sender.ts b/x-pack/plugins/security_solution/server/lib/telemetry/async_sender.ts index 6c2def2abb61d..edbd5d21e3aea 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/async_sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/async_sender.ts @@ -276,28 +276,15 @@ export class AsyncTelemetryEventsSender implements IAsyncTelemetryEventsSender { private enrich(event: Event): Event { const clusterInfo = this.telemetryReceiver?.getClusterInfo(); - // TODO(szaffarano): generalize the enrichment at channel level to not hardcode the logic here if (typeof event.payload === 'object') { let additional = {}; - if (event.channel !== TelemetryChannel.TASK_METRICS) { - additional = { - cluster_name: clusterInfo?.cluster_name, - cluster_uuid: clusterInfo?.cluster_uuid, - }; - } else { - additional = { - cluster_uuid: clusterInfo?.cluster_uuid, - }; - } - - if (event.channel === TelemetryChannel.ENDPOINT_ALERTS) { - const licenseInfo = this.telemetryReceiver?.getLicenseInfo(); - additional = { - ...additional, - ...(licenseInfo ? { license: copyLicenseFields(licenseInfo) } : {}), - }; - } + const licenseInfo = this.telemetryReceiver?.getLicenseInfo(); + additional = { + cluster_name: clusterInfo?.cluster_name, + cluster_uuid: clusterInfo?.cluster_uuid, + ...(licenseInfo ? { license: copyLicenseFields(licenseInfo) } : {}), + }; event.payload = { ...event.payload, diff --git a/x-pack/plugins/task_manager/server/integration_tests/__snapshots__/task_cost_check.test.ts.snap b/x-pack/plugins/task_manager/server/integration_tests/__snapshots__/task_cost_check.test.ts.snap new file mode 100644 index 0000000000000..e59912ed91905 --- /dev/null +++ b/x-pack/plugins/task_manager/server/integration_tests/__snapshots__/task_cost_check.test.ts.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Task cost checks detects tasks with cost definitions 1`] = ` +Array [ + Object { + "cost": 10, + "taskType": "alerting:siem.indicatorRule", + }, +] +`; diff --git a/x-pack/plugins/task_manager/server/integration_tests/task_cost_check.test.ts b/x-pack/plugins/task_manager/server/integration_tests/task_cost_check.test.ts new file mode 100644 index 0000000000000..96678f714ac69 --- /dev/null +++ b/x-pack/plugins/task_manager/server/integration_tests/task_cost_check.test.ts @@ -0,0 +1,63 @@ +/* + * 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 { + type TestElasticsearchUtils, + type TestKibanaUtils, +} from '@kbn/core-test-helpers-kbn-server'; +import { TaskCost, TaskDefinition } from '../task'; +import { setupTestServers } from './lib'; +import { TaskTypeDictionary } from '../task_type_dictionary'; + +jest.mock('../task_type_dictionary', () => { + const actual = jest.requireActual('../task_type_dictionary'); + return { + ...actual, + TaskTypeDictionary: jest.fn().mockImplementation((opts) => { + return new actual.TaskTypeDictionary(opts); + }), + }; +}); + +// Notify response-ops if a task sets a cost to something other than `Normal` +describe('Task cost checks', () => { + let esServer: TestElasticsearchUtils; + let kibanaServer: TestKibanaUtils; + let taskTypeDictionary: TaskTypeDictionary; + + beforeAll(async () => { + const setupResult = await setupTestServers(); + esServer = setupResult.esServer; + kibanaServer = setupResult.kibanaServer; + + const mockedTaskTypeDictionary = jest.requireMock('../task_type_dictionary'); + expect(mockedTaskTypeDictionary.TaskTypeDictionary).toHaveBeenCalledTimes(1); + taskTypeDictionary = mockedTaskTypeDictionary.TaskTypeDictionary.mock.results[0].value; + }); + + afterAll(async () => { + if (kibanaServer) { + await kibanaServer.stop(); + } + if (esServer) { + await esServer.stop(); + } + }); + + it('detects tasks with cost definitions', async () => { + const taskTypes = taskTypeDictionary.getAllDefinitions(); + const taskTypesWithCost = taskTypes + .map((taskType: TaskDefinition) => + !!taskType.cost ? { taskType: taskType.type, cost: taskType.cost } : null + ) + .filter( + (tt: { taskType: string; cost: TaskCost } | null) => + null != tt && tt.cost !== TaskCost.Normal + ); + expect(taskTypesWithCost).toMatchSnapshot(); + }); +}); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/monitoring_collection.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/monitoring_collection.ts index f87c1c095dd8f..902362f9fc31c 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/monitoring_collection.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/monitoring_collection.ts @@ -38,8 +38,7 @@ export default function alertingMonitoringCollectionTests({ getService }: FtrPro ? dedicatedTaskRunner.getSupertest() : supertest; - // Failing: See https://github.com/elastic/kibana/issues/187275 - describe.skip('monitoring_collection', () => { + describe('monitoring_collection', () => { let endDate: string; const objectRemover = new ObjectRemover(supertest); @@ -124,8 +123,8 @@ export default function alertingMonitoringCollectionTests({ getService }: FtrPro rule_type_id: 'test.cancellableRule', schedule: { interval: '4s' }, params: { - doLongSearch: true, - doLongPostProcessing: false, + doLongSearch: false, + doLongPostProcessing: true, }, }) ); diff --git a/x-pack/test/api_integration/apis/entity_manager/definitions.ts b/x-pack/test/api_integration/apis/entity_manager/definitions.ts new file mode 100644 index 0000000000000..89b6bae918fbb --- /dev/null +++ b/x-pack/test/api_integration/apis/entity_manager/definitions.ts @@ -0,0 +1,42 @@ +/* + * 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 expect from '@kbn/expect'; +import { EntityDefinition } from '@kbn/entities-schema'; +import { + entityDefinition as mockDefinition, + entityDefinitionWithBackfill as mockBackfillDefinition, +} from '@kbn/entityManager-plugin/server/lib/entities/helpers/fixtures'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { installDefinition, uninstallDefinition, getInstalledDefinitions } from './helpers/request'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('Entity definitions', () => { + describe('definitions installations', () => { + it('can install multiple definitions', async () => { + await installDefinition(supertest, mockDefinition); + await installDefinition(supertest, mockBackfillDefinition); + + const { definitions } = await getInstalledDefinitions(supertest); + expect(definitions.length).to.eql(2); + expect( + definitions.find((definition: EntityDefinition) => definition.id === mockDefinition.id) + ); + expect( + definitions.find( + (definition: EntityDefinition) => definition.id === mockBackfillDefinition.id + ) + ); + + await uninstallDefinition(supertest, mockDefinition.id); + await uninstallDefinition(supertest, mockBackfillDefinition.id); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/entity_manager/enablement.ts b/x-pack/test/api_integration/apis/entity_manager/enablement.ts index 8ec5f6743a51f..a84a293e36caf 100644 --- a/x-pack/test/api_integration/apis/entity_manager/enablement.ts +++ b/x-pack/test/api_integration/apis/entity_manager/enablement.ts @@ -11,11 +11,7 @@ import { builtInDefinitions } from '@kbn/entityManager-plugin/server/lib/entitie import { EntityDefinitionWithState } from '@kbn/entityManager-plugin/server/lib/entities/types'; import { FtrProviderContext } from '../../ftr_provider_context'; import { createAdmin, createRuntimeUser } from './helpers/user'; - -interface Auth { - username: string; - password: string; -} +import { Auth, getInstalledDefinitions } from './helpers/request'; export default function ({ getService }: FtrProviderContext) { const esClient = getService('es'); @@ -32,16 +28,6 @@ export default function ({ getService }: FtrProviderContext) { return response.body; }; - const getInstalledDefinitions = async (auth: Auth) => { - const response = await supertest - .get('/internal/entities/definition') - .auth(auth.username, auth.password) - .set('kbn-xsrf', 'xxx') - .send() - .expect(200); - return response.body; - }; - const entityDiscoveryState = enablementRequest('get'); const enableEntityDiscovery = enablementRequest('put'); const disableEntityDiscovery = enablementRequest('delete'); @@ -62,7 +48,7 @@ export default function ({ getService }: FtrProviderContext) { const enableResponse = await enableEntityDiscovery(authorizedUser); expect(enableResponse.success).to.eql(true, "authorized user can't enable EEM"); - let definitionsResponse = await getInstalledDefinitions(authorizedUser); + let definitionsResponse = await getInstalledDefinitions(supertest, authorizedUser); expect(definitionsResponse.definitions.length).to.eql(builtInDefinitions.length); expect( builtInDefinitions.every((builtin) => { @@ -93,7 +79,7 @@ export default function ({ getService }: FtrProviderContext) { stateResponse = await entityDiscoveryState(authorizedUser); expect(stateResponse.enabled).to.eql(false, 'EEM is not disabled'); - definitionsResponse = await getInstalledDefinitions(authorizedUser); + definitionsResponse = await getInstalledDefinitions(supertest, authorizedUser); expect(definitionsResponse.definitions).to.eql([]); }); }); @@ -107,7 +93,7 @@ export default function ({ getService }: FtrProviderContext) { const stateResponse = await entityDiscoveryState(unauthorizedUser); expect(stateResponse.enabled).to.eql(false, 'EEM is enabled'); - const definitionsResponse = await getInstalledDefinitions(unauthorizedUser); + const definitionsResponse = await getInstalledDefinitions(supertest, unauthorizedUser); expect(definitionsResponse.definitions).to.eql([]); }); @@ -115,11 +101,11 @@ export default function ({ getService }: FtrProviderContext) { const enableResponse = await enableEntityDiscovery(authorizedUser); expect(enableResponse.success).to.eql(true, "authorized user can't enable EEM"); - let disableResponse = await enableEntityDiscovery(unauthorizedUser); + let disableResponse = await disableEntityDiscovery(unauthorizedUser); expect(disableResponse.success).to.eql(false, 'unauthorized user can disable EEM'); expect(disableResponse.reason).to.eql(ERROR_USER_NOT_AUTHORIZED); - disableResponse = await enableEntityDiscovery(authorizedUser); + disableResponse = await disableEntityDiscovery(authorizedUser); expect(disableResponse.success).to.eql(true, "authorized user can't disable EEM"); }); }); diff --git a/x-pack/test/api_integration/apis/entity_manager/helpers/request.ts b/x-pack/test/api_integration/apis/entity_manager/helpers/request.ts new file mode 100644 index 0000000000000..edb1ccda18f6a --- /dev/null +++ b/x-pack/test/api_integration/apis/entity_manager/helpers/request.ts @@ -0,0 +1,39 @@ +/* + * 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 { Agent } from 'supertest'; +import { EntityDefinition } from '@kbn/entities-schema'; + +export interface Auth { + username: string; + password: string; +} + +export const getInstalledDefinitions = async (supertest: Agent, auth?: Auth) => { + let req = supertest.get('/internal/entities/definition').set('kbn-xsrf', 'xxx'); + if (auth) { + req = req.auth(auth.username, auth.password); + } + const response = await req.send().expect(200); + return response.body; +}; + +export const installDefinition = async (supertest: Agent, definition: EntityDefinition) => { + return supertest + .post('/internal/entities/definition') + .set('kbn-xsrf', 'xxx') + .send(definition) + .expect(200); +}; + +export const uninstallDefinition = (supertest: Agent, id: string) => { + return supertest + .delete(`/internal/entities/definition/${id}`) + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); +}; diff --git a/x-pack/test/api_integration/apis/entity_manager/index.ts b/x-pack/test/api_integration/apis/entity_manager/index.ts index 74a5493401fa3..b6876849ef682 100644 --- a/x-pack/test/api_integration/apis/entity_manager/index.ts +++ b/x-pack/test/api_integration/apis/entity_manager/index.ts @@ -12,5 +12,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { this.tags(['entityManager']); loadTestFile(require.resolve('./enablement')); + loadTestFile(require.resolve('./definitions')); }); } diff --git a/x-pack/test/functional/page_objects/search_playground_page.ts b/x-pack/test/functional/page_objects/search_playground_page.ts index 97e53e87ed2f9..8e59c7cc3e37a 100644 --- a/x-pack/test/functional/page_objects/search_playground_page.ts +++ b/x-pack/test/functional/page_objects/search_playground_page.ts @@ -54,6 +54,13 @@ export function SearchPlaygroundPageProvider({ getService }: FtrProviderContext) await testSubjects.existOrFail('connectLLMButton'); }, + async expectPlaygroundLLMConnectorOptionsExists() { + await testSubjects.existOrFail('create-connector-flyout'); + await testSubjects.existOrFail('.gemini-card'); + await testSubjects.existOrFail('.bedrock-card'); + await testSubjects.existOrFail('.gen-ai-card'); + }, + async expectPlaygroundStartChatPageIndexButtonExists() { await testSubjects.existOrFail('createIndexButton'); }, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/telemetry/remove_time_fields_from_telemetry_stats.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/telemetry/remove_time_fields_from_telemetry_stats.ts index 912649862a24f..2d27b375684ba 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/telemetry/remove_time_fields_from_telemetry_stats.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/telemetry/remove_time_fields_from_telemetry_stats.ts @@ -15,6 +15,8 @@ export const removeExtraFieldsFromTelemetryStats = (stats: any) => { unset(value, `[${i}][${j}].start_time`); unset(value, `[${i}][${j}].end_time`); unset(value, `[${i}][${j}].cluster_uuid`); + unset(value, `[${i}][${j}].cluster_name`); + unset(value, `[${i}][${j}].license`); }); }); }); diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 4b07d1fa44d7d..6e05b3d929fbe 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -175,6 +175,7 @@ "@kbn/securitysolution-lists-common", "@kbn/securitysolution-exceptions-common", "@kbn/entityManager-plugin", - "@kbn/osquery-plugin" + "@kbn/osquery-plugin", + "@kbn/entities-schema" ] } diff --git a/x-pack/test_serverless/api_integration/test_suites/common/platform_security/encrypted_saved_objects.ts b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/encrypted_saved_objects.ts index ad873c2bc9ce4..51a0d3f03be8f 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/platform_security/encrypted_saved_objects.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/encrypted_saved_objects.ts @@ -5,6 +5,7 @@ * 2.0. */ +import expect from 'expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { InternalRequestHeader, RoleCredentials } from '../../../../shared/services'; @@ -24,13 +25,40 @@ export default function ({ getService }: FtrProviderContext) { await svlUserManager.invalidateM2mApiKeyWithRoleScope(roleAuthc); }); describe('route access', () => { - describe('disabled', () => { + describe('internal', () => { it('rotate key', async () => { - const { body, status } = await supertestWithoutAuth + let body: unknown; + let status: number; + + ({ body, status } = await supertestWithoutAuth + .post('/api/encrypted_saved_objects/_rotate_key') + // .set(internalReqHeader) + .set(roleAuthc.apiKeyHeader)); + // svlCommonApi.assertApiNotFound(body, status); + // expect a rejection because we're not using the internal header + expect(body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: expect.stringContaining('Request must contain a kbn-xsrf header.'), + }); + expect(status).toBe(400); + + ({ body, status } = await supertestWithoutAuth .post('/api/encrypted_saved_objects/_rotate_key') .set(internalReqHeader) - .set(roleAuthc.apiKeyHeader); - svlCommonApi.assertApiNotFound(body, status); + .set(roleAuthc.apiKeyHeader)); + // expect a different, legitimate error when we use the internal header + // the config does not contain decryptionOnlyKeys, so when the API is + // called successfully, it will error for this reason, and not for an + // access or or missing header reason + expect(body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: expect.stringContaining( + 'Kibana is not configured to support encryption key rotation. Update `kibana.yml` to include `xpack.encryptedSavedObjects.keyRotation.decryptionOnlyKeys` to rotate your encryption keys.' + ), + }); + expect(status).toBe(400); }); }); }); diff --git a/x-pack/test_serverless/functional/test_suites/search/search_playground/playground_overview.ts b/x-pack/test_serverless/functional/test_suites/search/search_playground/playground_overview.ts index e50e27c54fd01..44169d1e8f379 100644 --- a/x-pack/test_serverless/functional/test_suites/search/search_playground/playground_overview.ts +++ b/x-pack/test_serverless/functional/test_suites/search/search_playground/playground_overview.ts @@ -203,5 +203,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('has embedded console', async () => { await testHasEmbeddedConsole(pageObjects); }); + + describe('connectors enabled on serverless search', () => { + it('has all LLM connectors', async () => { + await pageObjects.searchPlayground.PlaygroundStartChatPage.expectOpenConnectorPagePlayground(); + await pageObjects.searchPlayground.PlaygroundStartChatPage.expectPlaygroundLLMConnectorOptionsExists(); + }); + }); }); }