From 5d0714c36ac628023b3a5637bee51bbd1624a775 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Thu, 25 Apr 2024 09:50:05 -0400 Subject: [PATCH 01/20] [Security Solution][Endpoint] enable `get-file` UI console command for SentinelOne agent types (#181162) ## Summary - Enables `get-file` response action via the Console UI for sentinelone - response action is gated behind feature flag `responseActionsSentinelOneGetFileEnabled` - Refactors the hook that opens the Response Console and removes `agentType` specific logic from it. Retrieval of console commands for a given agent type is now done in the `getEndpointConsoleCommands()` - Also refactored the `isResponseActionSupported()` and remove prior UI only implementation (not needed) - Un-skip isolate unit tests --- .../is_response_action_supported.ts | 69 ++------------ .../common/experimental_features.ts | 9 +- .../use_responder_action_data.ts | 7 +- .../get_file_action.tsx | 8 +- .../get_file_action.test.tsx | 74 +++++++++++++-- .../integration_tests/isolate_action.test.tsx | 3 +- .../lib/console_commands_definition.ts | 92 ++++++++++++++++--- .../hooks/use_with_show_responder.tsx | 48 ++-------- .../view/hooks/use_endpoint_action_items.tsx | 4 +- 9 files changed, 181 insertions(+), 133 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/service/response_actions/is_response_action_supported.ts b/x-pack/plugins/security_solution/common/endpoint/service/response_actions/is_response_action_supported.ts index 31dd195f00e03..d197995de90d8 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/response_actions/is_response_action_supported.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/response_actions/is_response_action_supported.ts @@ -5,10 +5,7 @@ * 2.0. */ -import { getRbacControl } from './utils'; -import type { EndpointPrivileges } from '../../types'; import { - RESPONSE_ACTION_API_COMMAND_TO_CONSOLE_COMMAND_MAP, type ResponseActionAgentType, type ResponseActionsApiCommandNames, type ResponseActionType, @@ -19,65 +16,6 @@ type SupportMap = Record< Record> >; -/** @private */ -const getResponseActionsSupportMap = ({ - agentType, - actionName, - actionType, - privileges, -}: { - agentType: ResponseActionAgentType; - actionName: ResponseActionsApiCommandNames; - actionType: ResponseActionType; - privileges: EndpointPrivileges; -}): boolean => { - const commandName = RESPONSE_ACTION_API_COMMAND_TO_CONSOLE_COMMAND_MAP[actionName]; - const RESPONSE_ACTIONS_SUPPORT_MAP = { - [actionName]: { - automated: { - [agentType]: - agentType === 'endpoint' - ? getRbacControl({ - commandName, - privileges, - }) - : false, - }, - manual: { - [agentType]: - agentType === 'endpoint' - ? getRbacControl({ - commandName, - privileges, - }) - : actionName === 'isolate' || actionName === 'unisolate', - }, - }, - } as SupportMap; - return RESPONSE_ACTIONS_SUPPORT_MAP[actionName][actionType][agentType]; -}; - -/** - * Determine if a given response action is currently supported - * @param agentType - * @param actionName - * @param actionType - * @param privileges - */ -export const isResponseActionSupported = ( - agentType: ResponseActionAgentType, - actionName: ResponseActionsApiCommandNames, - actionType: ResponseActionType, - privileges: EndpointPrivileges -): boolean => { - return getResponseActionsSupportMap({ - privileges, - actionName, - actionType, - agentType, - }); -}; - /** @private */ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = { isolate: { @@ -162,7 +100,12 @@ const RESPONSE_ACTIONS_SUPPORT_MAP: SupportMap = { }, }; -// FIXME:PT reemove once this module is refactored. +/** + * Check if a given Response action is supported (implemented) for a given agent type and action type + * @param agentType + * @param actionName + * @param actionType + */ export const isActionSupportedByAgentType = ( agentType: ResponseActionAgentType, actionName: ResponseActionsApiCommandNames, diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 6004c15b222c5..edf11805a4de5 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -80,8 +80,9 @@ export const allowedExperimentalValues = Object.freeze({ responseActionsSentinelOneV1Enabled: true, /** - * Enables use of SentinelOne response actions that complete asynchronously as well as support - * for more response actions. + * Enables use of SentinelOne response actions that complete asynchronously + * + * Release: v8.14.0 */ responseActionsSentinelOneV2Enabled: false, @@ -200,7 +201,9 @@ export const allowedExperimentalValues = Object.freeze({ sentinelOneDataInAnalyzerEnabled: true, /** - * Enables SentinelOne manual host manipulation actions + * Enables SentinelOne manual host isolation response actions directly through the connector + * sub-actions framework. + * v8.12.0 */ sentinelOneManualHostActionsEnabled: true, diff --git a/x-pack/plugins/security_solution/public/detections/components/endpoint_responder/use_responder_action_data.ts b/x-pack/plugins/security_solution/public/detections/components/endpoint_responder/use_responder_action_data.ts index fb20548271191..5e17f3178d59c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/endpoint_responder/use_responder_action_data.ts +++ b/x-pack/plugins/security_solution/public/detections/components/endpoint_responder/use_responder_action_data.ts @@ -10,7 +10,10 @@ import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { getSentinelOneAgentId } from '../../../common/utils/sentinelone_alert_check'; import type { ThirdPartyAgentInfo } from '../../../../common/types'; -import type { ResponseActionAgentType } from '../../../../common/endpoint/service/response_actions/constants'; +import type { + ResponseActionAgentType, + EndpointCapabilities, +} from '../../../../common/endpoint/service/response_actions/constants'; import { useGetEndpointDetails, useWithShowResponder } from '../../../management/hooks'; import { HostStatus } from '../../../../common/endpoint/types'; import { @@ -144,7 +147,7 @@ export const useResponderActionData = ({ showResponseActionsConsole({ agentId: hostInfo.metadata.agent.id, agentType: 'endpoint', - capabilities: hostInfo.metadata.Endpoint.capabilities ?? [], + capabilities: (hostInfo.metadata.Endpoint.capabilities as EndpointCapabilities[]) ?? [], hostName: hostInfo.metadata.host.name, }); } diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/get_file_action.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/get_file_action.tsx index 75c57f6ad43c4..90e44c4a56ee2 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/get_file_action.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/get_file_action.tsx @@ -25,9 +25,11 @@ export const GetFileActionResult = memo< const actionRequestBody = useMemo(() => { const endpointId = command.commandDefinition?.meta?.endpointId; const { path, comment } = command.args.args; + const agentType = command.commandDefinition?.meta?.agentType; return endpointId ? { + agent_type: agentType, endpoint_ids: [endpointId], comment: comment?.[0], parameters: { @@ -35,7 +37,11 @@ export const GetFileActionResult = memo< }, } : undefined; - }, [command.args.args, command.commandDefinition?.meta?.endpointId]); + }, [ + command.args.args, + command.commandDefinition?.meta?.agentType, + command.commandDefinition?.meta?.endpointId, + ]); const { result, actionDetails } = useConsoleActionSubmitter({ ResultComponent, diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/get_file_action.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/get_file_action.test.tsx index 9c6e49818daf6..06148aed5b483 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/get_file_action.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/get_file_action.test.tsx @@ -14,6 +14,7 @@ import { ConsoleManagerTestComponent, getConsoleManagerMockRenderResultQueriesAndActions, } from '../../../console/components/console_manager/mocks'; +import type { GetEndpointConsoleCommandsOptions } from '../../lib/console_commands_definition'; import { getEndpointConsoleCommands } from '../../lib/console_commands_definition'; import React from 'react'; import { enterConsoleCommand } from '../../../console/mocks'; @@ -33,7 +34,6 @@ import type { HttpFetchOptionsWithPath } from '@kbn/core-http-browser'; import { endpointActionResponseCodes } from '../../lib/endpoint_action_response_codes'; jest.mock('../../../../../common/components/user_privileges'); -jest.mock('../../../../../common/experimental_features_service'); describe('When using get-file action from response actions console', () => { let render: ( @@ -45,13 +45,22 @@ describe('When using get-file action from response actions console', () => { typeof getConsoleManagerMockRenderResultQueriesAndActions >; let endpointPrivileges: EndpointPrivileges; + let getConsoleCommandsOptions: GetEndpointConsoleCommandsOptions; + let mockedContext: AppContextTestRender; beforeEach(() => { - const mockedContext = createAppRootMockRenderer(); + mockedContext = createAppRootMockRenderer(); apiMocks = responseActionsHttpMocks(mockedContext.coreStart.http); endpointPrivileges = { ...getEndpointAuthzInitialStateMock(), loading: false }; + getConsoleCommandsOptions = { + agentType: 'endpoint', + endpointAgentId: 'a.b.c', + endpointCapabilities: [...ENDPOINT_CAPABILITIES], + endpointPrivileges, + }; + render = async (capabilities: EndpointCapabilities[] = [...ENDPOINT_CAPABILITIES]) => { renderResult = mockedContext.render( { consoleProps: { 'data-test-subj': 'test', commands: getEndpointConsoleCommands({ - agentType: 'endpoint', - endpointAgentId: 'a.b.c', - endpointCapabilities: [...capabilities], - endpointPrivileges, + ...getConsoleCommandsOptions, + endpointCapabilities: capabilities, }), }, }; @@ -123,7 +130,7 @@ describe('When using get-file action from response actions console', () => { await waitFor(() => { expect(apiMocks.responseProvider.getFile).toHaveBeenCalledWith({ - body: '{"endpoint_ids":["a.b.c"],"parameters":{"path":"one/two"}}', + body: '{"agent_type":"endpoint","endpoint_ids":["a.b.c"],"parameters":{"path":"one/two"}}', path: GET_FILE_ROUTE, version: '2023-10-31', }); @@ -204,4 +211,57 @@ describe('When using get-file action from response actions console', () => { ); }); }); + + describe('And agent type is SentinelOne', () => { + beforeEach(() => { + getConsoleCommandsOptions.agentType = 'sentinel_one'; + mockedContext.setExperimentalFlag({ + responseActionsSentinelOneGetFileEnabled: true, + }); + }); + + it('should display error if feature flag is not enabled', async () => { + mockedContext.setExperimentalFlag({ + responseActionsSentinelOneGetFileEnabled: false, + }); + await render(); + enterConsoleCommand(renderResult, 'get-file --path="one/two"'); + + expect(renderResult.getByTestId('test-validationError-message').textContent).toEqual( + UPGRADE_AGENT_FOR_RESPONDER('sentinel_one', 'get-file') + ); + }); + + it('should call API with `agent_type` set to `sentinel_one`', async () => { + await render(); + enterConsoleCommand(renderResult, 'get-file --path="one/two"'); + + await waitFor(() => { + expect(apiMocks.responseProvider.getFile).toHaveBeenCalledWith({ + body: '{"agent_type":"sentinel_one","endpoint_ids":["a.b.c"],"parameters":{"path":"one/two"}}', + path: GET_FILE_ROUTE, + version: '2023-10-31', + }); + }); + }); + + it('should not look at `capabilities` to determine compatibility', async () => { + await render([]); + enterConsoleCommand(renderResult, 'get-file --path="one/two"'); + + await waitFor(() => { + expect(apiMocks.responseProvider.getFile).toHaveBeenCalled(); + }); + expect(renderResult.queryByTestId('test-validationError-message')).toBeNull(); + }); + + it('should display pending message', async () => { + await render(); + enterConsoleCommand(renderResult, 'get-file --path="one/two"'); + + await waitFor(() => { + expect(renderResult.getByTestId('getFile-pending')); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/isolate_action.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/isolate_action.test.tsx index 77e70fc14180c..f5a20be31580a 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/isolate_action.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/isolate_action.test.tsx @@ -24,8 +24,7 @@ import { UPGRADE_AGENT_FOR_RESPONDER } from '../../../../../common/translations' jest.mock('../../../../../common/experimental_features_service'); -// FLAKY https://github.com/elastic/kibana/issues/145363 -describe.skip('When using isolate action from response actions console', () => { +describe('When using isolate action from response actions console', () => { let render: ( capabilities?: EndpointCapabilities[] ) => Promise>; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts index 238efec7542dc..61fb75cb52450 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { isActionSupportedByAgentType } from '../../../../../common/endpoint/service/response_actions/is_response_action_supported'; import { getRbacControl } from '../../../../../common/endpoint/service/response_actions/utils'; import { UploadActionResult } from '../command_render_components/upload_action'; import { ArgumentFileSelector } from '../../console_argument_selectors'; @@ -16,7 +17,10 @@ import type { EndpointCapabilities, ResponseActionAgentType, } from '../../../../../common/endpoint/service/response_actions/constants'; -import { RESPONSE_CONSOLE_ACTION_COMMANDS_TO_ENDPOINT_CAPABILITY } from '../../../../../common/endpoint/service/response_actions/constants'; +import { + RESPONSE_CONSOLE_ACTION_COMMANDS_TO_ENDPOINT_CAPABILITY, + RESPONSE_CONSOLE_COMMAND_TO_API_COMMAND_MAP, +} from '../../../../../common/endpoint/service/response_actions/constants'; import { GetFileActionResult } from '../command_render_components/get_file_action'; import type { Command, CommandDefinition } from '../../console'; import { IsolateActionResult } from '../command_render_components/isolate_action'; @@ -83,14 +87,18 @@ const capabilitiesAndPrivilegesValidator = ( const responderCapability = RESPONSE_CONSOLE_ACTION_COMMANDS_TO_ENDPOINT_CAPABILITY[commandName]; let errorMessage = ''; - if (!responderCapability) { - errorMessage = errorMessage.concat(UPGRADE_AGENT_FOR_RESPONDER(agentType, commandName)); - } - if (responderCapability) { - if (!agentCapabilities.includes(responderCapability)) { + + // We only validate Agent capabilities for the command for Endpoint agents + if (agentType === 'endpoint') { + if (!responderCapability) { + errorMessage = errorMessage.concat(UPGRADE_AGENT_FOR_RESPONDER(agentType, commandName)); + } + + if (responderCapability && !agentCapabilities.includes(responderCapability)) { errorMessage = errorMessage.concat(UPGRADE_AGENT_FOR_RESPONDER(agentType, commandName)); } } + if (!getRbacControl({ commandName, privileges })) { errorMessage = errorMessage.concat(INSUFFICIENT_PRIVILEGES_FOR_COMMAND); } @@ -127,27 +135,36 @@ const COMMENT_ARG_ABOUT = i18n.translate( { defaultMessage: 'A comment to go along with the action' } ); +export interface GetEndpointConsoleCommandsOptions { + endpointAgentId: string; + agentType: ResponseActionAgentType; + endpointCapabilities: ImmutableArray; + endpointPrivileges: EndpointPrivileges; +} + export const getEndpointConsoleCommands = ({ endpointAgentId, agentType, endpointCapabilities, endpointPrivileges, -}: { - endpointAgentId: string; - agentType: ResponseActionAgentType; - endpointCapabilities: ImmutableArray; - endpointPrivileges: EndpointPrivileges; -}): CommandDefinition[] => { +}: GetEndpointConsoleCommandsOptions): CommandDefinition[] => { const featureFlags = ExperimentalFeaturesService.get(); const isUploadEnabled = featureFlags.responseActionUploadEnabled; const doesEndpointSupportCommand = (commandName: ConsoleResponseActionCommands) => { + // Agent capabilities is only validated for Endpoint agent types + if (agentType !== 'endpoint') { + return true; + } + const responderCapability = RESPONSE_CONSOLE_ACTION_COMMANDS_TO_ENDPOINT_CAPABILITY[commandName]; + if (responderCapability) { return endpointCapabilities.includes(responderCapability); } + return false; }; @@ -484,5 +501,54 @@ export const getEndpointConsoleCommands = ({ }); } - return consoleCommands; + switch (agentType) { + case 'sentinel_one': + return adjustCommandsForSentinelOne({ commandList: consoleCommands }); + default: + // agentType === endpoint: just returns the defined command list + return consoleCommands; + } +}; + +/** @private */ +const adjustCommandsForSentinelOne = ({ + commandList, +}: { + commandList: CommandDefinition[]; +}): CommandDefinition[] => { + const featureFlags = ExperimentalFeaturesService.get(); + const isHostIsolationEnabled = featureFlags.responseActionsSentinelOneV1Enabled; + const isGetFileFeatureEnabled = featureFlags.responseActionsSentinelOneGetFileEnabled; + + const disableCommand = (command: CommandDefinition) => { + command.helpDisabled = true; + command.helpHidden = true; + command.validate = () => + UPGRADE_AGENT_FOR_RESPONDER('sentinel_one', command.name as ConsoleResponseActionCommands); + }; + + return commandList.map((command) => { + const agentSupportsResponseAction = + command.name === 'status' + ? false + : isActionSupportedByAgentType( + 'sentinel_one', + RESPONSE_CONSOLE_COMMAND_TO_API_COMMAND_MAP[ + command.name as ConsoleResponseActionCommands + ], + 'manual' + ); + + // If command is not supported by SentinelOne - disable it + if ( + !agentSupportsResponseAction || + (command.name === 'get-file' && !isGetFileFeatureEnabled) || + (command.name === 'isolate' && !isHostIsolationEnabled) || + (command.name === 'release' && !isHostIsolationEnabled) + ) { + disableCommand(command); + } + + return command; + }); }; diff --git a/x-pack/plugins/security_solution/public/management/hooks/use_with_show_responder.tsx b/x-pack/plugins/security_solution/public/management/hooks/use_with_show_responder.tsx index c36b02d90ccf0..15186ddc486f3 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/use_with_show_responder.tsx +++ b/x-pack/plugins/security_solution/public/management/hooks/use_with_show_responder.tsx @@ -7,19 +7,11 @@ import React, { useCallback } from 'react'; import { EuiBetaBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { - TECHNICAL_PREVIEW, - TECHNICAL_PREVIEW_TOOLTIP, - UPGRADE_AGENT_FOR_RESPONDER, -} from '../../common/translations'; +import { TECHNICAL_PREVIEW, TECHNICAL_PREVIEW_TOOLTIP } from '../../common/translations'; import { useLicense } from '../../common/hooks/use_license'; -import type { ImmutableArray } from '../../../common/endpoint/types'; -import { - type ConsoleResponseActionCommands, - RESPONSE_CONSOLE_COMMAND_TO_API_COMMAND_MAP, - type ResponseActionAgentType, -} from '../../../common/endpoint/service/response_actions/constants'; -import { isResponseActionSupported } from '../../../common/endpoint/service/response_actions/is_response_action_supported'; +import type { MaybeImmutable } from '../../../common/endpoint/types'; +import type { EndpointCapabilities } from '../../../common/endpoint/service/response_actions/constants'; +import { type ResponseActionAgentType } from '../../../common/endpoint/service/response_actions/constants'; import { HeaderSentinelOneInfo } from '../components/endpoint_responder/components/header_info/sentinel_one/header_sentinel_one_info'; import { useUserPrivileges } from '../../common/components/user_privileges'; @@ -39,16 +31,16 @@ type ShowResponseActionsConsole = (props: ResponderInfoProps) => void; export interface BasicConsoleProps { agentId: string; hostName: string; + /** Required for Endpoint agents. */ + capabilities: MaybeImmutable; } type ResponderInfoProps = | (BasicConsoleProps & { agentType: Extract; - capabilities: ImmutableArray; }) | (BasicConsoleProps & { agentType: Exclude; - capabilities: ImmutableArray; platform: string; }); @@ -85,33 +77,6 @@ export const useWithShowResponder = (): ShowResponseActionsConsole => { endpointAgentId: agentId, endpointCapabilities: capabilities, endpointPrivileges, - }).map((command) => { - if (command.name !== 'status') { - return { - ...command, - helpHidden: !isResponseActionSupported( - agentType, - RESPONSE_CONSOLE_COMMAND_TO_API_COMMAND_MAP[ - command.name as ConsoleResponseActionCommands - ], - 'manual', - endpointPrivileges - ), - }; - } else if (agentType !== 'endpoint') { - // do not show 'status' for non-endpoint agents - return { - ...command, - helpHidden: true, - validate: () => { - return UPGRADE_AGENT_FOR_RESPONDER( - agentType, - command.name as ConsoleResponseActionCommands - ); - }, - }; - } - return command; }), 'data-test-subj': `${agentType}ResponseActionsConsole`, storagePrefix: 'xpack.securitySolution.Responder', @@ -138,6 +103,7 @@ export const useWithShowResponder = (): ShowResponseActionsConsole => { meta: { agentId, hostName, + capabilities, }, consoleProps, PageTitleComponent: () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx index 6f18c60d4dc6b..1bd0c3dff62ff 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx @@ -8,6 +8,7 @@ import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { pagePathGetters } from '@kbn/fleet-plugin/public'; +import type { EndpointCapabilities } from '../../../../../../common/endpoint/service/response_actions/constants'; import { useUserPrivileges } from '../../../../../common/components/user_privileges'; import { useWithShowResponder } from '../../../../hooks'; import { APP_UI_ID } from '../../../../../../common/constants'; @@ -130,7 +131,8 @@ export const useEndpointActionItems = ( showEndpointResponseActionsConsole({ agentId: endpointMetadata.agent.id, agentType: 'endpoint', - capabilities: endpointMetadata.Endpoint.capabilities ?? [], + capabilities: + (endpointMetadata.Endpoint.capabilities as EndpointCapabilities[]) ?? [], hostName: endpointMetadata.host.name, }); }, From b64b72a8beca14e62f59da006f2fc1b8dd804e5a Mon Sep 17 00:00:00 2001 From: Alex Szabo Date: Thu, 25 Apr 2024 15:59:12 +0200 Subject: [PATCH 02/20] [CI] fix typo in deployment purge criteria (#181704) ## Summary This was causing `elasticsearch` deployments to vanish before their time is due. cc: @pgayvallet --- .buildkite/scripts/steps/cloud/purge_projects.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.buildkite/scripts/steps/cloud/purge_projects.ts b/.buildkite/scripts/steps/cloud/purge_projects.ts index a8a83266a826e..84083417f6267 100644 --- a/.buildkite/scripts/steps/cloud/purge_projects.ts +++ b/.buildkite/scripts/steps/cloud/purge_projects.ts @@ -88,7 +88,7 @@ async function purgeProjects() { } else if ( !Boolean( pullRequest.labels.filter((label: any) => - /^ci:project-deploy-(elasticearch|security|observability)$/.test(label.name) + /^ci:project-deploy-(elasticsearch|security|observability)$/.test(label.name) ).length ) ) { From 7c447adf4cd62f33382c550046e546387111a838 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Thu, 25 Apr 2024 17:11:03 +0200 Subject: [PATCH 03/20] [Security Solution][Serverless] Fix project features url (#181608) ## Summary Fixes the double slash (`//`) in the URL to manage the serverless project features from the Get Started page: before: `https://console.qa.cld.elstc.co/projects//security/:id` before After: `https://console.qa.cld.elstc.co/projects/security/:id` after Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/navigation/util.test.ts | 4 ++-- .../security_solution_serverless/public/navigation/util.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/util.test.ts b/x-pack/plugins/security_solution_serverless/public/navigation/util.test.ts index fd03f6f2ecdf3..6ea4a935b9998 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/util.test.ts +++ b/x-pack/plugins/security_solution_serverless/public/navigation/util.test.ts @@ -12,7 +12,7 @@ const cloud = { serverless: { projectId: '1234', }, - projectsUrl: 'https://cloud.elastic.co/projects', + projectsUrl: 'https://cloud.elastic.co/projects/', } as CloudStart; describe('util', () => { @@ -29,7 +29,7 @@ describe('util', () => { it('should return the correct url', () => { expect(getProjectFeaturesUrl(cloud)).toBe( - `${cloud.projectsUrl}/security/${cloud.serverless?.projectId}?open=securityProjectFeatures` + `${cloud.projectsUrl}security/${cloud.serverless?.projectId}?open=securityProjectFeatures` ); }); }); diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/util.ts b/x-pack/plugins/security_solution_serverless/public/navigation/util.ts index d57b4f7e1a4ab..ca7e1e07d6922 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/util.ts +++ b/x-pack/plugins/security_solution_serverless/public/navigation/util.ts @@ -20,7 +20,7 @@ export const getProjectFeaturesUrl = (cloud: CloudStart): string | undefined => if (!projectsBaseUrl || !projectId) { return undefined; } - return `${projectsBaseUrl}/${SECURITY_PROJECT_TYPE}/${projectId}?open=securityProjectFeatures`; + return `${projectsBaseUrl}${SECURITY_PROJECT_TYPE}/${projectId}?open=securityProjectFeatures`; }; export const getCloudUrl: GetCloudUrl = (cloudUrlKey, cloud) => { From 8e758d936bd15784a7a0819c45699b55bf57769b Mon Sep 17 00:00:00 2001 From: Maryam Saeidi Date: Thu, 25 Apr 2024 17:19:06 +0200 Subject: [PATCH 04/20] [Alert details page][Log threshold] Fix alert number annotation on the history chart (#181702) Fixes #175203 ### Summary |Before|After| |---|---| |![image](https://github.com/elastic/kibana/assets/12370520/ba83f309-c7c5-4d2d-a8de-832e80bc6eb5)|![image](https://github.com/elastic/kibana/assets/12370520/8c0a5b3a-a420-47fe-be9f-bf184d64809c)| --- .../expression_editor/criterion_preview_chart.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/observability_solution/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx b/x-pack/plugins/observability_solution/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx index 47c826178bc9b..af3bbb0ae9273 100644 --- a/x-pack/plugins/observability_solution/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx +++ b/x-pack/plugins/observability_solution/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx @@ -333,7 +333,11 @@ const CriterionPreviewChart: React.FC = ({ tickFormat={yAxisFormatter} domain={chartDomain} /> - + From b4e0575882fef55a1d54e34a463c3d139e8b7f31 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Thu, 25 Apr 2024 17:30:52 +0200 Subject: [PATCH 05/20] [ES|QL] Small refactoring to ensure that the localstorage limit will always be respected (#181415) ## Summary Make the guard of 20 max queries in the local storage more robust. This is just a refactoring of the implementation. In case anything goes wrong, it makes sure that the queries will always be the maximum allowed. --- .../src/history_local_storage.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/kbn-text-based-editor/src/history_local_storage.ts b/packages/kbn-text-based-editor/src/history_local_storage.ts index dfe3b2e05b17c..b6bbdd7d5896a 100644 --- a/packages/kbn-text-based-editor/src/history_local_storage.ts +++ b/packages/kbn-text-based-editor/src/history_local_storage.ts @@ -102,16 +102,17 @@ export const updateCachedQueries = ( ); let allQueries = [...queriesToStore, ...newQueries]; - if (allQueries.length === maxQueriesAllowed + 1) { + if (allQueries.length >= maxQueriesAllowed + 1) { const sortedByDate = allQueries.sort((a, b) => sortDates(b?.startDateMilliseconds, a?.startDateMilliseconds) ); - // delete the last element - const toBeDeletedQuery = sortedByDate[maxQueriesAllowed]; - cachedQueries.delete(toBeDeletedQuery.queryString); - allQueries = allQueries.filter((q) => { - return q.queryString !== toBeDeletedQuery.queryString; + // queries to store in the localstorage + allQueries = sortedByDate.slice(0, maxQueriesAllowed); + // clear and reset the queries in the cache + cachedQueries.clear(); + allQueries.forEach((queryItem) => { + cachedQueries.set(queryItem.queryString, queryItem); }); } localStorage.setItem(QUERY_HISTORY_ITEM_KEY, JSON.stringify(allQueries)); From 1d150fb22f0390c185bd865600729469a13fde57 Mon Sep 17 00:00:00 2001 From: acrewdson Date: Thu, 25 Apr 2024 08:37:36 -0700 Subject: [PATCH 06/20] Use more idiomatic phrasing in connectors delete modal (#181627) Updates the connectors 'delete' modal text to use a more typical word order. --- .../components/connectors/delete_connector_modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/delete_connector_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/delete_connector_modal.tsx index 84b992c89c86c..512b0bd384697 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/delete_connector_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/delete_connector_modal.tsx @@ -170,7 +170,7 @@ export const DeleteConnectorModal: React.FC = ({ isCr id="delete-related-index" label={i18n.translate( 'xpack.enterpriseSearch.deleteConnectorModal.euiCheckbox.deleteAlsoRelatedIndexLabel', - { defaultMessage: 'Delete also related index' } + { defaultMessage: 'Also delete related index' } )} checked={shouldDeleteIndex} onChange={() => setShouldDeleteIndex(!shouldDeleteIndex)} From d10ffc5acc2c4fd972f6d385f1dcd6a824c9500c Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 25 Apr 2024 16:48:04 +0100 Subject: [PATCH 07/20] [ML] Adding ML feature privileges tooltip (#181595) Adds a tooltip to the machine learning feature to inform users that an ML all privilege also grants some additional saved object privileges. ![image](https://github.com/elastic/kibana/assets/22172091/73023a51-91b9-4eb9-8889-5d9d9fad1be7) --- x-pack/plugins/ml/server/plugin.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index b8cc406eaeadf..ee36bd7382843 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -133,6 +133,10 @@ export class MlServerPlugin category: DEFAULT_APP_CATEGORIES.kibana, app: [PLUGIN_ID, 'kibana'], catalogue: [PLUGIN_ID, `${PLUGIN_ID}_file_data_visualizer`], + privilegesTooltip: i18n.translate('xpack.ml.featureRegistry.privilegesTooltip', { + defaultMessage: + 'Granting All or Read feature privilege for Machine Learning will also grant the equivalent feature privileges to certain types of Kibana saved objects, namely index patterns, dashboards, saved searches and visualizations as well as machine learning job, trained model and module saved objects.', + }), management: { insightsAndAlerting: ['jobsListLink', 'triggersActions'], }, From aa09f35d13076ef32c30129af7adc0ba8fedfb33 Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Thu, 25 Apr 2024 17:51:25 +0200 Subject: [PATCH 08/20] [kbn-test] add codeOwners in junit report (#181711) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Related to #180802 This PR adds `codeOwners` attribute in FTR JUnit report to the failed test case: ``` ``` QAF will parse JUnit report to get failures and owner, that later can be used for Slack notification Note for reviewers: we are aware that the new attribute is not following JUnit validation schema, but it seems like the best option since we can't add property on `testcase` element --- .../src/mocha/junit_report_generation.js | 21 +++++++++++++++---- .../src/mocha/junit_report_generation.test.js | 21 +++++++++---------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/packages/kbn-test/src/mocha/junit_report_generation.js b/packages/kbn-test/src/mocha/junit_report_generation.js index 001fe79a38061..4b35fba4fb1e6 100644 --- a/packages/kbn-test/src/mocha/junit_report_generation.js +++ b/packages/kbn-test/src/mocha/junit_report_generation.js @@ -7,6 +7,7 @@ */ import { REPO_ROOT } from '@kbn/repo-info'; +import { getCodeOwnersForFile, getPathsWithOwnersReversed } from '@kbn/code-owners'; import { dirname, relative } from 'path'; import { writeFileSync, mkdirSync } from 'fs'; import { inspect } from 'util'; @@ -91,6 +92,9 @@ export function setupJUnitReportGeneration(runner, options = {}) { .filter((node) => node.pending || !results.find((result) => result.node === node)) .map((node) => ({ skipped: true, node })); + // cache codeowners for quicker lookup + const reversedCodeowners = getPathsWithOwnersReversed(); + const builder = xmlBuilder.create( 'testsuites', { encoding: 'utf-8' }, @@ -108,17 +112,26 @@ export function setupJUnitReportGeneration(runner, options = {}) { 'metadata-json': JSON.stringify(metadata ?? {}), }); - function addTestcaseEl(node) { - return testsuitesEl.ele('testcase', { + function addTestcaseEl(node, failed) { + const attrs = { name: getFullTitle(node), classname: `${reportName}.${getPath(node).replace(/\./g, '·')}`, time: getDuration(node), 'metadata-json': JSON.stringify(getTestMetadata(node) || {}), - }); + }; + + // adding code owners only for the failed test case + if (failed) { + const testCaseRelativePath = getPath(node); + const owners = getCodeOwnersForFile(testCaseRelativePath, reversedCodeowners); + attrs.owners = owners || ''; // empty string when no codeowners are defined + } + + return testsuitesEl.ele('testcase', attrs); } [...results, ...skippedResults].forEach((result) => { - const el = addTestcaseEl(result.node); + const el = addTestcaseEl(result.node, result.failed); if (result.failed) { el.ele('system-out').dat(escapeCdata(getSnapshotOfRunnableLogs(result.node) || '')); diff --git a/packages/kbn-test/src/mocha/junit_report_generation.test.js b/packages/kbn-test/src/mocha/junit_report_generation.test.js index ac23d91390ed9..b6bc2e951d1df 100644 --- a/packages/kbn-test/src/mocha/junit_report_generation.test.js +++ b/packages/kbn-test/src/mocha/junit_report_generation.test.js @@ -54,17 +54,14 @@ describe('dev/mocha/junit report generation', () => { const [testsuite] = report.testsuites.testsuite; expect(testsuite.$.time).toMatch(DURATION_REGEX); expect(testsuite.$.timestamp).toMatch(ISO_DATE_SEC_REGEX); - expect(testsuite).toEqual({ - $: { - failures: '2', - name: 'test', - skipped: '1', - tests: '4', - 'metadata-json': '{}', - time: testsuite.$.time, - timestamp: testsuite.$.timestamp, - }, - testcase: testsuite.testcase, + expect(testsuite.$).toEqual({ + failures: '2', + name: 'test', + skipped: '1', + tests: '4', + 'metadata-json': '{}', + time: testsuite.$.time, + timestamp: testsuite.$.timestamp, }); // there are actually only three tests, but since the hook failed @@ -94,6 +91,7 @@ describe('dev/mocha/junit report generation', () => { name: 'SUITE fails', time: testFail.$.time, 'metadata-json': '{}', + owners: '', }, 'system-out': testFail['system-out'], failure: [testFail.failure[0]], @@ -108,6 +106,7 @@ describe('dev/mocha/junit report generation', () => { name: 'SUITE SUB_SUITE "before each" hook: fail hook for "never runs"', time: beforeEachFail.$.time, 'metadata-json': '{}', + owners: '', }, 'system-out': testFail['system-out'], failure: [beforeEachFail.failure[0]], From 15c6a36eeb735af5aef8bb5564efbd4f28f7bc47 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Thu, 25 Apr 2024 09:05:29 -0700 Subject: [PATCH 09/20] [Reporting] Remove usage of deprecated React rendering utilities (#180759) ## Summary Partially addresses https://github.com/elastic/kibana-team/issues/805 Follows https://github.com/elastic/kibana/pull/180516 These changes come up from searching in the code and finding where certain kinds of deprecated AppEx-SharedUX modules are imported. **Reviewers: Please interact with critical paths through the UI components touched in this PR, ESPECIALLY in terms of testing dark mode and i18n.** This focuses on code within Reporting. image Note: this also makes inclusion of `i18n` and `analytics` dependencies consistent. Analytics is an optional dependency for the SharedUX modules, which wrap `KibanaErrorBoundaryProvider` and is designed to capture telemetry about errors that are caught in the error boundary. ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../panel_actions/get_csv_panel_action.tsx | 17 ++- packages/kbn-reporting/public/share/index.ts | 2 +- .../public/share/share_context_menu/index.ts | 33 +++-- .../register_csv_modal_reporting.tsx | 13 +- .../register_csv_reporting.tsx | 8 +- .../register_pdf_png_modal_reporting.tsx | 33 ++--- .../register_pdf_png_reporting.tsx | 12 +- .../reporting_panel_content.test.tsx | 16 +-- .../reporting_panel_content.tsx | 127 +++++++++--------- .../screen_capture_panel_content.test.tsx | 32 ++--- .../share/shared/get_shared_components.tsx | 42 +++--- packages/kbn-reporting/public/tsconfig.json | 3 + packages/kbn-reporting/public/types.ts | 16 +++ .../public/lib/stream_handler.test.ts | 76 ++--------- .../reporting/public/lib/stream_handler.ts | 43 +++--- .../management/mount_management_section.tsx | 39 +++--- x-pack/plugins/reporting/public/mocks.ts | 3 +- .../public/notifier/general_error.tsx | 12 +- .../reporting/public/notifier/job_failure.tsx | 12 +- .../reporting/public/notifier/job_success.tsx | 10 +- .../reporting/public/notifier/job_warning.tsx | 10 +- .../public/notifier/job_warning_formulas.tsx | 10 +- .../public/notifier/job_warning_max_size.tsx | 10 +- x-pack/plugins/reporting/public/plugin.ts | 89 ++++++------ x-pack/plugins/reporting/public/types.ts | 21 +++ x-pack/plugins/reporting/tsconfig.json | 3 +- 26 files changed, 318 insertions(+), 374 deletions(-) diff --git a/packages/kbn-reporting/get_csv_panel_actions/panel_actions/get_csv_panel_action.tsx b/packages/kbn-reporting/get_csv_panel_actions/panel_actions/get_csv_panel_action.tsx index cd34d64a8429a..bcf3f2af51956 100644 --- a/packages/kbn-reporting/get_csv_panel_actions/panel_actions/get_csv_panel_action.tsx +++ b/packages/kbn-reporting/get_csv_panel_actions/panel_actions/get_csv_panel_action.tsx @@ -48,11 +48,26 @@ export interface PanelActionDependencies { licensing: LicensingPluginStart; } +type StartServices = [ + Pick< + CoreStart, + // required for modules that render React + | 'analytics' + | 'i18n' + | 'theme' + // used extensively in Reporting share panel action + | 'application' + | 'uiSettings' + >, + PanelActionDependencies, + unknown +]; + interface Params { apiClient: ReportingAPIClient; csvConfig: ClientConfigType['csv']; core: CoreSetup; - startServices$: Observable<[CoreStart, PanelActionDependencies, unknown]>; + startServices$: Observable; usesUiCapabilities: boolean; } diff --git a/packages/kbn-reporting/public/share/index.ts b/packages/kbn-reporting/public/share/index.ts index 7c7b6819afc07..b2587965858d8 100644 --- a/packages/kbn-reporting/public/share/index.ts +++ b/packages/kbn-reporting/public/share/index.ts @@ -12,4 +12,4 @@ export { reportingScreenshotShareProvider } from './share_context_menu/register_ export { reportingCsvShareProvider } from './share_context_menu/register_csv_reporting'; export { reportingCsvShareProvider as reportingCsvShareModalProvider } from './share_context_menu/register_csv_modal_reporting'; export type { ReportingPublicComponents } from './shared/get_shared_components'; -export type { JobParamsProviderOptions } from './share_context_menu'; +export type { JobParamsProviderOptions, StartServices } from './share_context_menu'; diff --git a/packages/kbn-reporting/public/share/share_context_menu/index.ts b/packages/kbn-reporting/public/share/share_context_menu/index.ts index 1259aee0008ad..c27ec3f38c68c 100644 --- a/packages/kbn-reporting/public/share/share_context_menu/index.ts +++ b/packages/kbn-reporting/public/share/share_context_menu/index.ts @@ -6,35 +6,42 @@ * Side Public License, v 1. */ -import type { - ApplicationStart, - I18nStart, - IUiSettingsClient, - ThemeServiceSetup, - ToastsSetup, -} from '@kbn/core/public'; +import * as Rx from 'rxjs'; + +import type { ApplicationStart, CoreStart } from '@kbn/core/public'; import { ILicense } from '@kbn/licensing-plugin/public'; import type { LayoutParams } from '@kbn/screenshotting-plugin/common'; + import type { ReportingAPIClient } from '../../reporting_api_client'; +export type StartServices = [ + Pick< + CoreStart, + // required for modules that render React + | 'analytics' + | 'i18n' + | 'theme' + // used extensively in Reporting share context menus and modal + | 'notifications' + >, + unknown, + unknown +]; + export interface ExportModalShareOpts { apiClient: ReportingAPIClient; - uiSettings: IUiSettingsClient; usesUiCapabilities: boolean; license: ILicense; application: ApplicationStart; - theme: ThemeServiceSetup; - i18n: I18nStart; + startServices$: Rx.Observable; } export interface ExportPanelShareOpts { apiClient: ReportingAPIClient; - toasts: ToastsSetup; - uiSettings: IUiSettingsClient; usesUiCapabilities: boolean; license: ILicense; application: ApplicationStart; - theme: ThemeServiceSetup; + startServices$: Rx.Observable; } export interface ReportingSharingData { diff --git a/packages/kbn-reporting/public/share/share_context_menu/register_csv_modal_reporting.tsx b/packages/kbn-reporting/public/share/share_context_menu/register_csv_modal_reporting.tsx index 7525c714de7b2..70225a3033773 100644 --- a/packages/kbn-reporting/public/share/share_context_menu/register_csv_modal_reporting.tsx +++ b/packages/kbn-reporting/public/share/share_context_menu/register_csv_modal_reporting.tsx @@ -7,14 +7,15 @@ */ import { i18n } from '@kbn/i18n'; -import React from 'react'; import { toMountPoint } from '@kbn/react-kibana-mount'; +import React from 'react'; +import { firstValueFrom } from 'rxjs'; import { CSV_JOB_TYPE, CSV_JOB_TYPE_V2 } from '@kbn/reporting-export-types-csv-common'; import type { SearchSourceFields } from '@kbn/data-plugin/common'; -import { ShareContext, ShareMenuItem } from '@kbn/share-plugin/public'; import { FormattedMessage, InjectedIntl } from '@kbn/i18n-react'; +import { ShareContext, ShareMenuItem } from '@kbn/share-plugin/public'; import type { ExportModalShareOpts } from '.'; import { checkLicense } from '../..'; @@ -23,8 +24,7 @@ export const reportingCsvShareProvider = ({ application, license, usesUiCapabilities, - i18n: i18nStart, - theme, + startServices$, }: ExportModalShareOpts) => { const getShareMenuItems = ({ objectType, sharingData, toasts }: ShareContext) => { if ('search' !== objectType) { @@ -86,7 +86,8 @@ export const reportingCsvShareProvider = ({ const decoratedJobParams = apiClient.getDecoratedJobParams(getJobParams()); return apiClient .createReportingJob(reportType, decoratedJobParams) - .then(() => { + .then(() => firstValueFrom(startServices$)) + .then(([startServices]) => { toasts.addSuccess({ title: intl.formatMessage( { @@ -110,7 +111,7 @@ export const reportingCsvShareProvider = ({ ), }} />, - { theme, i18n: i18nStart } + startServices ), 'data-test-subj': 'queueReportSuccess', }); diff --git a/packages/kbn-reporting/public/share/share_context_menu/register_csv_reporting.tsx b/packages/kbn-reporting/public/share/share_context_menu/register_csv_reporting.tsx index 58c380a2201e4..5144d32bc48cd 100644 --- a/packages/kbn-reporting/public/share/share_context_menu/register_csv_reporting.tsx +++ b/packages/kbn-reporting/public/share/share_context_menu/register_csv_reporting.tsx @@ -19,12 +19,10 @@ import { ReportingPanelContent } from './reporting_panel_content_lazy'; export const reportingCsvShareProvider = ({ apiClient, - toasts, - uiSettings, application, license, usesUiCapabilities, - theme, + startServices$, }: ExportPanelShareOpts): ShareMenuProvider => { const getShareMenuItems = ({ objectType, objectId, sharingData, onClose }: ShareContext) => { if ('search' !== objectType) { @@ -104,14 +102,12 @@ export const reportingCsvShareProvider = ({ ), }, diff --git a/packages/kbn-reporting/public/share/share_context_menu/register_pdf_png_modal_reporting.tsx b/packages/kbn-reporting/public/share/share_context_menu/register_pdf_png_modal_reporting.tsx index 1761b8df45878..621ab6fc5a0d3 100644 --- a/packages/kbn-reporting/public/share/share_context_menu/register_pdf_png_modal_reporting.tsx +++ b/packages/kbn-reporting/public/share/share_context_menu/register_pdf_png_modal_reporting.tsx @@ -7,17 +7,18 @@ */ import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { ShareContext, ShareMenuItem, ShareMenuProvider } from '@kbn/share-plugin/public'; import { FormattedMessage, InjectedIntl } from '@kbn/i18n-react'; import { toMountPoint } from '@kbn/react-kibana-mount'; -import { checkLicense } from '../../license_check'; +import { ShareContext, ShareMenuItem, ShareMenuProvider } from '@kbn/share-plugin/public'; +import React from 'react'; +import { firstValueFrom } from 'rxjs'; import { ExportModalShareOpts, ExportPanelShareOpts, JobParamsProviderOptions, ReportingSharingData, } from '.'; +import { checkLicense } from '../../license_check'; import { ScreenCapturePanelContent } from './screen_capture_panel_content_lazy'; const getJobParams = (opts: JobParamsProviderOptions, type: 'pngV2' | 'printablePdfV2') => () => { @@ -45,12 +46,10 @@ const getJobParams = (opts: JobParamsProviderOptions, type: 'pngV2' | 'printable */ export const reportingScreenshotShareProvider = ({ apiClient, - toasts, - uiSettings, license, application, usesUiCapabilities, - theme, + startServices$, }: ExportPanelShareOpts): ShareMenuProvider => { const getShareMenuItems = ({ objectType, @@ -136,15 +135,13 @@ export const reportingScreenshotShareProvider = ({ content: ( ), }, @@ -169,8 +166,7 @@ export const reportingScreenshotShareProvider = ({ content: ( ), }, @@ -200,8 +195,7 @@ export const reportingExportModalProvider = ({ license, application, usesUiCapabilities, - theme, - i18n: i18nStart, + startServices$, }: ExportModalShareOpts): ShareMenuProvider => { const getShareMenuItems = ({ objectType, @@ -294,7 +288,8 @@ export const reportingExportModalProvider = ({ return apiClient .createReportingJob('printablePdfV2', decoratedJobParams) - .then(() => { + .then(() => firstValueFrom(startServices$)) + .then(([startServices]) => { toasts.addSuccess({ title: intl.formatMessage( { @@ -318,7 +313,7 @@ export const reportingExportModalProvider = ({ ), }} />, - { theme, i18n: i18nStart } + startServices ), 'data-test-subj': 'queueReportSuccess', }); @@ -347,7 +342,8 @@ export const reportingExportModalProvider = ({ }); return apiClient .createReportingJob('pngV2', decoratedJobParams) - .then(() => { + .then(() => firstValueFrom(startServices$)) + .then(([startServices]) => { toasts.addSuccess({ title: intl.formatMessage( { @@ -371,7 +367,7 @@ export const reportingExportModalProvider = ({ ), }} />, - { theme, i18n: i18nStart } + startServices ), 'data-test-subj': 'queueReportSuccess', }); @@ -414,7 +410,6 @@ export const reportingExportModalProvider = ({ /> ), layoutOption: objectType === 'dashboard' ? ('print' as const) : undefined, - theme, renderLayoutOptionSwitch: objectType === 'dashboard', renderCopyURLButton: true, absoluteUrl: new URL(relativePathPDF, window.location.href).toString(), diff --git a/packages/kbn-reporting/public/share/share_context_menu/register_pdf_png_reporting.tsx b/packages/kbn-reporting/public/share/share_context_menu/register_pdf_png_reporting.tsx index 6446efb787f66..15e671d2afc64 100644 --- a/packages/kbn-reporting/public/share/share_context_menu/register_pdf_png_reporting.tsx +++ b/packages/kbn-reporting/public/share/share_context_menu/register_pdf_png_reporting.tsx @@ -57,12 +57,10 @@ const getJobParams = export const reportingScreenshotShareProvider = ({ apiClient, - toasts, - uiSettings, license, application, usesUiCapabilities, - theme, + startServices$, }: ExportPanelShareOpts): ShareMenuProvider => { const getShareMenuItems = ({ objectType, @@ -150,15 +148,13 @@ export const reportingScreenshotShareProvider = ({ content: ( ), }, @@ -185,8 +181,6 @@ export const reportingScreenshotShareProvider = ({ content: ( ), }, diff --git a/packages/kbn-reporting/public/share/share_context_menu/reporting_panel_content/reporting_panel_content.test.tsx b/packages/kbn-reporting/public/share/share_context_menu/reporting_panel_content/reporting_panel_content.test.tsx index 67a7433fe0878..133ea782c0bdf 100644 --- a/packages/kbn-reporting/public/share/share_context_menu/reporting_panel_content/reporting_panel_content.test.tsx +++ b/packages/kbn-reporting/public/share/share_context_menu/reporting_panel_content/reporting_panel_content.test.tsx @@ -6,14 +6,10 @@ * Side Public License, v 1. */ -import { - httpServiceMock, - notificationServiceMock, - themeServiceMock, - uiSettingsServiceMock, -} from '@kbn/core/public/mocks'; +import { coreMock, httpServiceMock, uiSettingsServiceMock } from '@kbn/core/public/mocks'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import React from 'react'; +import * as Rx from 'rxjs'; import { ReportingPanelProps as Props, ReportingPanelContent } from '.'; import { ReportingAPIClient } from '../../..'; import { ErrorUnsavedWorkPanel } from './components'; @@ -23,8 +19,6 @@ jest.mock('./constants', () => ({ getMaxUrlLength: jest.fn(() => 9999999), })); -const theme = themeServiceMock.createSetupContract(); - describe('ReportingPanelContent', () => { const props: Partial = { layoutId: 'super_cool_layout_id_X', @@ -34,7 +28,6 @@ describe('ReportingPanelContent', () => { objectType: 'noice_object', title: 'ultimate_title', }; - const toasts = notificationServiceMock.createSetupContract().toasts; const http = httpServiceMock.createSetupContract(); const uiSettings = uiSettingsServiceMock.createSetupContract(); let apiClient: ReportingAPIClient; @@ -50,6 +43,7 @@ describe('ReportingPanelContent', () => { apiClient = new ReportingAPIClient(http, uiSettings, '7.15.0-test'); }); + const { getStartServices } = coreMock.createSetup(); const mountComponent = (newProps: Partial) => mountWithIntl( { layoutId={props.layoutId} getJobParams={() => jobParams} apiClient={apiClient} - toasts={toasts} - uiSettings={uiSettings} - theme={theme} + startServices$={Rx.from(getStartServices())} {...props} {...newProps} /> diff --git a/packages/kbn-reporting/public/share/share_context_menu/reporting_panel_content/reporting_panel_content.tsx b/packages/kbn-reporting/public/share/share_context_menu/reporting_panel_content/reporting_panel_content.tsx index bafc355470fad..a544e6a44464b 100644 --- a/packages/kbn-reporting/public/share/share_context_menu/reporting_panel_content/reporting_panel_content.tsx +++ b/packages/kbn-reporting/public/share/share_context_menu/reporting_panel_content/reporting_panel_content.tsx @@ -7,6 +7,7 @@ */ import React, { Component, ReactElement } from 'react'; +import * as Rx from 'rxjs'; import { CSV_REPORT_TYPE, CSV_REPORT_TYPE_V2 } from '@kbn/reporting-export-types-csv-common'; import { PDF_REPORT_TYPE, PDF_REPORT_TYPE_V2 } from '@kbn/reporting-export-types-pdf-common'; @@ -22,13 +23,14 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; -import { IUiSettingsClient, ThemeServiceSetup, ToastsSetup } from '@kbn/core/public'; + import { i18n } from '@kbn/i18n'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n-react'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { toMountPoint } from '@kbn/react-kibana-mount'; import type { BaseParams } from '@kbn/reporting-common/types'; -import { ReportingAPIClient } from '../../../reporting_api_client'; +import type { StartServices } from '../..'; +import type { ReportingAPIClient } from '../../../reporting_api_client'; import { ErrorUnsavedWorkPanel, ErrorUrlTooLongPanel } from './components'; import { getMaxUrlLength } from './constants'; @@ -38,8 +40,6 @@ import { getMaxUrlLength } from './constants'; */ export interface ReportingPanelProps { apiClient: ReportingAPIClient; - toasts: ToastsSetup; - uiSettings: IUiSettingsClient; reportType: string; requiresSavedState: boolean; // Whether the report to be generated requires saved state that is not captured in the URL submitted to the report generator. @@ -51,7 +51,8 @@ export interface ReportingPanelProps { options?: ReactElement | null; isDirty?: boolean; onClose?: () => void; - theme: ThemeServiceSetup; + + startServices$: Rx.Observable; } export type Props = ReportingPanelProps & { intl: InjectedIntl }; @@ -277,68 +278,66 @@ class ReportingPanelContentUi extends Component { this.setState({ absoluteUrl }); }; - private createReportingJob = () => { - const { intl } = this.props; - const decoratedJobParams = this.props.apiClient.getDecoratedJobParams( - this.props.getJobParams() - ); + private createReportingJob = async () => { + const { startServices$, apiClient, intl } = this.props; + const [coreStart] = await Rx.firstValueFrom(startServices$); + const decoratedJobParams = apiClient.getDecoratedJobParams(this.props.getJobParams()); + const { toasts } = coreStart.notifications; this.setState({ isCreatingReportJob: true }); - return this.props.apiClient - .createReportingJob(this.props.reportType, decoratedJobParams) - .then(() => { - this.props.toasts.addSuccess({ - title: intl.formatMessage( - { - id: 'reporting.share.panelContent.successfullyQueuedReportNotificationTitle', - defaultMessage: 'Queued report for {objectType}', - }, - { objectType: this.state.objectType } - ), - text: toMountPoint( - - - - ), - }} - />, - { theme$: this.props.theme.theme$ } - ), - 'data-test-subj': 'queueReportSuccess', - }); - if (this.props.onClose) { - this.props.onClose(); - } - if (this.mounted) { - this.setState({ isCreatingReportJob: false }); - } - }) - .catch((error) => { - // eslint-disable-next-line no-console - console.error(error); - this.props.toasts.addError(error, { - title: intl.formatMessage({ - id: 'reporting.share.panelContent.notification.reportingErrorTitle', - defaultMessage: 'Unable to create report', - }), - toastMessage: intl.formatMessage({ - id: 'reporting.share.panelContent.notification.reportingErrorToastMessage', - defaultMessage: `We couldn't create a report at this time.`, - }), - }); - if (this.mounted) { - this.setState({ isCreatingReportJob: false }); - } + try { + await this.props.apiClient.createReportingJob(this.props.reportType, decoratedJobParams); + toasts.addSuccess({ + title: intl.formatMessage( + { + id: 'reporting.share.panelContent.successfullyQueuedReportNotificationTitle', + defaultMessage: 'Queued report for {objectType}', + }, + { objectType: this.state.objectType } + ), + text: toMountPoint( + + + + ), + }} + />, + coreStart + ), + 'data-test-subj': 'queueReportSuccess', }); + if (this.props.onClose) { + this.props.onClose(); + } + if (this.mounted) { + this.setState({ isCreatingReportJob: false }); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + toasts.addError(error, { + title: intl.formatMessage({ + id: 'reporting.share.panelContent.notification.reportingErrorTitle', + defaultMessage: 'Unable to create report', + }), + toastMessage: intl.formatMessage({ + id: 'reporting.share.panelContent.notification.reportingErrorToastMessage', + defaultMessage: `We couldn't create a report at this time.`, + }), + }); + if (this.mounted) { + this.setState({ isCreatingReportJob: false }); + } + } }; } diff --git a/packages/kbn-reporting/public/share/share_context_menu/screen_capture_panel_content.test.tsx b/packages/kbn-reporting/public/share/share_context_menu/screen_capture_panel_content.test.tsx index 42d599da19622..854ac403e2d7c 100644 --- a/packages/kbn-reporting/public/share/share_context_menu/screen_capture_panel_content.test.tsx +++ b/packages/kbn-reporting/public/share/share_context_menu/screen_capture_panel_content.test.tsx @@ -6,14 +6,16 @@ * Side Public License, v 1. */ -import { coreMock, themeServiceMock } from '@kbn/core/public/mocks'; +import * as Rx from 'rxjs'; +import { coreMock } from '@kbn/core/public/mocks'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { mount } from 'enzyme'; import React from 'react'; import { ReportingAPIClient } from '../..'; import { ScreenCapturePanelContent } from './screen_capture_panel_content'; -const { http, uiSettings, ...coreSetup } = coreMock.createSetup(); +const { http, uiSettings, getStartServices } = coreMock.createSetup(); +const startServices$ = Rx.from(getStartServices()); uiSettings.get.mockImplementation((key: string) => { switch (key) { case 'dateFormat:tz': @@ -28,8 +30,6 @@ const getJobParamsDefault = () => ({ browserTimezone: 'America/New_York', }); -const theme = themeServiceMock.createSetupContract(); - test('ScreenCapturePanelContent renders the default view properly', () => { const component = mount( @@ -37,10 +37,8 @@ test('ScreenCapturePanelContent renders the default view properly', () => { reportType="Analytical App" requiresSavedState={false} apiClient={apiClient} - uiSettings={uiSettings} - toasts={coreSetup.notifications.toasts} getJobParams={getJobParamsDefault} - theme={theme} + startServices$={startServices$} /> ); @@ -57,10 +55,8 @@ test('ScreenCapturePanelContent properly renders a view with "canvas" layout opt reportType="Analytical App" requiresSavedState={false} apiClient={apiClient} - uiSettings={uiSettings} - toasts={coreSetup.notifications.toasts} getJobParams={getJobParamsDefault} - theme={theme} + startServices$={startServices$} /> ); @@ -76,11 +72,9 @@ test('ScreenCapturePanelContent allows POST URL to be copied when objectId is pr reportType="Analytical App" requiresSavedState={false} apiClient={apiClient} - uiSettings={uiSettings} - toasts={coreSetup.notifications.toasts} getJobParams={getJobParamsDefault} objectId={'1234-5'} - theme={theme} + startServices$={startServices$} /> ); @@ -96,10 +90,8 @@ test('ScreenCapturePanelContent does not allow POST URL to be copied when object reportType="Analytical App" requiresSavedState={false} apiClient={apiClient} - uiSettings={uiSettings} - toasts={coreSetup.notifications.toasts} getJobParams={getJobParamsDefault} - theme={theme} + startServices$={startServices$} /> ); @@ -115,10 +107,8 @@ test('ScreenCapturePanelContent properly renders a view with "print" layout opti reportType="Analytical App" requiresSavedState={false} apiClient={apiClient} - uiSettings={uiSettings} - toasts={coreSetup.notifications.toasts} getJobParams={getJobParamsDefault} - theme={theme} + startServices$={startServices$} /> ); @@ -135,10 +125,8 @@ test('ScreenCapturePanelContent decorated job params are visible in the POST URL requiresSavedState={false} isDirty={false} apiClient={apiClient} - uiSettings={uiSettings} - toasts={coreSetup.notifications.toasts} getJobParams={getJobParamsDefault} - theme={theme} + startServices$={startServices$} /> ); diff --git a/packages/kbn-reporting/public/share/shared/get_shared_components.tsx b/packages/kbn-reporting/public/share/shared/get_shared_components.tsx index e9a4499071d97..bc4ecc2428132 100644 --- a/packages/kbn-reporting/public/share/shared/get_shared_components.tsx +++ b/packages/kbn-reporting/public/share/shared/get_shared_components.tsx @@ -6,13 +6,17 @@ * Side Public License, v 1. */ -import { CoreSetup } from '@kbn/core/public'; -import { PDF_REPORT_TYPE_V2 } from '@kbn/reporting-export-types-pdf-common'; -import { PNG_REPORT_TYPE_V2 } from '@kbn/reporting-export-types-png-common'; import React from 'react'; +import { Observable } from 'rxjs'; + +import { PDF_REPORT_TYPE, PDF_REPORT_TYPE_V2 } from '@kbn/reporting-export-types-pdf-common'; +import { PNG_REPORT_TYPE, PNG_REPORT_TYPE_V2 } from '@kbn/reporting-export-types-png-common'; + +import { StartServices } from '..'; import { ReportingAPIClient } from '../..'; import { ReportingPanelProps } from '../share_context_menu/reporting_panel_content'; import { ScreenCapturePanelContent } from '../share_context_menu/screen_capture_panel_content_lazy'; + /** * Properties for displaying a share menu with Reporting features. */ @@ -53,24 +57,20 @@ export interface ReportingPublicComponents { * Related Discuss issue: https://github.com/elastic/kibana/issues/101422 */ export function getSharedComponents( - core: CoreSetup, - apiClient: ReportingAPIClient + apiClient: ReportingAPIClient, + startServices$: Observable ): ReportingPublicComponents { return { ReportingPanelPDFV2(props: ApplicationProps) { - const getJobParams = props.getJobParams as ReportingPanelProps['getJobParams']; if (props.layoutOption === 'canvas') { return ( ); } else { @@ -78,55 +78,43 @@ export function getSharedComponents( } }, ReportingPanelPNGV2(props: ApplicationProps) { - const getJobParams = props.getJobParams as ReportingPanelProps['getJobParams']; if (props.layoutOption === 'canvas') { return ( ); } }, ReportingModalPDF(props: ApplicationProps) { - const getJobParams = props.getJobParams as ReportingPanelProps['getJobParams']; if (props.layoutOption === 'canvas') { return ( ); } }, ReportingModalPNG(props: ApplicationProps) { - const getJobParams = props.getJobParams as ReportingPanelProps['getJobParams']; if (props.layoutOption === 'canvas') { return ( ); } diff --git a/packages/kbn-reporting/public/tsconfig.json b/packages/kbn-reporting/public/tsconfig.json index 7b36e7eeeb616..1f17ff412c286 100644 --- a/packages/kbn-reporting/public/tsconfig.json +++ b/packages/kbn-reporting/public/tsconfig.json @@ -31,5 +31,8 @@ "@kbn/i18n-react", "@kbn/test-jest-helpers", "@kbn/react-kibana-mount", + "@kbn/home-plugin", + "@kbn/management-plugin", + "@kbn/ui-actions-plugin", ] } diff --git a/packages/kbn-reporting/public/types.ts b/packages/kbn-reporting/public/types.ts index 67f5755e367cc..82da5e5cfa001 100644 --- a/packages/kbn-reporting/public/types.ts +++ b/packages/kbn-reporting/public/types.ts @@ -6,6 +6,22 @@ * Side Public License, v 1. */ +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { HomePublicPluginStart } from '@kbn/home-plugin/public'; +import type { LicensingPluginStart } from '@kbn/licensing-plugin/public'; +import type { ManagementStart } from '@kbn/management-plugin/public'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; +import type { UiActionsStart } from '@kbn/ui-actions-plugin/public'; + +export interface ReportingPublicPluginStartDependencies { + home: HomePublicPluginStart; + data: DataPublicPluginStart; + management: ManagementStart; + licensing: LicensingPluginStart; + uiActions: UiActionsStart; + share: SharePluginStart; +} + export interface ClientConfigType { csv: { enablePanelActionDownload: boolean; diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts index 37ef7967ae287..88a624df55280 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { NotificationsStart } from '@kbn/core/public'; -import { coreMock, docLinksServiceMock, themeServiceMock } from '@kbn/core/public/mocks'; +import { coreMock } from '@kbn/core/public/mocks'; import { JobId, ReportApiJSON } from '@kbn/reporting-common/types'; import { JobSummary, JobSummarySet } from '../types'; @@ -43,19 +42,10 @@ jobQueueClientMock.getError = () => Promise.resolve('this is the failed report e jobQueueClientMock.getManagementLink = () => '/#management'; jobQueueClientMock.getReportURL = () => '/reporting/download/job-123'; -const mockShowDanger = jest.fn(); -const mockShowSuccess = jest.fn(); -const mockShowWarning = jest.fn(); -const notificationsMock = { - toasts: { - addDanger: mockShowDanger, - addSuccess: mockShowSuccess, - addWarning: mockShowWarning, - }, -} as unknown as NotificationsStart; - -const theme = themeServiceMock.createStartContract(); -const docLink = docLinksServiceMock.createStartContract(); +const core = coreMock.createStart(); +const mockShowDanger = jest.spyOn(core.notifications.toasts, 'addDanger'); +const mockShowSuccess = jest.spyOn(core.notifications.toasts, 'addSuccess'); +const mockShowWarning = jest.spyOn(core.notifications.toasts, 'addWarning'); describe('stream handler', () => { afterEach(() => { @@ -63,23 +53,13 @@ describe('stream handler', () => { }); it('constructs', () => { - const sh = new TestReportingNotifierStreamHandler( - notificationsMock, - jobQueueClientMock, - theme, - docLink - ); + const sh = new TestReportingNotifierStreamHandler(jobQueueClientMock, core); expect(sh).not.toBe(null); }); describe('findChangedStatusJobs', () => { it('finds no changed status jobs from empty', (done) => { - const sh = new TestReportingNotifierStreamHandler( - notificationsMock, - jobQueueClientMock, - theme, - docLink - ); + const sh = new TestReportingNotifierStreamHandler(jobQueueClientMock, core); const findJobs = sh.testFindChangedStatusJobs([]); findJobs.subscribe((data) => { expect(data).toEqual({ completed: [], failed: [] }); @@ -88,12 +68,7 @@ describe('stream handler', () => { }); it('finds changed status jobs', (done) => { - const sh = new TestReportingNotifierStreamHandler( - notificationsMock, - jobQueueClientMock, - theme, - docLink - ); + const sh = new TestReportingNotifierStreamHandler(jobQueueClientMock, core); const findJobs = sh.testFindChangedStatusJobs([ 'job-source-mock1', 'job-source-mock2', @@ -110,12 +85,7 @@ describe('stream handler', () => { describe('showNotifications', () => { it('show success', (done) => { - const sh = new TestReportingNotifierStreamHandler( - notificationsMock, - jobQueueClientMock, - theme, - docLink - ); + const sh = new TestReportingNotifierStreamHandler(jobQueueClientMock, core); sh.testShowNotifications({ completed: [ { @@ -136,12 +106,7 @@ describe('stream handler', () => { }); it('show max length warning', (done) => { - const sh = new TestReportingNotifierStreamHandler( - notificationsMock, - jobQueueClientMock, - theme, - docLink - ); + const sh = new TestReportingNotifierStreamHandler(jobQueueClientMock, core); sh.testShowNotifications({ completed: [ { @@ -163,12 +128,7 @@ describe('stream handler', () => { }); it('show csv formulas warning', (done) => { - const sh = new TestReportingNotifierStreamHandler( - notificationsMock, - jobQueueClientMock, - theme, - docLink - ); + const sh = new TestReportingNotifierStreamHandler(jobQueueClientMock, core); sh.testShowNotifications({ completed: [ { @@ -190,12 +150,7 @@ describe('stream handler', () => { }); it('show failed job toast', (done) => { - const sh = new TestReportingNotifierStreamHandler( - notificationsMock, - jobQueueClientMock, - theme, - docLink - ); + const sh = new TestReportingNotifierStreamHandler(jobQueueClientMock, core); sh.testShowNotifications({ completed: [], failed: [ @@ -216,12 +171,7 @@ describe('stream handler', () => { }); it('show multiple toast', (done) => { - const sh = new TestReportingNotifierStreamHandler( - notificationsMock, - jobQueueClientMock, - theme, - docLink - ); + const sh = new TestReportingNotifierStreamHandler(jobQueueClientMock, core); sh.testShowNotifications({ completed: [ { diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.ts b/x-pack/plugins/reporting/public/lib/stream_handler.ts index 049aea96e1af2..78513de46c801 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.ts @@ -8,7 +8,7 @@ import * as Rx from 'rxjs'; import { catchError, filter, map, mergeMap, takeUntil } from 'rxjs'; -import { DocLinksStart, NotificationsSetup, ThemeServiceStart } from '@kbn/core/public'; +import { CoreStart } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { JOB_STATUS } from '@kbn/reporting-common'; import { JobId } from '@kbn/reporting-common/types'; @@ -42,18 +42,14 @@ function getReportStatus(src: Job): JobSummary { }; } -function handleError( - err: Error, - notifications: NotificationsSetup, - theme: ThemeServiceStart -): Rx.Observable { - notifications.toasts.addDanger( +function handleError(core: CoreStart, err: Error): Rx.Observable { + core.notifications.toasts.addDanger( getGeneralErrorToast( i18n.translate('xpack.reporting.publicNotifier.pollingErrorMessage', { defaultMessage: 'Reporting notifier error!', }), err, - theme + core ) ); window.console.error(err); @@ -63,12 +59,7 @@ function handleError( export class ReportingNotifierStreamHandler { private jobCompletionNotifications = jobCompletionNotifications(); - constructor( - private notifications: NotificationsSetup, - private apiClient: ReportingAPIClient, - private theme: ThemeServiceStart, - private docLinks: DocLinksStart - ) {} + constructor(private apiClient: ReportingAPIClient, private core: CoreStart) {} public startPolling(interval: number, stop$: Rx.Observable) { Rx.timer(0, interval) @@ -81,7 +72,7 @@ export class ReportingNotifierStreamHandler { catchError((err) => { // eslint-disable-next-line no-console console.error(err); - return handleError(err, this.notifications, this.theme); + return handleError(this.core, err); }) ) .subscribe(); @@ -94,10 +85,10 @@ export class ReportingNotifierStreamHandler { completed: completedJobs, failed: failedJobs, }: JobSummarySet): Rx.Observable { - const notifications = this.notifications; + const notifications = this.core.notifications; const apiClient = this.apiClient; - const theme = this.theme; - const docLinks = this.docLinks; + const core = this.core; + const docLinks = this.core.docLinks; const getManagementLink = apiClient.getManagementLink.bind(apiClient); const getDownloadLink = apiClient.getDownloadLink.bind(apiClient); @@ -108,22 +99,22 @@ export class ReportingNotifierStreamHandler { for (const job of completedJobs ?? []) { if (job.csvContainsFormulas) { notifications.toasts.addWarning( - getWarningFormulasToast(job, getManagementLink, getDownloadLink, theme), + getWarningFormulasToast(job, getManagementLink, getDownloadLink, core), completedOptions ); } else if (job.maxSizeReached) { notifications.toasts.addWarning( - getWarningMaxSizeToast(job, getManagementLink, getDownloadLink, theme), + getWarningMaxSizeToast(job, getManagementLink, getDownloadLink, core), completedOptions ); } else if (job.status === JOB_STATUS.WARNINGS) { notifications.toasts.addWarning( - getWarningToast(job, getManagementLink, getDownloadLink, theme), + getWarningToast(job, getManagementLink, getDownloadLink, core), completedOptions ); } else { notifications.toasts.addSuccess( - getSuccessToast(job, getManagementLink, getDownloadLink, theme), + getSuccessToast(job, getManagementLink, getDownloadLink, core), completedOptions ); } @@ -132,8 +123,8 @@ export class ReportingNotifierStreamHandler { // no download link available for (const job of failedJobs ?? []) { const errorText = await apiClient.getError(job.id); - this.notifications.toasts.addDanger( - getFailureToast(errorText, job, getManagementLink, theme, docLinks) + notifications.toasts.addDanger( + getFailureToast(errorText, job, getManagementLink, docLinks, core) ); } return { completed: completedJobs, failed: failedJobs }; @@ -178,13 +169,13 @@ export class ReportingNotifierStreamHandler { }), catchError((err) => { // show connection refused toast - this.notifications.toasts.addDanger( + this.core.notifications.toasts.addDanger( getGeneralErrorToast( i18n.translate('xpack.reporting.publicNotifier.httpErrorMessage', { defaultMessage: 'Could not check Reporting job status!', }), err, - this.theme + this.core ) ); window.console.error(err); diff --git a/x-pack/plugins/reporting/public/management/mount_management_section.tsx b/x-pack/plugins/reporting/public/management/mount_management_section.tsx index 1e81119439e20..4352557e57617 100644 --- a/x-pack/plugins/reporting/public/management/mount_management_section.tsx +++ b/x-pack/plugins/reporting/public/management/mount_management_section.tsx @@ -10,11 +10,10 @@ import { render, unmountComponentAtNode } from 'react-dom'; import type { CoreStart } from '@kbn/core/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { I18nProvider } from '@kbn/i18n-react'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import type { LicensingPluginStart } from '@kbn/licensing-plugin/public'; import type { ManagementAppMountParams } from '@kbn/management-plugin/public'; -import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import type { ClientConfigType } from '@kbn/reporting-public'; import type { SharePluginStart } from '@kbn/share-plugin/public'; import { @@ -44,25 +43,23 @@ export async function mountManagementSection( }; render( - - - - - - - - - - - , + + + + + + + + + , params.element ); diff --git a/x-pack/plugins/reporting/public/mocks.ts b/x-pack/plugins/reporting/public/mocks.ts index 34aa311e54822..b1a447f48a8c0 100644 --- a/x-pack/plugins/reporting/public/mocks.ts +++ b/x-pack/plugins/reporting/public/mocks.ts @@ -5,6 +5,7 @@ * 2.0. */ +import * as Rx from 'rxjs'; import { coreMock } from '@kbn/core/public/mocks'; import { getSharedComponents } from '@kbn/reporting-public/share'; import { ReportingAPIClient } from '@kbn/reporting-public/reporting_api_client'; @@ -17,7 +18,7 @@ const createSetupContract = (): Setup => { const apiClient = new ReportingAPIClient(coreSetup.http, coreSetup.uiSettings, '7.15.0'); return { usesUiCapabilities: jest.fn().mockImplementation(() => true), - components: getSharedComponents(coreSetup, apiClient), + components: getSharedComponents(apiClient, Rx.from(coreSetup.getStartServices())), }; }; diff --git a/x-pack/plugins/reporting/public/notifier/general_error.tsx b/x-pack/plugins/reporting/public/notifier/general_error.tsx index a3a18edd454d7..3764c5b6e8478 100644 --- a/x-pack/plugins/reporting/public/notifier/general_error.tsx +++ b/x-pack/plugins/reporting/public/notifier/general_error.tsx @@ -5,16 +5,16 @@ * 2.0. */ -import React from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import { ThemeServiceStart, ToastInput } from '@kbn/core/public'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { CoreStart, ToastInput } from '@kbn/core/public'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { toMountPoint } from '@kbn/react-kibana-mount'; +import React from 'react'; export const getGeneralErrorToast = ( errorText: string, err: Error, - theme: ThemeServiceStart + core: CoreStart ): ToastInput => ({ text: toMountPoint( <> @@ -29,7 +29,7 @@ export const getGeneralErrorToast = ( defaultMessage="Try refreshing the page." /> , - { theme$: theme.theme$ } + core ), iconType: undefined, }); diff --git a/x-pack/plugins/reporting/public/notifier/job_failure.tsx b/x-pack/plugins/reporting/public/notifier/job_failure.tsx index e5c6f06413bdf..c8f44931c2940 100644 --- a/x-pack/plugins/reporting/public/notifier/job_failure.tsx +++ b/x-pack/plugins/reporting/public/notifier/job_failure.tsx @@ -8,8 +8,8 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import React from 'react'; -import { DocLinksStart, ThemeServiceStart, ToastInput } from '@kbn/core/public'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { CoreStart, DocLinksStart, ToastInput } from '@kbn/core/public'; +import { toMountPoint } from '@kbn/react-kibana-mount'; import * as errors from '@kbn/reporting-common/errors'; import { ManagementLinkFn } from '@kbn/reporting-common/types'; import { sharedI18nTexts } from '../shared_i18n_texts'; @@ -19,8 +19,8 @@ export const getFailureToast = ( errorText: string, job: JobSummary, getManagmenetLink: ManagementLinkFn, - theme: ThemeServiceStart, - docLinks: DocLinksStart + docLinks: DocLinksStart, + core: CoreStart ): ToastInput => { return { title: toMountPoint( @@ -29,7 +29,7 @@ export const getFailureToast = ( defaultMessage="Cannot create {reportType} report for '{reportObjectTitle}'." values={{ reportType: job.jobtype, reportObjectTitle: job.title }} />, - { theme$: theme.theme$ } + core ), text: toMountPoint( <> @@ -60,7 +60,7 @@ export const getFailureToast = ( />

, - { theme$: theme.theme$ } + core ), iconType: undefined, 'data-test-subj': 'completeReportFailure', diff --git a/x-pack/plugins/reporting/public/notifier/job_success.tsx b/x-pack/plugins/reporting/public/notifier/job_success.tsx index 00b08ed2413d9..ae721f675f605 100644 --- a/x-pack/plugins/reporting/public/notifier/job_success.tsx +++ b/x-pack/plugins/reporting/public/notifier/job_success.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import { ThemeServiceStart, ToastInput } from '@kbn/core/public'; +import { CoreStart, ToastInput } from '@kbn/core/public'; import { FormattedMessage } from '@kbn/i18n-react'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { toMountPoint } from '@kbn/react-kibana-mount'; import { JobId } from '@kbn/reporting-common/types'; import React from 'react'; import { JobSummary } from '../types'; @@ -18,7 +18,7 @@ export const getSuccessToast = ( job: JobSummary, getReportLink: () => string, getDownloadLink: (jobId: JobId) => string, - theme: ThemeServiceStart + core: CoreStart ): ToastInput => ({ title: toMountPoint( , - { theme$: theme.theme$ } + core ), color: 'success', text: toMountPoint( @@ -36,7 +36,7 @@ export const getSuccessToast = (

, - { theme$: theme.theme$ } + core ), 'data-test-subj': 'completeReportSuccess', }); diff --git a/x-pack/plugins/reporting/public/notifier/job_warning.tsx b/x-pack/plugins/reporting/public/notifier/job_warning.tsx index 6751eb76ab073..34c73e561f976 100644 --- a/x-pack/plugins/reporting/public/notifier/job_warning.tsx +++ b/x-pack/plugins/reporting/public/notifier/job_warning.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import { ThemeServiceStart, ToastInput } from '@kbn/core/public'; +import { CoreStart, ToastInput } from '@kbn/core/public'; import { FormattedMessage } from '@kbn/i18n-react'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { toMountPoint } from '@kbn/react-kibana-mount'; import { JobId } from '@kbn/reporting-common/types'; import React from 'react'; import { JobSummary } from '../types'; @@ -18,7 +18,7 @@ export const getWarningToast = ( job: JobSummary, getReportLink: () => string, getDownloadLink: (jobId: JobId) => string, - theme: ThemeServiceStart + core: CoreStart ): ToastInput => ({ title: toMountPoint( , - { theme$: theme.theme$ } + core ), text: toMountPoint( <> @@ -35,7 +35,7 @@ export const getWarningToast = (

, - { theme$: theme.theme$ } + core ), 'data-test-subj': 'completeReportWarning', }); diff --git a/x-pack/plugins/reporting/public/notifier/job_warning_formulas.tsx b/x-pack/plugins/reporting/public/notifier/job_warning_formulas.tsx index 4cf9f3f655cc1..2f8c39b904666 100644 --- a/x-pack/plugins/reporting/public/notifier/job_warning_formulas.tsx +++ b/x-pack/plugins/reporting/public/notifier/job_warning_formulas.tsx @@ -7,9 +7,9 @@ import React from 'react'; -import { ThemeServiceStart, ToastInput } from '@kbn/core/public'; +import { CoreStart, ToastInput } from '@kbn/core/public'; import { FormattedMessage } from '@kbn/i18n-react'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { toMountPoint } from '@kbn/react-kibana-mount'; import { JobId } from '@kbn/reporting-common/types'; import { DownloadButton } from './job_download_button'; @@ -20,7 +20,7 @@ export const getWarningFormulasToast = ( job: JobSummary, getReportLink: () => string, getDownloadLink: (jobId: JobId) => string, - theme: ThemeServiceStart + core: CoreStart ): ToastInput => ({ title: toMountPoint( , - { theme$: theme.theme$ } + core ), text: toMountPoint( <> @@ -44,7 +44,7 @@ export const getWarningFormulasToast = (

, - { theme$: theme.theme$ } + core ), 'data-test-subj': 'completeReportCsvFormulasWarning', }); diff --git a/x-pack/plugins/reporting/public/notifier/job_warning_max_size.tsx b/x-pack/plugins/reporting/public/notifier/job_warning_max_size.tsx index 54c7628242067..b87547669d704 100644 --- a/x-pack/plugins/reporting/public/notifier/job_warning_max_size.tsx +++ b/x-pack/plugins/reporting/public/notifier/job_warning_max_size.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import { ThemeServiceStart, ToastInput } from '@kbn/core/public'; +import { CoreStart, ToastInput } from '@kbn/core/public'; import { FormattedMessage } from '@kbn/i18n-react'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { toMountPoint } from '@kbn/react-kibana-mount'; import type { JobId } from '@kbn/reporting-common/types'; import React from 'react'; import { JobSummary } from '../types'; @@ -18,7 +18,7 @@ export const getWarningMaxSizeToast = ( job: JobSummary, getReportLink: () => string, getDownloadLink: (jobId: JobId) => string, - theme: ThemeServiceStart + core: CoreStart ): ToastInput => ({ title: toMountPoint( , - { theme$: theme.theme$ } + core ), text: toMountPoint( <> @@ -41,7 +41,7 @@ export const getWarningMaxSizeToast = (

, - { theme$: theme.theme$ } + core ), 'data-test-subj': 'completeReportMaxSizeWarning', }); diff --git a/x-pack/plugins/reporting/public/plugin.ts b/x-pack/plugins/reporting/public/plugin.ts index dddf18003fb94..cb606ca35152f 100644 --- a/x-pack/plugins/reporting/public/plugin.ts +++ b/x-pack/plugins/reporting/public/plugin.ts @@ -5,16 +5,9 @@ * 2.0. */ -import { from, ReplaySubject } from 'rxjs'; +import { from, map, type Observable, ReplaySubject } from 'rxjs'; -import { - CoreSetup, - CoreStart, - HttpSetup, - IUiSettingsClient, - Plugin, - PluginInitializerContext, -} from '@kbn/core/public'; +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public'; import type { HomePublicPluginSetup, HomePublicPluginStart } from '@kbn/home-plugin/public'; @@ -39,6 +32,7 @@ import { import { ReportingCsvPanelAction } from '@kbn/reporting-csv-share-panel'; import type { ReportingSetup, ReportingStart } from '.'; import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_handler'; +import { StartServices } from './types'; export interface ReportingPublicPluginSetupDependencies { home: HomePublicPluginSetup; @@ -57,6 +51,8 @@ export interface ReportingPublicPluginStartDependencies { share: SharePluginStart; } +type StartServices$ = Observable; + /** * @internal * @implements Plugin @@ -81,29 +77,18 @@ export class ReportingPublicPlugin }); private config: ClientConfigType; private contract?: ReportingSetup; + private startServices$?: StartServices$; constructor(initializerContext: PluginInitializerContext) { this.config = initializerContext.config.get(); this.kibanaVersion = initializerContext.env.packageInfo.version; } - /* - * Use a single instance of ReportingAPIClient for all the reporting code - */ - private getApiClient(http: HttpSetup, uiSettings: IUiSettingsClient) { - if (!this.apiClient) { - this.apiClient = new ReportingAPIClient(http, uiSettings, this.kibanaVersion); - } - return this.apiClient; - } - - private getContract(core?: CoreSetup) { - if (core) { - this.contract = { - usesUiCapabilities: () => this.config.roles?.enabled === false, - components: getSharedComponents(core, this.getApiClient(core.http, core.uiSettings)), - }; - } + private getContract(apiClient: ReportingAPIClient, startServices$: StartServices$) { + this.contract = { + usesUiCapabilities: () => this.config.roles?.enabled === false, + components: getSharedComponents(apiClient, startServices$), + }; if (!this.contract) { throw new Error(`Setup error in Reporting plugin!`); @@ -116,7 +101,7 @@ export class ReportingPublicPlugin core: CoreSetup, setupDeps: ReportingPublicPluginSetupDependencies ) { - const { getStartServices, uiSettings } = core; + const { getStartServices } = core; const { home: homeSetup, management: managementSetup, @@ -125,10 +110,25 @@ export class ReportingPublicPlugin uiActions: uiActionsSetup, } = setupDeps; - const startServices$ = from(getStartServices()); + const startServices$: Observable = from(getStartServices()).pipe( + map(([services, ...rest]) => { + return [ + { + application: services.application, + analytics: services.analytics, + i18n: services.i18n, + theme: services.theme, + notifications: services.notifications, + uiSettings: services.uiSettings, + }, + ...rest, + ]; + }) + ); const usesUiCapabilities = !this.config.roles.enabled; - const apiClient = this.getApiClient(core.http, core.uiSettings); + const apiClient = new ReportingAPIClient(core.http, core.uiSettings, this.kibanaVersion); + this.apiClient = apiClient; homeSetup.featureCatalogue.register({ id: 'reporting', @@ -204,20 +204,15 @@ export class ReportingPublicPlugin }) ); - const reportingStart = this.getContract(core); - const { toasts } = core.notifications; - - startServices$.subscribe(([{ application, i18n: i18nStart }, { licensing }]) => { + startServices$.subscribe(([{ application }, { licensing }]) => { licensing.license$.subscribe((license) => { shareSetup.register( reportingCsvShareProvider({ apiClient, - toasts, - uiSettings, license, application, usesUiCapabilities, - theme: core.theme, + startServices$, }) ); if (this.config.export_types.pdf.enabled || this.config.export_types.png.enabled) { @@ -225,12 +220,10 @@ export class ReportingPublicPlugin shareSetup.register( reportingScreenshotShareProvider({ apiClient, - toasts, - uiSettings, license, application, usesUiCapabilities, - theme: core.theme, + startServices$, }) ); } @@ -238,12 +231,10 @@ export class ReportingPublicPlugin shareSetup.register( reportingCsvShareModalProvider({ apiClient, - uiSettings, license, application, usesUiCapabilities, - theme: core.theme, - i18n: i18nStart, + startServices$, }) ); @@ -251,29 +242,27 @@ export class ReportingPublicPlugin shareSetup.register( reportingExportModalProvider({ apiClient, - uiSettings, license, application, usesUiCapabilities, - theme: core.theme, - i18n: i18nStart, + startServices$, }) ); } } }); }); - return reportingStart; + + this.startServices$ = startServices$; + return this.getContract(apiClient, startServices$); } public start(core: CoreStart) { - const { notifications, docLinks } = core; - const apiClient = this.getApiClient(core.http, core.uiSettings); - const streamHandler = new StreamHandler(notifications, apiClient, core.theme, docLinks); + const streamHandler = new StreamHandler(this.apiClient!, core); const interval = durationToNumber(this.config.poll.jobsRefresh.interval); streamHandler.startPolling(interval, this.stop$); - return this.getContract(); + return this.getContract(this.apiClient!, this.startServices$!); } public stop() { diff --git a/x-pack/plugins/reporting/public/types.ts b/x-pack/plugins/reporting/public/types.ts index d5af032db617c..9ba50435471ab 100644 --- a/x-pack/plugins/reporting/public/types.ts +++ b/x-pack/plugins/reporting/public/types.ts @@ -5,8 +5,29 @@ * 2.0. */ +import type { CoreStart } from '@kbn/core/public'; import { JOB_STATUS } from '@kbn/reporting-common'; import type { JobId, ReportOutput, ReportSource, TaskRunResult } from '@kbn/reporting-common/types'; +import { ReportingPublicPluginStartDependencies } from './plugin'; + +/* + * Required services for mounting React components + */ +export type StartServices = [ + Pick< + CoreStart, + // required for modules that render React + | 'analytics' + | 'i18n' + | 'theme' + // used extensively in Reporting plugin + | 'application' + | 'notifications' + | 'uiSettings' + >, + ReportingPublicPluginStartDependencies, + unknown +]; /* * Notifier Toasts diff --git a/x-pack/plugins/reporting/tsconfig.json b/x-pack/plugins/reporting/tsconfig.json index 867233ad463b0..e0f781d28f62c 100644 --- a/x-pack/plugins/reporting/tsconfig.json +++ b/x-pack/plugins/reporting/tsconfig.json @@ -43,13 +43,14 @@ "@kbn/reporting-export-types-png", "@kbn/reporting-export-types-pdf-common", "@kbn/reporting-export-types-csv-common", - "@kbn/react-kibana-context-theme", "@kbn/reporting-export-types-png-common", "@kbn/reporting-mocks-server", "@kbn/core-http-request-handler-context-server", "@kbn/reporting-public", "@kbn/analytics-client", "@kbn/reporting-csv-share-panel", + "@kbn/react-kibana-context-render", + "@kbn/react-kibana-mount", ], "exclude": [ "target/**/*", From d13d89ecb8b948e9a8ca6bba193c5af5ec587cfc Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Thu, 25 Apr 2024 12:06:16 -0400 Subject: [PATCH 10/20] [Investigations] - Add unified components to eql tab (#180972) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR introduces the unified table and field list to the correlations tab as seen in the snapshots below. **As of this PR, items that are not working** 1. Table row height controls 2. Expandable flyout integration 3. Leading cell actions (pinning, notes, row actions, analyzer, session view) **Changes in this PR:** Sequence Highlighting: image Building block highlighting: Screenshot 2024-04-17 at 1 11 13 PM To test: 1. Add `xpack.securitySolution.enableExperimental: [unifiedComponentsInTimelineEnabled]` to your `kibana.dev.yml` 2. Generate test data (endpoint data is fine) 5. Go to the correlations tab and enter this query to see default events/alerts that should have no highlighting ```any where true``` 6. Enter this query to see a generic sequence ``` sequence [any where true] [any where true] ``` You can also do something like ``` sequence [any where host.name=={HOST NAME VALUE HERE}] [any where true] ``` 7. You can also create a correlation rule using any of the above queries to generate building block alerts, and then query those alerts in the correlations tab as well --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../timeline/body/unified_timeline_body.tsx | 2 + .../timeline/tabs/eql/header/index.test.tsx | 64 +++ .../timeline/tabs/eql/header/index.tsx | 68 +++ .../components/timeline/tabs/eql/index.tsx | 327 ++++++------ .../timeline/tabs/query/header/index.test.tsx | 5 +- .../timeline/tabs/query/header/index.tsx | 111 ++-- .../components/timeline/tabs/query/index.tsx | 158 ++---- .../use_timeline_columns.test.ts.snap | 497 ++++++++++++++++++ .../tabs/shared/use_timeline_columns.test.ts | 152 ++++++ .../tabs/shared/use_timeline_columns.ts | 79 +++ .../components/timeline/tabs/shared/utils.ts | 18 +- ...stom_timeline_data_grid_body.test.tsx.snap | 4 +- .../custom_timeline_data_grid_body.tsx | 16 +- .../unified_components/data_table/index.tsx | 18 +- .../use_get_event_type_row_classname.test.ts | 57 ++ .../use_get_event_type_row_classname.ts | 34 ++ .../timeline/unified_components/index.tsx | 3 + .../timeline/unified_components/styles.tsx | 20 +- 18 files changed, 1291 insertions(+), 342 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/header/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/header/index.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/__snapshots__/use_timeline_columns.test.ts.snap create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.test.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/use_get_event_type_row_classname.test.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/use_get_event_type_row_classname.ts diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.tsx index 1a131871dc4fe..21713d10b61f1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.tsx @@ -22,6 +22,7 @@ export interface UnifiedTimelineBodyProps extends ComponentProps { const { header, + isSortEnabled, pageInfo, columns, rowRenderers, @@ -68,6 +69,7 @@ export const UnifiedTimelineBody = (props: UnifiedTimelineBodyProps) => { { + const props = { + activeTab: TimelineTabs.eql, + timelineId: TimelineId.test, + timelineFullScreen: false, + setTimelineFullScreen: jest.fn(), + } as EqlTabHeaderProps; + + describe('rendering', () => { + beforeEach(() => { + render( + + + + ); + }); + + test('should render the eql query bar', async () => { + expect(screen.getByTestId('EqlQueryBarTimeline')).toBeInTheDocument(); + }); + + test('should render the sourcerer selector', async () => { + expect(screen.getByTestId('timeline-sourcerer-popover')).toBeInTheDocument(); + }); + + test('should render the date picker', async () => { + expect(screen.getByTestId('superDatePickerToggleQuickMenuButton')).toBeInTheDocument(); + }); + }); + + describe('full screen', () => { + beforeEach(() => { + const updatedProps = { + ...props, + timelineFullScreen: true, + } as EqlTabHeaderProps; + + render( + + + + ); + }); + + test('should render the exit full screen component', async () => { + expect(screen.getByTestId('exit-full-screen')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/header/index.tsx new file mode 100644 index 0000000000000..4ba340cb9f5cd --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/header/index.tsx @@ -0,0 +1,68 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { memo } from 'react'; + +import { InputsModelId } from '../../../../../../common/store/inputs/constants'; +import { TimelineTabs } from '../../../../../../../common/types/timeline'; +import { ExitFullScreen } from '../../../../../../common/components/exit_full_screen'; +import { SuperDatePicker } from '../../../../../../common/components/super_date_picker'; +import { SourcererScopeName } from '../../../../../../common/store/sourcerer/model'; +import { TimelineDatePickerLock } from '../../../date_picker_lock'; +import type { TimelineFullScreen } from '../../../../../../common/containers/use_full_screen'; +import { EqlQueryBarTimeline } from '../../../query_bar/eql'; +import { Sourcerer } from '../../../../../../common/components/sourcerer'; +import { StyledEuiFlyoutHeader, TabHeaderContainer } from '../../shared/layout'; + +export type EqlTabHeaderProps = { + activeTab: TimelineTabs; + timelineId: string; +} & TimelineFullScreen; + +export const EqlTabHeader = memo( + ({ activeTab, setTimelineFullScreen, timelineFullScreen, timelineId }: EqlTabHeaderProps) => ( + <> + + + + + {timelineFullScreen && setTimelineFullScreen != null && ( + + )} + + {activeTab === TimelineTabs.eql && ( + + )} + + + + + + + + + + + + + + + + ) +); + +EqlTabHeader.displayName = 'EqlTabHeader'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx index d11fa0e84b7d7..83a7487212e61 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx @@ -15,20 +15,17 @@ import deepEqual from 'fast-deep-equal'; import { InPortal } from 'react-reverse-portal'; import { DataLoadingState } from '@kbn/unified-data-table'; -import type { ControlColumnProps } from '../../../../../../common/types'; import { InputsModelId } from '../../../../../common/store/inputs/constants'; +import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; +import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import { timelineActions, timelineSelectors } from '../../../../store'; import { useTimelineEvents } from '../../../../containers'; -import { defaultHeaders } from '../../body/column_headers/default_headers'; import { StatefulBody } from '../../body'; import { Footer, footerHeight } from '../../footer'; import { calculateTotalPages } from '../../helpers'; import { TimelineRefetch } from '../../refetch_timeline'; import type { ToggleDetailPanel } from '../../../../../../common/types/timeline'; -import { TimelineTabs } from '../../../../../../common/types/timeline'; -import { requiredFieldsForActions } from '../../../../../detections/components/alerts_table/default_config'; -import { ExitFullScreen } from '../../../../../common/components/exit_full_screen'; -import { SuperDatePicker } from '../../../../../common/components/super_date_picker'; +import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline'; import { EventDetailsWidthProvider } from '../../../../../common/components/events_viewer/event_details_width_context'; import type { inputsModel, State } from '../../../../../common/store'; import { inputsSelectors } from '../../../../../common/store'; @@ -37,34 +34,29 @@ import { timelineDefaults } from '../../../../store/defaults'; import { useSourcererDataView } from '../../../../../common/containers/sourcerer'; import { useEqlEventsCountPortal } from '../../../../../common/hooks/use_timeline_events_count'; import type { TimelineModel } from '../../../../store/model'; -import { TimelineDatePickerLock } from '../../date_picker_lock'; import { useTimelineFullScreen } from '../../../../../common/containers/use_full_screen'; import { DetailsPanel } from '../../../side_panel'; -import { EqlQueryBarTimeline } from '../../query_bar/eql'; -import { getDefaultControlColumn } from '../../body/control_columns'; -import type { Sort } from '../../body/sort'; -import { Sourcerer } from '../../../../../common/components/sourcerer'; -import { useLicense } from '../../../../../common/hooks/use_license'; -import { HeaderActions } from '../../../../../common/components/header_actions/header_actions'; import { EventsCountBadge, FullWidthFlexGroup, ScrollableFlexItem, - StyledEuiFlyoutHeader, StyledEuiFlyoutBody, StyledEuiFlyoutFooter, VerticalRule, - TabHeaderContainer, } from '../shared/layout'; -import { EMPTY_EVENTS, isTimerangeSame } from '../shared/utils'; +import { + TIMELINE_EMPTY_EVENTS, + isTimerangeSame, + timelineEmptyTrailingControlColumns, + TIMELINE_NO_SORTING, +} from '../shared/utils'; import type { TimelineTabCommonProps } from '../shared/types'; +import { UnifiedTimelineBody } from '../../body/unified_timeline_body'; +import { EqlTabHeader } from './header'; +import { useTimelineColumns } from '../shared/use_timeline_columns'; export type Props = TimelineTabCommonProps & PropsFromRedux; -const NO_SORTING: Sort[] = []; - -const trailingControlColumns: ControlColumnProps[] = []; // stable reference - export const EqlTabContentComponent: React.FC = ({ activeTab, columns, @@ -93,39 +85,46 @@ export const EqlTabContentComponent: React.FC = ({ runtimeMappings, selectedPatterns, } = useSourcererDataView(SourcererScopeName.timeline); + const { augmentedColumnHeaders, getTimelineQueryFieldsFromColumns, leadingControlColumns } = + useTimelineColumns(columns); - const isEnterprisePlus = useLicense().isEnterprise(); - const ACTION_BUTTON_COUNT = isEnterprisePlus ? 6 : 5; + const unifiedComponentsInTimelineEnabled = useIsExperimentalFeatureEnabled( + 'unifiedComponentsInTimelineEnabled' + ); - const isBlankTimeline: boolean = isEmpty(eqlQuery); + const getManageTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - const canQueryTimeline = () => - loadingSourcerer != null && - !loadingSourcerer && - !isEmpty(start) && - !isEmpty(end) && - !isBlankTimeline; + const currentTimeline = useDeepEqualSelector((state) => + getManageTimeline(state, timelineId ?? TimelineId.active) + ); - const getTimelineQueryFields = () => { - const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; - const columnFields = columnsHeader.map((c) => c.id); + const { sampleSize } = currentTimeline; - return [...columnFields, ...requiredFieldsForActions]; - }; + const isBlankTimeline: boolean = isEmpty(eqlQuery); + + const canQueryTimeline = useCallback( + () => + loadingSourcerer != null && + !loadingSourcerer && + !isEmpty(start) && + !isEmpty(end) && + !isBlankTimeline, + [end, isBlankTimeline, loadingSourcerer, start] + ); const [ - queryLoadingState, + dataLoadingState, { events, inspect, totalCount, pageInfo, loadPage, refreshedAt, refetch }, ] = useTimelineEvents({ dataViewId, endDate: end, eqlOptions: restEqlOption, - fields: getTimelineQueryFields(), + fields: getTimelineQueryFieldsFromColumns(), filterQuery: eqlQuery ?? '', id: timelineId, indexNames: selectedPatterns, language: 'eql', - limit: itemsPerPage, + limit: unifiedComponentsInTimelineEnabled ? sampleSize : itemsPerPage, runtimeMappings, skip: !canQueryTimeline(), startDate: start, @@ -134,9 +133,9 @@ export const EqlTabContentComponent: React.FC = ({ const isQueryLoading = useMemo( () => - queryLoadingState === DataLoadingState.loading || - queryLoadingState === DataLoadingState.loadingMore, - [queryLoadingState] + dataLoadingState === DataLoadingState.loading || + dataLoadingState === DataLoadingState.loadingMore, + [dataLoadingState] ); const handleOnPanelClosed = useCallback(() => { @@ -152,136 +151,142 @@ export const EqlTabContentComponent: React.FC = ({ ); }, [loadingSourcerer, timelineId, isQueryLoading, dispatch]); - const leadingControlColumns = useMemo( - () => - getDefaultControlColumn(ACTION_BUTTON_COUNT).map((x) => ({ - ...x, - headerCellRender: HeaderActions, - })), - [ACTION_BUTTON_COUNT] + const unifiedHeader = useMemo( + () => ( + + + + ), + [activeTab, setTimelineFullScreen, timelineFullScreen, timelineId] ); return ( <> - - {totalCount >= 0 ? {totalCount} : null} - - - - - - - - - - {timelineFullScreen && setTimelineFullScreen != null && ( - + + {totalCount >= 0 ? {totalCount} : null} + + + + + + + + ) : ( + <> + + {totalCount >= 0 ? {totalCount} : null} + + + + + + + + + + - )} - - {activeTab === TimelineTabs.eql && ( - + + + + {!isBlankTimeline && ( +