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`
After:
`https://console.qa.cld.elstc.co/projects/security/:id`
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.
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:
Building block highlighting:
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 && (
+
)}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ {showExpandedDetails && (
+ <>
+
+
+
-
-
-
- {!isBlankTimeline && (
-
- )}
-
-
-
-
-
- {showExpandedDetails && (
- <>
-
-
-
-
- >
- )}
-
+
+ >
+ )}
+
+ >
+ )}
>
);
};
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/header/index.test.tsx
index a69e1de6c898d..526963cd78607 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/header/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/header/index.test.tsx
@@ -17,7 +17,7 @@ import { useMountAppended } from '../../../../../../common/utils/use_mount_appen
import { QueryTabHeader } from '.';
import { TimelineStatus, TimelineType } from '../../../../../../../common/api/timeline';
import { waitFor } from '@testing-library/react';
-import { TimelineId } from '../../../../../../../common/types';
+import { TimelineId, TimelineTabs } from '../../../../../../../common/types';
const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings;
@@ -32,6 +32,9 @@ describe('Header', () => {
return wrapper;
};
const props = {
+ activeTab: TimelineTabs.query,
+ showEventsCountBadge: true,
+ totalCount: 1,
browserFields: {},
dataProviders: mockDataProviders,
filterManager: new FilterManager(mockUiSettingsForFilterManager),
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/header/index.tsx
index 7e798865c86b6..a8abf4e9f9f1e 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/header/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/header/index.tsx
@@ -8,26 +8,34 @@
import { EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { useMemo } from 'react';
import type { FilterManager } from '@kbn/data-plugin/public';
-
+import { InPortal } from 'react-reverse-portal';
import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid';
import styled from '@emotion/styled';
import { euiThemeVars } from '@kbn/ui-theme';
-
+import { useIsExperimentalFeatureEnabled } from '../../../../../../common/hooks/use_experimental_features';
+import { useTimelineEventsCountPortal } from '../../../../../../common/hooks/use_timeline_events_count';
+import { useTimelineFullScreen } from '../../../../../../common/containers/use_full_screen';
+import { ExitFullScreen } from '../../../../../../common/components/exit_full_screen';
import type { TimelineStatusLiteralWithNull } from '../../../../../../../common/api/timeline';
import { TimelineStatus, TimelineType } from '../../../../../../../common/api/timeline';
+import type { TimelineTabs } from '../../../../../../../common/types/timeline';
import { timelineSelectors } from '../../../../../store';
import { useDeepEqualSelector } from '../../../../../../common/hooks/use_selector';
import { timelineDefaults } from '../../../../../store/defaults';
import * as i18n from './translations';
import { StatefulSearchOrFilter } from '../../../search_or_filter';
import { DataProviders } from '../../../data_providers';
+import { StyledEuiFlyoutHeader, EventsCountBadge, TabHeaderContainer } from '../../shared/layout';
interface Props {
+ activeTab: TimelineTabs;
filterManager: FilterManager;
show: boolean;
showCallOutUnauthorizedMsg: boolean;
+ showEventsCountBadge: boolean;
status: TimelineStatusLiteralWithNull;
timelineId: string;
+ totalCount: number;
}
const DataProvidersContainer = styled.div<{ $shouldShowQueryBuilder: boolean }>`
@@ -50,12 +58,20 @@ const DataProvidersContainer = styled.div<{ $shouldShowQueryBuilder: boolean }>`
`;
const QueryTabHeaderComponent: React.FC = ({
+ activeTab,
filterManager,
show,
showCallOutUnauthorizedMsg,
status,
timelineId,
+ showEventsCountBadge,
+ totalCount,
}) => {
+ const unifiedComponentsInTimelineEnabled = useIsExperimentalFeatureEnabled(
+ 'unifiedComponentsInTimelineEnabled'
+ );
+ const { portalNode: timelineEventsCountPortalNode } = useTimelineEventsCountPortal();
+ const { setTimelineFullScreen, timelineFullScreen } = useTimelineFullScreen();
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const getIsDataProviderVisible = useMemo(
@@ -74,41 +90,64 @@ const QueryTabHeaderComponent: React.FC = ({
const shouldShowQueryBuilder = isDataProviderVisible || timelineType === TimelineType.template;
return (
-
-
-
-
- {showCallOutUnauthorizedMsg && (
-
-
-
- )}
- {status === TimelineStatus.immutable && (
-
-
+
+
+ {showEventsCountBadge ? {totalCount} : null}
+
+
+ {!unifiedComponentsInTimelineEnabled &&
+ timelineFullScreen &&
+ setTimelineFullScreen != null && (
+
+
+
+
+
+ )}
+
+
+
+
+
+
+ {showCallOutUnauthorizedMsg && (
+
+
+
+ )}
+ {status === TimelineStatus.immutable && (
+
+
+
+ )}
+ {show ? (
+
+
+
+ ) : null}
+
+
- )}
- {show ? (
-
-
-
- ) : null}
-
+
+
);
};
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx
index 472aa17a389d0..99491cdbbbfd7 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx
@@ -5,29 +5,22 @@
* 2.0.
*/
-import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { isEmpty } from 'lodash/fp';
import React, { useMemo, useEffect, useCallback } from 'react';
import type { Dispatch } from 'redux';
import type { ConnectedProps } from 'react-redux';
import { connect, useDispatch } from 'react-redux';
import deepEqual from 'fast-deep-equal';
-import { InPortal } from 'react-reverse-portal';
-
import { getEsQueryConfig } from '@kbn/data-plugin/common';
import { DataLoadingState } from '@kbn/unified-data-table';
-import type { BrowserFields, ColumnHeaderOptions } from '@kbn/timelines-plugin/common';
-import memoizeOne from 'memoize-one';
import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
-import type { ControlColumnProps } from '../../../../../../common/types';
import { InputsModelId } from '../../../../../common/store/inputs/constants';
import { useInvalidFilterQuery } from '../../../../../common/hooks/use_invalid_filter_query';
import { timelineActions, timelineSelectors } from '../../../../store';
import type { Direction } from '../../../../../../common/search_strategy';
import { useTimelineEvents } from '../../../../containers';
import { useKibana } from '../../../../../common/lib/kibana';
-import { defaultHeaders } from '../../body/column_headers/default_headers';
import { StatefulBody } from '../../body';
import { Footer, footerHeight } from '../../footer';
import { QueryTabHeader } from './header';
@@ -39,42 +32,29 @@ import type {
ToggleDetailPanel,
} from '../../../../../../common/types/timeline';
import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
-import { requiredFieldsForActions } from '../../../../../detections/components/alerts_table/default_config';
import { EventDetailsWidthProvider } from '../../../../../common/components/events_viewer/event_details_width_context';
import type { inputsModel, State } from '../../../../../common/store';
import { inputsSelectors } from '../../../../../common/store';
import { SourcererScopeName } from '../../../../../common/store/sourcerer/model';
import { timelineDefaults } from '../../../../store/defaults';
import { useSourcererDataView } from '../../../../../common/containers/sourcerer';
-import { useTimelineEventsCountPortal } from '../../../../../common/hooks/use_timeline_events_count';
import type { TimelineModel } from '../../../../store/model';
-import { useTimelineFullScreen } from '../../../../../common/containers/use_full_screen';
import { DetailsPanel } from '../../../side_panel';
-import { ExitFullScreen } from '../../../../../common/components/exit_full_screen';
-import { getDefaultControlColumn } from '../../body/control_columns';
-import { useLicense } from '../../../../../common/hooks/use_license';
-import { HeaderActions } from '../../../../../common/components/header_actions/header_actions';
-import { defaultUdtHeaders } from '../../unified_components/default_headers';
import { UnifiedTimelineBody } from '../../body/unified_timeline_body';
-import { getColumnHeaders } from '../../body/column_headers/helpers';
import {
- StyledEuiFlyoutHeader,
- EventsCountBadge,
FullWidthFlexGroup,
ScrollableFlexItem,
StyledEuiFlyoutBody,
StyledEuiFlyoutFooter,
VerticalRule,
- TabHeaderContainer,
} from '../shared/layout';
-import { EMPTY_EVENTS, isTimerangeSame } from '../shared/utils';
+import {
+ TIMELINE_EMPTY_EVENTS,
+ isTimerangeSame,
+ timelineEmptyTrailingControlColumns,
+} from '../shared/utils';
import type { TimelineTabCommonProps } from '../shared/types';
-
-const memoizedGetColumnHeaders: (
- headers: ColumnHeaderOptions[],
- browserFields: BrowserFields,
- isEventRenderedView: boolean
-) => ColumnHeaderOptions[] = memoizeOne(getColumnHeaders);
+import { useTimelineColumns } from '../shared/use_timeline_columns';
const compareQueryProps = (prevProps: Props, nextProps: Props) =>
prevProps.kqlMode === nextProps.kqlMode &&
@@ -83,8 +63,6 @@ const compareQueryProps = (prevProps: Props, nextProps: Props) =>
export type Props = TimelineTabCommonProps & PropsFromRedux;
-const trailingControlColumns: ControlColumnProps[] = []; // stable reference
-
export const QueryTabContentComponent: React.FC = ({
activeTab,
columns,
@@ -111,8 +89,6 @@ export const QueryTabContentComponent: React.FC = ({
expandedDetail,
}) => {
const dispatch = useDispatch();
- const { portalNode: timelineEventsCountPortalNode } = useTimelineEventsCountPortal();
- const { setTimelineFullScreen, timelineFullScreen } = useTimelineFullScreen();
const {
browserFields,
dataViewId,
@@ -123,11 +99,14 @@ export const QueryTabContentComponent: React.FC = ({
// in order to include the exclude filters in the search that are not stored in the timeline
selectedPatterns,
} = useSourcererDataView(SourcererScopeName.timeline);
+ const {
+ augmentedColumnHeaders,
+ defaultColumns,
+ getTimelineQueryFieldsFromColumns,
+ leadingControlColumns,
+ } = useTimelineColumns(columns);
const { uiSettings, timelineFilterManager } = useKibana().services;
- const isEnterprisePlus = useLicense().isEnterprise();
- const ACTION_BUTTON_COUNT = isEnterprisePlus ? 6 : 5;
-
const unifiedComponentsInTimelineEnabled = useIsExperimentalFeatureEnabled(
'unifiedComponentsInTimelineEnabled'
);
@@ -185,24 +164,6 @@ export const QueryTabContentComponent: React.FC = ({
[combinedQueries, end, loadingSourcerer, start]
);
- const defaultColumns = useMemo(
- () => (unifiedComponentsInTimelineEnabled ? defaultUdtHeaders : defaultHeaders),
- [unifiedComponentsInTimelineEnabled]
- );
-
- const localColumns = useMemo(
- () => (isEmpty(columns) ? defaultColumns : columns),
- [columns, defaultColumns]
- );
-
- const augumentedColumnHeaders = memoizedGetColumnHeaders(localColumns, browserFields, false);
-
- const getTimelineQueryFields = () => {
- const columnFields = augumentedColumnHeaders.map((c) => c.id);
-
- return [...columnFields, ...requiredFieldsForActions];
- };
-
const timelineQuerySortField = sort.map(({ columnId, columnType, esTypes, sortDirection }) => ({
field: columnId,
direction: sortDirection as Direction,
@@ -225,7 +186,7 @@ export const QueryTabContentComponent: React.FC = ({
] = useTimelineEvents({
dataViewId,
endDate: end,
- fields: getTimelineQueryFields(),
+ fields: getTimelineQueryFieldsFromColumns(),
filterQuery: combinedQueries?.filterQuery,
id: timelineId,
indexNames: selectedPatterns,
@@ -256,79 +217,27 @@ export const QueryTabContentComponent: React.FC = ({
);
}, [loadingSourcerer, timelineId, isQueryLoading, dispatch]);
- const leadingControlColumns = useMemo(
- () =>
- getDefaultControlColumn(ACTION_BUTTON_COUNT).map((x) => ({
- ...x,
- headerCellRender: HeaderActions,
- })),
- [ACTION_BUTTON_COUNT]
- );
-
// NOTE: The timeline is blank after browser FORWARD navigation (after using back button to navigate to
// the previous page from the timeline), yet we still see total count. This is because the timeline
// is not getting refreshed when using browser navigation.
const showEventsCountBadge = !isBlankTimeline && totalCount >= 0;
- const header = useMemo(
- () => (
-
-
- {showEventsCountBadge ? {totalCount} : null}
-
-
- {!unifiedComponentsInTimelineEnabled &&
- timelineFullScreen &&
- setTimelineFullScreen != null && (
-
-
-
-
-
- )}
-
-
-
-
-
- {/* TODO: This is a temporary solution to hide the KPIs until lens components play nicely with timelines */}
- {/* https://github.com/elastic/kibana/issues/17156 */}
- {/* */}
- {/* */}
- {/* */}
-
-
- ),
- [
- activeTab,
- timelineFilterManager,
- show,
- showCallOutUnauthorizedMsg,
- status,
- timelineId,
- setTimelineFullScreen,
- timelineFullScreen,
- unifiedComponentsInTimelineEnabled,
- timelineEventsCountPortalNode,
- showEventsCountBadge,
- totalCount,
- ]
- );
-
if (unifiedComponentsInTimelineEnabled) {
return (
+ }
+ columns={augmentedColumnHeaders}
rowRenderers={rowRenderers}
timelineId={timelineId}
itemsPerPage={itemsPerPage}
@@ -362,7 +271,16 @@ export const QueryTabContentComponent: React.FC = ({
/>
- {header}
+
= ({
= ({
itemsPerPage,
})}
leadingControlColumns={leadingControlColumns}
- trailingControlColumns={trailingControlColumns}
+ trailingControlColumns={timelineEmptyTrailingControlColumns}
/>
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/__snapshots__/use_timeline_columns.test.ts.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/__snapshots__/use_timeline_columns.test.ts.snap
new file mode 100644
index 0000000000000..41e597f66051d
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/__snapshots__/use_timeline_columns.test.ts.snap
@@ -0,0 +1,497 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`useTimelineColumns augmentedColumnHeaders should return the default columns 1`] = `
+Array [
+ Object {
+ "aggregatable": true,
+ "category": "base",
+ "columnHeaderType": "not-filtered",
+ "description": "Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.",
+ "esTypes": Array [
+ "date",
+ ],
+ "example": "2016-05-23T08:05:34.853Z",
+ "format": "",
+ "id": "@timestamp",
+ "indexes": Array [
+ "auditbeat",
+ "filebeat",
+ "packetbeat",
+ ],
+ "initialWidth": 190,
+ "name": "@timestamp",
+ "readFromDocValues": true,
+ "searchable": true,
+ "type": "date",
+ },
+ Object {
+ "aggregatable": false,
+ "category": "base",
+ "columnHeaderType": "not-filtered",
+ "description": "For log events the message field contains the log message, optimized for viewing in a log viewer. For structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event. If multiple messages exist, they can be combined into one message.",
+ "esTypes": Array [
+ "text",
+ ],
+ "example": "Hello World",
+ "format": "string",
+ "id": "message",
+ "indexes": Array [
+ "auditbeat",
+ "filebeat",
+ "packetbeat",
+ ],
+ "initialWidth": 180,
+ "name": "message",
+ "searchable": true,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "category": "event",
+ "columnHeaderType": "not-filtered",
+ "description": "This is one of four ECS Categorization Fields, and indicates the second level in the ECS category hierarchy. \`event.category\` represents the \\"big buckets\\" of ECS categories. For example, filtering on \`event.category:process\` yields all events relating to process activity. This field is closely related to \`event.type\`, which is used as a subcategory. This field is an array. This will allow proper categorization of some events that fall in multiple categories.",
+ "esTypes": Array [
+ "keyword",
+ ],
+ "example": "authentication",
+ "format": "string",
+ "id": "event.category",
+ "indexes": Array [
+ "apm-*-transaction*",
+ "auditbeat-*",
+ "endgame-*",
+ "filebeat-*",
+ "logs-*",
+ "packetbeat-*",
+ "traces-apm*",
+ "winlogbeat-*",
+ "-*elastic-cloud-logs-*",
+ ],
+ "initialWidth": 180,
+ "name": "event.category",
+ "searchable": true,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "category": "event",
+ "columnHeaderType": "not-filtered",
+ "description": "The action captured by the event. This describes the information in the event. It is more specific than \`event.category\`. Examples are \`group-add\`, \`process-started\`, \`file-created\`. The value is normally defined by the implementer.",
+ "esTypes": Array [
+ "keyword",
+ ],
+ "example": "user-password-change",
+ "format": "string",
+ "id": "event.action",
+ "indexes": Array [
+ "apm-*-transaction*",
+ "auditbeat-*",
+ "endgame-*",
+ "filebeat-*",
+ "logs-*",
+ "packetbeat-*",
+ "traces-apm*",
+ "winlogbeat-*",
+ "-*elastic-cloud-logs-*",
+ ],
+ "initialWidth": 180,
+ "name": "event.action",
+ "searchable": true,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "category": "host",
+ "columnHeaderType": "not-filtered",
+ "description": "Name of the host. It can contain what \`hostname\` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.",
+ "esTypes": Array [
+ "keyword",
+ ],
+ "format": "string",
+ "id": "host.name",
+ "indexes": Array [
+ "apm-*-transaction*",
+ "auditbeat-*",
+ "endgame-*",
+ "filebeat-*",
+ "logs-*",
+ "packetbeat-*",
+ "traces-apm*",
+ "winlogbeat-*",
+ "-*elastic-cloud-logs-*",
+ ],
+ "initialWidth": 180,
+ "name": "host.name",
+ "searchable": true,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "category": "source",
+ "columnHeaderType": "not-filtered",
+ "description": "IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.",
+ "esTypes": Array [
+ "ip",
+ ],
+ "example": "",
+ "format": "",
+ "id": "source.ip",
+ "indexes": Array [
+ "auditbeat",
+ "filebeat",
+ "packetbeat",
+ ],
+ "initialWidth": 180,
+ "name": "source.ip",
+ "searchable": true,
+ "type": "ip",
+ },
+ Object {
+ "aggregatable": true,
+ "category": "destination",
+ "columnHeaderType": "not-filtered",
+ "description": "IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.",
+ "esTypes": Array [
+ "ip",
+ ],
+ "example": "",
+ "format": "",
+ "id": "destination.ip",
+ "indexes": Array [
+ "auditbeat",
+ "filebeat",
+ "packetbeat",
+ ],
+ "initialWidth": 180,
+ "name": "destination.ip",
+ "searchable": true,
+ "type": "ip",
+ },
+ Object {
+ "aggregatable": true,
+ "category": "user",
+ "columnHeaderType": "not-filtered",
+ "description": "Short name or login of the user.",
+ "esTypes": Array [
+ "keyword",
+ ],
+ "example": "albert",
+ "format": "string",
+ "id": "user.name",
+ "indexes": Array [
+ "auditbeat",
+ "filebeat",
+ "packetbeat",
+ ],
+ "initialWidth": 180,
+ "name": "user.name",
+ "searchable": true,
+ "type": "string",
+ },
+]
+`;
+
+exports[`useTimelineColumns augmentedColumnHeaders should return the default unified data table (udt) columns 1`] = `
+Array [
+ Object {
+ "aggregatable": true,
+ "category": "base",
+ "columnHeaderType": "not-filtered",
+ "description": "Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.",
+ "esTypes": Array [
+ "date",
+ ],
+ "example": "2016-05-23T08:05:34.853Z",
+ "format": "",
+ "id": "@timestamp",
+ "indexes": Array [
+ "auditbeat",
+ "filebeat",
+ "packetbeat",
+ ],
+ "initialWidth": 215,
+ "name": "@timestamp",
+ "readFromDocValues": true,
+ "searchable": true,
+ "type": "date",
+ },
+ Object {
+ "aggregatable": false,
+ "category": "base",
+ "columnHeaderType": "not-filtered",
+ "description": "For log events the message field contains the log message, optimized for viewing in a log viewer. For structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event. If multiple messages exist, they can be combined into one message.",
+ "esTypes": Array [
+ "text",
+ ],
+ "example": "Hello World",
+ "format": "string",
+ "id": "message",
+ "indexes": Array [
+ "auditbeat",
+ "filebeat",
+ "packetbeat",
+ ],
+ "initialWidth": 360,
+ "name": "message",
+ "searchable": true,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "category": "event",
+ "columnHeaderType": "not-filtered",
+ "description": "This is one of four ECS Categorization Fields, and indicates the second level in the ECS category hierarchy. \`event.category\` represents the \\"big buckets\\" of ECS categories. For example, filtering on \`event.category:process\` yields all events relating to process activity. This field is closely related to \`event.type\`, which is used as a subcategory. This field is an array. This will allow proper categorization of some events that fall in multiple categories.",
+ "esTypes": Array [
+ "keyword",
+ ],
+ "example": "authentication",
+ "format": "string",
+ "id": "event.category",
+ "indexes": Array [
+ "apm-*-transaction*",
+ "auditbeat-*",
+ "endgame-*",
+ "filebeat-*",
+ "logs-*",
+ "packetbeat-*",
+ "traces-apm*",
+ "winlogbeat-*",
+ "-*elastic-cloud-logs-*",
+ ],
+ "name": "event.category",
+ "searchable": true,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "category": "event",
+ "columnHeaderType": "not-filtered",
+ "description": "The action captured by the event. This describes the information in the event. It is more specific than \`event.category\`. Examples are \`group-add\`, \`process-started\`, \`file-created\`. The value is normally defined by the implementer.",
+ "esTypes": Array [
+ "keyword",
+ ],
+ "example": "user-password-change",
+ "format": "string",
+ "id": "event.action",
+ "indexes": Array [
+ "apm-*-transaction*",
+ "auditbeat-*",
+ "endgame-*",
+ "filebeat-*",
+ "logs-*",
+ "packetbeat-*",
+ "traces-apm*",
+ "winlogbeat-*",
+ "-*elastic-cloud-logs-*",
+ ],
+ "name": "event.action",
+ "searchable": true,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "category": "host",
+ "columnHeaderType": "not-filtered",
+ "description": "Name of the host. It can contain what \`hostname\` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.",
+ "esTypes": Array [
+ "keyword",
+ ],
+ "format": "string",
+ "id": "host.name",
+ "indexes": Array [
+ "apm-*-transaction*",
+ "auditbeat-*",
+ "endgame-*",
+ "filebeat-*",
+ "logs-*",
+ "packetbeat-*",
+ "traces-apm*",
+ "winlogbeat-*",
+ "-*elastic-cloud-logs-*",
+ ],
+ "name": "host.name",
+ "searchable": true,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "category": "source",
+ "columnHeaderType": "not-filtered",
+ "description": "IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.",
+ "esTypes": Array [
+ "ip",
+ ],
+ "example": "",
+ "format": "",
+ "id": "source.ip",
+ "indexes": Array [
+ "auditbeat",
+ "filebeat",
+ "packetbeat",
+ ],
+ "name": "source.ip",
+ "searchable": true,
+ "type": "ip",
+ },
+ Object {
+ "aggregatable": true,
+ "category": "destination",
+ "columnHeaderType": "not-filtered",
+ "description": "IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.",
+ "esTypes": Array [
+ "ip",
+ ],
+ "example": "",
+ "format": "",
+ "id": "destination.ip",
+ "indexes": Array [
+ "auditbeat",
+ "filebeat",
+ "packetbeat",
+ ],
+ "name": "destination.ip",
+ "searchable": true,
+ "type": "ip",
+ },
+ Object {
+ "aggregatable": true,
+ "category": "user",
+ "columnHeaderType": "not-filtered",
+ "description": "Short name or login of the user.",
+ "esTypes": Array [
+ "keyword",
+ ],
+ "example": "albert",
+ "format": "string",
+ "id": "user.name",
+ "indexes": Array [
+ "auditbeat",
+ "filebeat",
+ "packetbeat",
+ ],
+ "name": "user.name",
+ "searchable": true,
+ "type": "string",
+ },
+]
+`;
+
+exports[`useTimelineColumns augmentedColumnHeaders should return the provided columns 1`] = `
+Array [
+ Object {
+ "aggregatable": true,
+ "category": "source",
+ "columnHeaderType": "not-filtered",
+ "description": "IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.",
+ "esTypes": Array [
+ "ip",
+ ],
+ "example": "",
+ "format": "",
+ "id": "source.ip",
+ "indexes": Array [
+ "auditbeat",
+ "filebeat",
+ "packetbeat",
+ ],
+ "initialWidth": 150,
+ "name": "source.ip",
+ "searchable": true,
+ "type": "ip",
+ },
+ Object {
+ "columnHeaderType": "not-filtered",
+ "id": "agent.type",
+ "initialWidth": 150,
+ },
+]
+`;
+
+exports[`useTimelineColumns getTimelineQueryFieldsFromColumns should have a width of 152 for 5 actions 1`] = `
+Array [
+ "source.ip",
+ "agent.type",
+ "@timestamp",
+ "kibana.alert.workflow_status",
+ "kibana.alert.workflow_tags",
+ "kibana.alert.workflow_assignee_ids",
+ "kibana.alert.group.id",
+ "kibana.alert.original_time",
+ "kibana.alert.building_block_type",
+ "kibana.alert.rule.from",
+ "kibana.alert.rule.name",
+ "kibana.alert.rule.to",
+ "kibana.alert.rule.uuid",
+ "kibana.alert.rule.rule_id",
+ "kibana.alert.rule.type",
+ "kibana.alert.suppression.docs_count",
+ "kibana.alert.original_event.kind",
+ "kibana.alert.original_event.module",
+ "file.path",
+ "file.Ext.code_signature.subject_name",
+ "file.Ext.code_signature.trusted",
+ "file.hash.sha256",
+ "host.os.family",
+ "event.code",
+ "process.entry_leader.entity_id",
+]
+`;
+
+exports[`useTimelineColumns getTimelineQueryFieldsFromColumns should return the list of all the fields 1`] = `
+Array [
+ "@timestamp",
+ "message",
+ "event.category",
+ "event.action",
+ "host.name",
+ "source.ip",
+ "destination.ip",
+ "user.name",
+ "@timestamp",
+ "kibana.alert.workflow_status",
+ "kibana.alert.workflow_tags",
+ "kibana.alert.workflow_assignee_ids",
+ "kibana.alert.group.id",
+ "kibana.alert.original_time",
+ "kibana.alert.building_block_type",
+ "kibana.alert.rule.from",
+ "kibana.alert.rule.name",
+ "kibana.alert.rule.to",
+ "kibana.alert.rule.uuid",
+ "kibana.alert.rule.rule_id",
+ "kibana.alert.rule.type",
+ "kibana.alert.suppression.docs_count",
+ "kibana.alert.original_event.kind",
+ "kibana.alert.original_event.module",
+ "file.path",
+ "file.Ext.code_signature.subject_name",
+ "file.Ext.code_signature.trusted",
+ "file.hash.sha256",
+ "host.os.family",
+ "event.code",
+ "process.entry_leader.entity_id",
+]
+`;
+
+exports[`useTimelineColumns leadingControlColumns should return the leading control columns 1`] = `
+Array [
+ Object {
+ "headerCellRender": Object {
+ "$$typeof": Symbol(react.memo),
+ "compare": null,
+ "type": Object {
+ "$$typeof": Symbol(react.memo),
+ "compare": null,
+ "type": [Function],
+ },
+ },
+ "id": "default-timeline-control-column",
+ "rowCellRender": Object {
+ "$$typeof": Symbol(react.memo),
+ "compare": null,
+ "type": [Function],
+ },
+ "width": 180,
+ },
+]
+`;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.test.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.test.ts
new file mode 100644
index 0000000000000..91b57cd839098
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.test.ts
@@ -0,0 +1,152 @@
+/*
+ * 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 { TestProviders } from '../../../../../common/mock';
+import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
+import { renderHook } from '@testing-library/react-hooks';
+import { useLicense } from '../../../../../common/hooks/use_license';
+import { useTimelineColumns } from './use_timeline_columns';
+import { defaultUdtHeaders } from '../../unified_components/default_headers';
+import { defaultHeaders } from '../../body/column_headers/default_headers';
+import type { ColumnHeaderOptions } from '../../../../../../common/types/timeline/columns';
+
+jest.mock('../../../../../common/hooks/use_experimental_features', () => ({
+ useIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(false),
+}));
+
+const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
+
+jest.mock('../../../../../common/hooks/use_license', () => ({
+ useLicense: jest.fn().mockReturnValue({
+ isEnterprise: () => true,
+ }),
+}));
+
+const useLicenseMock = useLicense as jest.Mock;
+
+describe('useTimelineColumns', () => {
+ const mockColumns: ColumnHeaderOptions[] = [
+ {
+ columnHeaderType: 'not-filtered',
+ id: 'source.ip',
+ initialWidth: 150,
+ },
+ {
+ columnHeaderType: 'not-filtered',
+ id: 'agent.type',
+ initialWidth: 150,
+ },
+ ];
+ describe('defaultColumns', () => {
+ it('should return the default columns', () => {
+ const { result } = renderHook(() => useTimelineColumns([]), {
+ wrapper: TestProviders,
+ });
+ expect(result.current.defaultColumns).toEqual(defaultHeaders);
+ });
+
+ it('should return the default unified data table (udt) columns', () => {
+ useIsExperimentalFeatureEnabledMock.mockReturnValue(true);
+ const { result } = renderHook(() => useTimelineColumns([]), {
+ wrapper: TestProviders,
+ });
+ expect(result.current.defaultColumns).toEqual(defaultUdtHeaders);
+ });
+ });
+
+ describe('localColumns', () => {
+ it('should return the default columns', () => {
+ useIsExperimentalFeatureEnabledMock.mockReturnValue(false);
+ const { result } = renderHook(() => useTimelineColumns([]), {
+ wrapper: TestProviders,
+ });
+ expect(result.current.localColumns).toEqual(defaultHeaders);
+ });
+
+ it('should return the default unified data table (udt) columns', () => {
+ useIsExperimentalFeatureEnabledMock.mockReturnValue(true);
+ const { result } = renderHook(() => useTimelineColumns([]), {
+ wrapper: TestProviders,
+ });
+ expect(result.current.localColumns).toEqual(defaultUdtHeaders);
+ });
+
+ it('should return the provided columns', () => {
+ const { result } = renderHook(() => useTimelineColumns(mockColumns), {
+ wrapper: TestProviders,
+ });
+ expect(result.current.localColumns).toEqual(mockColumns);
+ });
+ });
+
+ describe('augmentedColumnHeaders', () => {
+ it('should return the default columns', () => {
+ useIsExperimentalFeatureEnabledMock.mockReturnValue(false);
+ const { result } = renderHook(() => useTimelineColumns([]), {
+ wrapper: TestProviders,
+ });
+ expect(result.current.augmentedColumnHeaders).toMatchSnapshot();
+ });
+
+ it('should return the default unified data table (udt) columns', () => {
+ useIsExperimentalFeatureEnabledMock.mockReturnValue(true);
+ const { result } = renderHook(() => useTimelineColumns([]), {
+ wrapper: TestProviders,
+ });
+ expect(result.current.augmentedColumnHeaders).toMatchSnapshot();
+ });
+
+ it('should return the provided columns', () => {
+ const { result } = renderHook(() => useTimelineColumns(mockColumns), {
+ wrapper: TestProviders,
+ });
+ expect(result.current.augmentedColumnHeaders).toMatchSnapshot();
+ });
+ });
+
+ describe('leadingControlColumns', () => {
+ it('should return the leading control columns', () => {
+ const { result } = renderHook(() => useTimelineColumns([]), {
+ wrapper: TestProviders,
+ });
+ expect(result.current.leadingControlColumns).toMatchSnapshot();
+ });
+ it('should have a width of 152 for 5 actions', () => {
+ useLicenseMock.mockReturnValue({
+ isEnterprise: () => false,
+ });
+ const { result } = renderHook(() => useTimelineColumns([]), {
+ wrapper: TestProviders,
+ });
+ expect(result.current.leadingControlColumns[0].width).toBe(152);
+ });
+ it('should have a width of 180 for 6 actions', () => {
+ useLicenseMock.mockReturnValue({
+ isEnterprise: () => true,
+ });
+ const { result } = renderHook(() => useTimelineColumns([]), {
+ wrapper: TestProviders,
+ });
+ expect(result.current.leadingControlColumns[0].width).toBe(180);
+ });
+ });
+
+ describe('getTimelineQueryFieldsFromColumns', () => {
+ it('should return the list of all the fields', () => {
+ const { result } = renderHook(() => useTimelineColumns([]), {
+ wrapper: TestProviders,
+ });
+ expect(result.current.getTimelineQueryFieldsFromColumns()).toMatchSnapshot();
+ });
+ it('should have a width of 152 for 5 actions', () => {
+ const { result } = renderHook(() => useTimelineColumns(mockColumns), {
+ wrapper: TestProviders,
+ });
+ expect(result.current.getTimelineQueryFieldsFromColumns()).toMatchSnapshot();
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.ts
new file mode 100644
index 0000000000000..ed82fb52de915
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.ts
@@ -0,0 +1,79 @@
+/*
+ * 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 { isEmpty } from 'lodash/fp';
+import { useCallback, useMemo } from 'react';
+import { useLicense } from '../../../../../common/hooks/use_license';
+import { SourcererScopeName } from '../../../../../common/store/sourcerer/model';
+import { useSourcererDataView } from '../../../../../common/containers/sourcerer';
+import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
+import { defaultHeaders } from '../../body/column_headers/default_headers';
+import { requiredFieldsForActions } from '../../../../../detections/components/alerts_table/default_config';
+import { getDefaultControlColumn } from '../../body/control_columns';
+import { HeaderActions } from '../../../../../common/components/header_actions/header_actions';
+import { defaultUdtHeaders } from '../../unified_components/default_headers';
+import type { ColumnHeaderOptions } from '../../../../../../common/types';
+import { memoizedGetTimelineColumnHeaders } from './utils';
+
+export const useTimelineColumns = (columns: ColumnHeaderOptions[]) => {
+ const { browserFields } = useSourcererDataView(SourcererScopeName.timeline);
+
+ const unifiedComponentsInTimelineEnabled = useIsExperimentalFeatureEnabled(
+ 'unifiedComponentsInTimelineEnabled'
+ );
+
+ const isEnterprisePlus = useLicense().isEnterprise();
+ const ACTION_BUTTON_COUNT = isEnterprisePlus ? 6 : 5;
+
+ const defaultColumns = useMemo(
+ () => (unifiedComponentsInTimelineEnabled ? defaultUdtHeaders : defaultHeaders),
+ [unifiedComponentsInTimelineEnabled]
+ );
+
+ const localColumns = useMemo(
+ () => (isEmpty(columns) ? defaultColumns : columns),
+ [columns, defaultColumns]
+ );
+
+ const augmentedColumnHeaders = memoizedGetTimelineColumnHeaders(
+ localColumns,
+ browserFields,
+ false
+ );
+
+ const getTimelineQueryFieldsFromColumns = useCallback(() => {
+ const columnFields = augmentedColumnHeaders.map((c) => c.id);
+
+ return [...columnFields, ...requiredFieldsForActions];
+ }, [augmentedColumnHeaders]);
+
+ const leadingControlColumns = useMemo(
+ () =>
+ getDefaultControlColumn(ACTION_BUTTON_COUNT).map((x) => ({
+ ...x,
+ headerCellRender: HeaderActions,
+ })),
+ [ACTION_BUTTON_COUNT]
+ );
+
+ return useMemo(
+ () => ({
+ defaultColumns,
+ localColumns,
+ augmentedColumnHeaders,
+ getTimelineQueryFieldsFromColumns,
+ leadingControlColumns,
+ }),
+ [
+ augmentedColumnHeaders,
+ defaultColumns,
+ getTimelineQueryFieldsFromColumns,
+ leadingControlColumns,
+ localColumns,
+ ]
+ );
+};
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/utils.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/utils.ts
index 716f9e7441b41..879e4b140a61a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/utils.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/utils.ts
@@ -4,9 +4,13 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+import type { BrowserFields, ColumnHeaderOptions } from '@kbn/timelines-plugin/common';
+import memoizeOne from 'memoize-one';
+import type { ControlColumnProps } from '../../../../../../common/types';
+import type { Sort } from '../../body/sort';
import type { TimelineItem } from '../../../../../../common/search_strategy';
import type { inputsModel } from '../../../../../common/store';
-
+import { getColumnHeaders } from '../../body/column_headers/helpers';
interface TimerangeSimilarityProps {
end: inputsModel.InputsRange['timerange']['to'];
start: inputsModel.InputsRange['timerange']['from'];
@@ -21,4 +25,14 @@ export const isTimerangeSame = (
prevProps.start === nextProps.start &&
prevProps.timerangeKind === nextProps.timerangeKind;
-export const EMPTY_EVENTS: TimelineItem[] = [];
+export const TIMELINE_EMPTY_EVENTS: TimelineItem[] = [];
+
+export const TIMELINE_NO_SORTING: Sort[] = [];
+
+export const timelineEmptyTrailingControlColumns: ControlColumnProps[] = [];
+
+export const memoizedGetTimelineColumnHeaders: (
+ headers: ColumnHeaderOptions[],
+ browserFields: BrowserFields,
+ isEventRenderedView: boolean
+) => ColumnHeaderOptions[] = memoizeOne(getColumnHeaders);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/__snapshots__/custom_timeline_data_grid_body.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/__snapshots__/custom_timeline_data_grid_body.test.tsx.snap
index d1d02dc0019ee..c9b48cf89395f 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/__snapshots__/custom_timeline_data_grid_body.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/__snapshots__/custom_timeline_data_grid_body.test.tsx.snap
@@ -21,7 +21,7 @@ exports[`CustomTimelineDataGridBody should render exactly as snapshots 1`] = `
role="row"
>
@@ -43,7 +43,7 @@ exports[`CustomTimelineDataGridBody should render exactly as snapshots 1`] = `
role="row"
>
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/custom_timeline_data_grid_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/custom_timeline_data_grid_body.tsx
index ea18218200544..62ea20d165842 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/custom_timeline_data_grid_body.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/custom_timeline_data_grid_body.tsx
@@ -15,6 +15,7 @@ import styled from 'styled-components';
import type { RowRenderer } from '../../../../../../common/types';
import { TIMELINE_EVENT_DETAIL_ROW_ID } from '../../body/constants';
import { useStatefulRowRenderer } from '../../body/events/stateful_row_renderer/use_stateful_row_renderer';
+import { useGetEventTypeRowClassName } from './use_get_event_type_row_classname';
export type CustomTimelineDataGridBodyProps = EuiDataGridCustomBodyProps & {
rows: Array
| undefined;
@@ -75,11 +76,13 @@ const CustomGridRow = styled.div.attrs<{
border-bottom: 1px solid ${(props) => (props.theme as EuiTheme).eui.euiBorderThin};
`;
-/**
- *
- * A Simple Wrapper component for displaying a custom data grid `cell`
- */
-const CustomGridRowCellWrapper = styled.div.attrs({ className: 'rowCellWrapper', role: 'row' })`
+/* below styles as per : https://eui.elastic.co/#/tabular-content/data-grid-advanced#custom-body-renderer */
+const CustomGridRowCellWrapper = styled.div.attrs<{
+ className?: string;
+}>((props) => ({
+ className: `rowCellWrapper ${props.className ?? ''}`,
+ role: 'row',
+}))`
display: flex;
`;
@@ -117,13 +120,14 @@ const CustomDataGridSingleRow = memo(function CustomDataGridSingleRow(
: {},
[canShowRowRenderer]
);
+ const eventTypeRowClassName = useGetEventTypeRowClassName(rowData.ecs);
return (
-
+
{visibleColumns.map((column, colIndex) => {
// Skip the expanded row cell - we'll render it manually outside of the flex wrapper
if (column.id !== TIMELINE_EVENT_DETAIL_ROW_ID) {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.tsx
index b823f3715c566..a3f4588415503 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.tsx
@@ -26,8 +26,9 @@ import type {
OnChangePage,
RowRenderer,
ToggleDetailPanel,
+ TimelineTabs,
} from '../../../../../../common/types/timeline';
-import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
+import { TimelineId } from '../../../../../../common/types/timeline';
import type { State, inputsModel } from '../../../../../common/store';
import { SourcererScopeName } from '../../../../../common/store/sourcerer/model';
import { useSourcererDataView } from '../../../../../common/containers/sourcerer';
@@ -65,7 +66,7 @@ type CommonDataTableProps = {
dataLoadingState: DataLoadingState;
updatedAt: number;
isTextBasedQuery?: boolean;
-} & Pick;
+} & Pick;
interface DataTableProps extends CommonDataTableProps {
dataView: DataView;
@@ -83,6 +84,7 @@ export const TimelineDataTableComponent: React.FC = memo(
rowRenderers,
sort,
events,
+ isSortEnabled = true,
onFieldEdited,
refetch,
dataLoadingState,
@@ -149,27 +151,27 @@ export const TimelineDataTableComponent: React.FC = memo(
dispatch(
timelineActions.toggleDetailPanel({
...updatedExpandedDetail,
- tabType: TimelineTabs.query,
+ tabType: activeTab,
id: timelineId,
})
);
activeTimeline.toggleExpandedDetail({ ...updatedExpandedDetail });
},
- [dispatch, refetch, timelineId]
+ [activeTab, dispatch, refetch, timelineId]
);
const handleOnPanelClosed = useCallback(() => {
if (
- expandedDetail[TimelineTabs.query]?.panelView &&
+ expandedDetail[activeTab]?.panelView &&
timelineId === TimelineId.active &&
showExpandedDetails
) {
activeTimeline.toggleExpandedDetail({});
}
setExpandedDoc(undefined);
- onEventClosed({ tabType: TimelineTabs.query, id: timelineId });
- }, [onEventClosed, timelineId, expandedDetail, showExpandedDetails]);
+ onEventClosed({ tabType: activeTab, id: timelineId });
+ }, [expandedDetail, activeTab, timelineId, showExpandedDetails, onEventClosed]);
const onSetExpandedDoc = useCallback(
(newDoc?: DataTableRecord) => {
@@ -364,7 +366,7 @@ export const TimelineDataTableComponent: React.FC = memo(
onUpdateSampleSize={onUpdateSampleSize}
setExpandedDoc={onSetExpandedDoc}
showTimeCol={showTimeCol}
- isSortEnabled={true}
+ isSortEnabled={isSortEnabled}
sort={sort}
rowHeightState={rowHeight}
isPlainRecord={isTextBasedQuery}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/use_get_event_type_row_classname.test.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/use_get_event_type_row_classname.test.ts
new file mode 100644
index 0000000000000..fb9865f5a7a86
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/use_get_event_type_row_classname.test.ts
@@ -0,0 +1,57 @@
+/*
+ * 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 { useGetEventTypeRowClassName } from './use_get_event_type_row_classname';
+import { renderHook } from '@testing-library/react-hooks';
+
+const mockOddEqlEvent = {
+ _id: 'test-eql-alert',
+ eql: { parentId: 'some-test-id', sequenceNumber: '1-0' },
+};
+
+const mockBuildingBlockAlert = {
+ _id: 'test-eql-alert',
+ eql: { parentId: 'some-test-id', sequenceNumber: '2-0' },
+ kibana: {
+ alert: { building_block_type: ['default'] },
+ },
+};
+
+const mockAlert = {
+ _id: 'test-alert',
+ kibana: { alert: { rule: { uuid: ['test-uuid'], parameters: {} } } },
+};
+
+const mockEvent = {
+ _id: 'basic-event',
+};
+
+describe('useGetEventTypeRowClassName', () => {
+ it('should return rawEvent', () => {
+ const { result } = renderHook(() => useGetEventTypeRowClassName(mockEvent));
+ expect(result.current).toEqual('rawEvent');
+ });
+
+ it('should contain eqlSequence', () => {
+ const { result } = renderHook(() => useGetEventTypeRowClassName(mockBuildingBlockAlert));
+ expect(result.current).toContain('eqlSequence');
+ });
+
+ it('should contain buildingBlockType', () => {
+ const { result } = renderHook(() => useGetEventTypeRowClassName(mockBuildingBlockAlert));
+ expect(result.current).toContain('buildingBlockType');
+ });
+
+ it('should return eqlNonSequence', () => {
+ const { result } = renderHook(() => useGetEventTypeRowClassName(mockOddEqlEvent));
+ expect(result.current).toEqual('eqlNonSequence');
+ });
+
+ it('should return nonRawEvent', () => {
+ const { result } = renderHook(() => useGetEventTypeRowClassName(mockAlert));
+ expect(result.current).toEqual('nonRawEvent');
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/use_get_event_type_row_classname.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/use_get_event_type_row_classname.ts
new file mode 100644
index 0000000000000..32d7ea4c01c79
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/use_get_event_type_row_classname.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { TimelineItem } from '@kbn/timelines-plugin/common';
+import { useMemo } from 'react';
+import { getEventType, isEvenEqlSequence, isEventBuildingBlockType } from '../../body/helpers';
+
+export const useGetEventTypeRowClassName = (ecsData: TimelineItem['ecs']) => {
+ const eventType = useMemo(() => getEventType(ecsData), [ecsData]);
+ const eventTypeClassName = useMemo(
+ () =>
+ eventType === 'raw'
+ ? 'rawEvent'
+ : eventType === 'eql'
+ ? isEvenEqlSequence(ecsData)
+ ? 'eqlSequence'
+ : 'eqlNonSequence'
+ : 'nonRawEvent',
+ [ecsData, eventType]
+ );
+ const buildingBlockTypeClassName = useMemo(
+ () => (isEventBuildingBlockType(ecsData) ? 'buildingBlockType' : ''),
+ [ecsData]
+ );
+
+ return useMemo(
+ () => `${eventTypeClassName} ${buildingBlockTypeClassName}`.trim(),
+ [eventTypeClassName, buildingBlockTypeClassName]
+ );
+};
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.tsx
index 6121dd1f36b7b..527b0146dfdbe 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.tsx
@@ -97,6 +97,7 @@ export const HIDE_FOR_SIZES = ['xs', 's'];
interface Props {
columns: ColumnHeaderOptions[];
+ isSortEnabled?: boolean;
rowRenderers: RowRenderer[];
timelineId: string;
itemsPerPage: number;
@@ -118,6 +119,7 @@ interface Props {
const UnifiedTimelineComponent: React.FC = ({
columns,
+ isSortEnabled,
activeTab,
timelineId,
itemsPerPage,
@@ -397,6 +399,7 @@ const UnifiedTimelineComponent: React.FC = ({
columnIds={currentColumnIds}
rowRenderers={rowRenderers}
timelineId={timelineId}
+ isSortEnabled={isSortEnabled}
itemsPerPage={itemsPerPage}
itemsPerPageOptions={itemsPerPageOptions}
sort={sortingColumns}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/styles.tsx
index 279284bf004a4..3e87083f7e098 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/styles.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/styles.tsx
@@ -99,7 +99,8 @@ export const StyledTimelineUnifiedDataTable = styled.div.attrs(({ className = ''
);
}
.udtTimeline .euiDataGridRow:has(.eqlSequence) {
- .euiDataGridRowCell--firstColumn {
+ .euiDataGridRowCell--firstColumn,
+ .euiDataGridRowCell--lastColumn {
${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorPrimary};`}
}
background: repeating-linear-gradient(
@@ -111,7 +112,8 @@ export const StyledTimelineUnifiedDataTable = styled.div.attrs(({ className = ''
);
}
.udtTimeline .euiDataGridRow:has(.eqlNonSequence) {
- .euiDataGridRowCell--firstColumn {
+ .euiDataGridRowCell--firstColumn,
+ .euiDataGridRowCell--lastColumn {
${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorAccent};`}
}
background: repeating-linear-gradient(
@@ -122,11 +124,17 @@ export const StyledTimelineUnifiedDataTable = styled.div.attrs(({ className = ''
rgba(221, 10, 115, 0.05) 10px
);
}
- .udtTimeline .euiDataGridRow:has(.nonRawEvent) .euiDataGridRowCell--firstColumn {
- ${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorWarning};`}
+ .udtTimeline .euiDataGridRow:has(.nonRawEvent) {
+ .euiDataGridRowCell--firstColumn,
+ .euiDataGridRowCell--lastColumn {
+ ${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorWarning};`}
+ }
}
- .udtTimeline .euiDataGridRow:has(.rawEvent) .euiDataGridRowCell--firstColumn {
- ${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorLightShade};`}
+ .udtTimeline .euiDataGridRow:has(.rawEvent) {
+ .euiDataGridRowCell--firstColumn,
+ .euiDataGridRowCell--lastColumn {
+ ${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorLightShade};`}
+ }
}
.udtTimeline .rowCellWrapper {
From 4fcae21ea24943286f27bff78d0de8ddd35c9688 Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Thu, 25 Apr 2024 17:22:36 +0100
Subject: [PATCH 11/20] skip flaky suite (#181466)
---
.../cypress/e2e/investigations/timelines/timelines_table.cy.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/timelines_table.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/timelines_table.cy.ts
index 784c30eaa6cac..9f6833c7da2c8 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/timelines_table.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/timelines_table.cy.ts
@@ -26,7 +26,8 @@ import { clearSearchBar, searchForTimeline, toggleFavoriteFilter } from '../../.
const mockTimeline = getTimeline();
const mockFavoritedTimeline = getFavoritedTimeline();
-describe('timeline overview search', { tags: ['@ess', '@serverless'] }, () => {
+// FLAKY: https://github.com/elastic/kibana/issues/181466
+describe.skip('timeline overview search', { tags: ['@ess', '@serverless'] }, () => {
beforeEach(() => {
deleteTimelines();
createTimeline();
From 974158880d3ed5de8ea08501efe9df38b3ebba6d Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Thu, 25 Apr 2024 12:23:37 -0400
Subject: [PATCH 12/20] skip failing test suite (#181466)
---
.../cypress/e2e/investigations/timelines/timelines_table.cy.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/timelines_table.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/timelines_table.cy.ts
index 9f6833c7da2c8..6c1e7ee3f6cf4 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/timelines_table.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/timelines_table.cy.ts
@@ -27,6 +27,7 @@ const mockTimeline = getTimeline();
const mockFavoritedTimeline = getFavoritedTimeline();
// FLAKY: https://github.com/elastic/kibana/issues/181466
+// Failing: See https://github.com/elastic/kibana/issues/181466
describe.skip('timeline overview search', { tags: ['@ess', '@serverless'] }, () => {
beforeEach(() => {
deleteTimelines();
From 6ac0b5d73ac8fd4c1d2da7b9e0774a5f1724d844 Mon Sep 17 00:00:00 2001
From: Kevin Delemme
Date: Thu, 25 Apr 2024 12:32:17 -0400
Subject: [PATCH 13/20] fix(slo): remove assertion on deleted rollup documents
(#181725)
Resolves https://github.com/elastic/kibana/issues/180982
## Summary
When deleting an SLO, we also start a delete_by_query on the rollup
documents. This request is done asynchronously in the background. The
serverless integration test asserts on the deletion of the rollup
documents but fails some time.
As the deletion of the rollup document causes no harms if not done, I'm
removing this assertion from the test.
---
.../test_suites/observability/slos/delete_slo.ts | 14 --------------
1 file changed, 14 deletions(-)
diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/slos/delete_slo.ts b/x-pack/test_serverless/api_integration/test_suites/observability/slos/delete_slo.ts
index d605e57d997ef..f49cd75a23edc 100644
--- a/x-pack/test_serverless/api_integration/test_suites/observability/slos/delete_slo.ts
+++ b/x-pack/test_serverless/api_integration/test_suites/observability/slos/delete_slo.ts
@@ -136,7 +136,6 @@ export default function ({ getService }: FtrProviderContext) {
indexName: SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
});
- const numberOfRollupDocumentsBeforeDeletion = sloRollupData.hits.hits.length;
expect(sloRollupData.hits.hits.length > 0).to.be(true);
expect(sloSummaryData.hits.hits.length > 0).to.be(true);
@@ -164,19 +163,6 @@ export default function ({ getService }: FtrProviderContext) {
}
return true;
});
-
- await retry.waitForWithTimeout('SLO rollup data is deleted', 60 * 1000, async () => {
- const sloRollupDataAfterDeletion = await sloApi.getSloData({
- sloId,
- indexName: SLO_DESTINATION_INDEX_PATTERN,
- });
- if (
- sloRollupDataAfterDeletion.hits.hits.length >= numberOfRollupDocumentsBeforeDeletion
- ) {
- throw new Error('SLO rollup data not deleted yet');
- }
- return true;
- });
});
});
});
From 1c0ece5fc6b958a58a057680950c10331922e31f Mon Sep 17 00:00:00 2001
From: James Gowdy
Date: Thu, 25 Apr 2024 17:40:15 +0100
Subject: [PATCH 14/20] [ML] Hide file upload doc count chart until data is
searchable (#181460)
Fixes https://github.com/elastic/kibana/issues/179131
In serverless, there is a larger delay in when the newly ingested data
becomes searchable. Rather than displaying an empty chart, we now hide
the chart until we see some non-zero values.
---
.../components/doc_count_chart/doc_count_chart.tsx | 14 +++++++++++---
1 file changed, 11 insertions(+), 3 deletions(-)
diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/doc_count_chart/doc_count_chart.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/doc_count_chart/doc_count_chart.tsx
index 2ab341d5d6fb3..fd2b8a82248be 100644
--- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/doc_count_chart/doc_count_chart.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/doc_count_chart/doc_count_chart.tsx
@@ -18,7 +18,8 @@ import { useDataVisualizerKibana } from '../../../kibana_context';
const BAR_TARGET = 150;
const PROGRESS_INCREMENT = 5;
-const FINISHED_CHECKS = 3;
+const FINISHED_CHECKS = 10;
+const FINISHED_CHECKS_INTERVAL_MS = 2 * 1000;
const ERROR_ATTEMPTS = 3;
const BACK_FILL_BUCKETS = 8;
@@ -43,6 +44,7 @@ export const DocCountChart: FC<{
const [eventRateChartData, setEventRateChartData] = useState([]);
const [timeRange, setTimeRange] = useState<{ start: Moment; end: Moment } | undefined>(undefined);
+ const [dataReady, setDataReady] = useState(false);
const loadFullData = useRef(false);
@@ -91,6 +93,10 @@ export const DocCountChart: FC<{
? data
: [...eventRateChartData].splice(0, lastNonZeroTimeMs?.index ?? 0).concat(data);
+ if (dataReady === false && newData.some((d) => d.value > 0)) {
+ setDataReady(true);
+ }
+
setEventRateChartData(newData);
setLastNonZeroTimeMs(findLastTimestamp(newData, BACK_FILL_BUCKETS));
} catch (error) {
@@ -104,6 +110,7 @@ export const DocCountChart: FC<{
timeBuckets,
lastNonZeroTimeMs,
dataStart,
+ dataReady,
eventRateChartData,
recordFailure,
]);
@@ -114,7 +121,7 @@ export const DocCountChart: FC<{
if (counter !== 0) {
setTimeout(() => {
finishedChecks(counter - 1);
- }, 2 * 1000);
+ }, FINISHED_CHECKS_INTERVAL_MS);
}
},
[loadData]
@@ -179,7 +186,8 @@ export const DocCountChart: FC<{
statuses.indexCreatedStatus === IMPORT_STATUS.INCOMPLETE ||
statuses.ingestPipelineCreatedStatus === IMPORT_STATUS.INCOMPLETE ||
errorAttempts === 0 ||
- eventRateChartData.length === 0
+ eventRateChartData.length === 0 ||
+ dataReady === false
) {
return null;
}
From 2ab8875a36740ee6c558cfd385d5d0757fd769d4 Mon Sep 17 00:00:00 2001
From: James Gowdy
Date: Thu, 25 Apr 2024 17:53:57 +0100
Subject: [PATCH 15/20] [ML] Removing datafeed preview frozen tier message in
serverless (#181440)
When running in serverless, the warning about frozen tiers when no data
is available for the datafeed preview is hidden.
![image](https://github.com/elastic/kibana/assets/22172091/a49a5df0-cece-4172-9bac-c51fa26ce6df)
Also updates the page template for all ML pages to ensure a background
colour is always used.
Fixes https://github.com/elastic/kibana/issues/180020
---
.../components/ml_page/ml_page.tsx | 1 +
.../job_details/datafeed_preview_tab.tsx | 20 +++++++++++--------
2 files changed, 13 insertions(+), 8 deletions(-)
diff --git a/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx b/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx
index 3553f145fcead..0e16d569021d8 100644
--- a/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx
+++ b/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx
@@ -128,6 +128,7 @@ export const MlPage: FC<{ pageDeps: PageDependencies }> = React.memo(({ pageDeps
className={'ml-app'}
data-test-subj={'mlApp'}
restrictWidth={false}
+ panelled
solutionNav={
showMLNavMenu
? {
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.tsx
index 7fa4d0c289a5f..b9813ece905f4 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.tsx
@@ -9,6 +9,7 @@ import type { FC } from 'react';
import React, { useEffect, useState } from 'react';
import { EuiCallOut, EuiLoadingSpinner } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
+import { useEnabledFeatures } from '../../../../contexts/ml';
import { ML_DATA_PREVIEW_COUNT } from '../../../../../../common/util/job_utils';
import { useMlApiContext } from '../../../../contexts/kibana';
import { usePermissionCheck } from '../../../../capabilities/check_capabilities';
@@ -23,6 +24,7 @@ export const DatafeedPreviewPane: FC = ({ job }) => {
const {
jobs: { datafeedPreview },
} = useMlApiContext();
+ const { showNodeInfo } = useEnabledFeatures();
const canPreviewDatafeed = usePermissionCheck('canPreviewDatafeed');
const [loading, setLoading] = useState(false);
@@ -54,7 +56,7 @@ export const DatafeedPreviewPane: FC = ({ job }) => {
) : (
<>
{previewJson === null ? (
-
+
) : (
)}
@@ -82,7 +84,7 @@ const InsufficientPermissions: FC = () => (
);
-const EmptyResults: FC = () => (
+const EmptyResults: FC<{ showFrozenTierText: boolean }> = ({ showFrozenTierText }) => (
(
color="warning"
iconType="warning"
>
-
-
-
+ {showFrozenTierText ? (
+
+
+
+ ) : null}
);
From 3a65e17d78a8f5148adae6c6445230da55ce4869 Mon Sep 17 00:00:00 2001
From: Bhavya RM
Date: Thu, 25 Apr 2024 22:24:18 +0530
Subject: [PATCH 16/20] [a11y] Remove jext-axe and supporting code from kibana
(#180694)
Removing jest-axe and supporting code from Kibana because this library
(not regularly updated anymore and used only in one test file) is
blocking me from updating axe-core.
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
package.json | 2 -
packages/kbn-test-jest-helpers/index.ts | 2 -
.../kbn-test-jest-helpers/src/axe_helpers.ts | 35 ----------
packages/kbn-test-jest-helpers/tsconfig.json | 1 -
.../__jest__/a11y/indices_tab.a11y.test.ts | 50 --------------
yarn.lock | 68 +------------------
6 files changed, 1 insertion(+), 157 deletions(-)
delete mode 100644 packages/kbn-test-jest-helpers/src/axe_helpers.ts
delete mode 100644 x-pack/plugins/index_management/__jest__/a11y/indices_tab.a11y.test.ts
diff --git a/package.json b/package.json
index 2839e22158d8a..dc0e1b763c80d 100644
--- a/package.json
+++ b/package.json
@@ -1445,7 +1445,6 @@
"@types/inquirer": "^7.3.1",
"@types/intl-relativeformat": "^2.1.0",
"@types/jest": "^29.5.3",
- "@types/jest-axe": "^3.5.3",
"@types/jquery": "^3.3.31",
"@types/js-levenshtein": "^1.1.0",
"@types/js-search": "^1.4.0",
@@ -1618,7 +1617,6 @@
"http-proxy": "^1.18.1",
"ignore": "^5.3.0",
"jest": "^29.6.1",
- "jest-axe": "^5.0.0",
"jest-canvas-mock": "^2.5.2",
"jest-cli": "^29.6.1",
"jest-config": "^29.6.1",
diff --git a/packages/kbn-test-jest-helpers/index.ts b/packages/kbn-test-jest-helpers/index.ts
index 1bb9875776ad1..758795ee80eaf 100644
--- a/packages/kbn-test-jest-helpers/index.ts
+++ b/packages/kbn-test-jest-helpers/index.ts
@@ -26,8 +26,6 @@ export * from './src/stub_web_worker';
export * from './src/testbed';
-export * from './src/axe_helpers';
-
export const nextTick = () => new Promise((res) => process.nextTick(res));
export const delay = (time = 0) => new Promise((resolve) => setTimeout(resolve, time));
diff --git a/packages/kbn-test-jest-helpers/src/axe_helpers.ts b/packages/kbn-test-jest-helpers/src/axe_helpers.ts
deleted file mode 100644
index 6b04bed95c95a..0000000000000
--- a/packages/kbn-test-jest-helpers/src/axe_helpers.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import { configureAxe } from 'jest-axe';
-import { Result } from 'axe-core';
-import { AXE_OPTIONS, AXE_CONFIG } from '@kbn/axe-config';
-import { ReactWrapper } from './testbed/types';
-
-const axeRunner = configureAxe({ globalOptions: { ...AXE_CONFIG } });
-
-/**
- * Function to test if a component doesn't have a11y violations from axe automated testing
- * @param component
- */
-export const expectToBeAccessible = async (component: ReactWrapper): Promise => {
- const violations = await getA11yViolations(component);
- expect(violations).toHaveLength(0);
-};
-
-/**
- * Returns a11y violations as found by axe testing
- * @param component
- */
-export const getA11yViolations = async (component: ReactWrapper): Promise => {
- const axeResults = await axeRunner(component.html(), {
- ...AXE_OPTIONS,
- resultTypes: ['violations'],
- });
- return axeResults.violations;
-};
diff --git a/packages/kbn-test-jest-helpers/tsconfig.json b/packages/kbn-test-jest-helpers/tsconfig.json
index 99630e68c5b98..596a28a55b540 100644
--- a/packages/kbn-test-jest-helpers/tsconfig.json
+++ b/packages/kbn-test-jest-helpers/tsconfig.json
@@ -10,7 +10,6 @@
],
"kbn_references": [
"@kbn/i18n-react",
- "@kbn/axe-config",
"@kbn/shared-ux-router",
],
"exclude": [
diff --git a/x-pack/plugins/index_management/__jest__/a11y/indices_tab.a11y.test.ts b/x-pack/plugins/index_management/__jest__/a11y/indices_tab.a11y.test.ts
deleted file mode 100644
index 08b11fc4bb50c..0000000000000
--- a/x-pack/plugins/index_management/__jest__/a11y/indices_tab.a11y.test.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { act } from 'react-dom/test-utils';
-import { expectToBeAccessible } from '@kbn/test-jest-helpers';
-import { IndicesTestBed, setup } from '../client_integration/home/indices_tab.helpers';
-import { setupEnvironment } from '../client_integration/helpers';
-import {
- createDataStreamBackingIndex,
- createNonDataStreamIndex,
-} from '../client_integration/home/data_streams_tab.helpers';
-
-describe('A11y Indices tab', () => {
- let testBed: IndicesTestBed;
- let httpSetup: ReturnType['httpSetup'];
- let httpRequestsMockHelpers: ReturnType['httpRequestsMockHelpers'];
-
- beforeEach(() => {
- const mockEnvironment = setupEnvironment();
- httpRequestsMockHelpers = mockEnvironment.httpRequestsMockHelpers;
- httpSetup = mockEnvironment.httpSetup;
- });
-
- it('when there are no indices', async () => {
- httpRequestsMockHelpers.setLoadIndicesResponse([]);
- await act(async () => {
- testBed = await setup(httpSetup);
- });
- const { component } = testBed;
- component.update();
- await expectToBeAccessible(component);
- });
-
- it('when there are indices', async () => {
- httpRequestsMockHelpers.setLoadIndicesResponse([
- createNonDataStreamIndex('non-data-stream-test-index'),
- createDataStreamBackingIndex('data-stream-test-index', 'test-data-stream'),
- ]);
- await act(async () => {
- testBed = await setup(httpSetup);
- });
- const { component } = testBed;
- component.update();
- await expectToBeAccessible(component);
- });
-});
diff --git a/yarn.lock b/yarn.lock
index 16b7c7a3932a0..1f7dd83962b44 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9956,14 +9956,6 @@
dependencies:
"@types/istanbul-lib-report" "*"
-"@types/jest-axe@^3.5.3":
- version "3.5.3"
- resolved "https://registry.yarnpkg.com/@types/jest-axe/-/jest-axe-3.5.3.tgz#5af918553388aa0a448af75603b44093985778c6"
- integrity sha512-ad9qI9f+00N8IlOuGh6dnZ6o0BDdV9VhGfTUr1zCejsPvOfZd6eohffe4JYxUoUuRYEftyMcaJ6Ux4+MsOpGHg==
- dependencies:
- "@types/jest" "*"
- axe-core "^3.5.5"
-
"@types/jest-specific-snapshot@^0.5.3":
version "0.5.5"
resolved "https://registry.yarnpkg.com/@types/jest-specific-snapshot/-/jest-specific-snapshot-0.5.5.tgz#47ce738870be99898ed6d7b08dbf0240c74ae553"
@@ -12235,16 +12227,6 @@ aws4@^1.12.0, aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3"
integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==
-axe-core@4.2.1:
- version "4.2.1"
- resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.2.1.tgz#2e50bcf10ee5b819014f6e342e41e45096239e34"
- integrity sha512-evY7DN8qSIbsW2H/TWQ1bX3sXN1d4MNb5Vb4n7BzPuCwRHdkZ1H2eNLuSh73EoQqkGKUtju2G2HCcjCfhvZIAA==
-
-axe-core@^3.5.5:
- version "3.5.6"
- resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.5.6.tgz#e762a90d7f6dbd244ceacb4e72760ff8aad521b5"
- integrity sha512-LEUDjgmdJoA3LqklSTwKYqkjcZ4HKc4ddIYGSAiSkr46NTjzg2L9RNB+lekO9P7Dlpa87+hBtzc2Fzn/+GUWMQ==
-
axe-core@^4.2.0, axe-core@^4.6.2:
version "4.7.2"
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.2.tgz#040a7342b20765cb18bb50b628394c21bccc17a0"
@@ -13428,14 +13410,6 @@ chalk@2.4.2, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
-chalk@4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
- integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==
- dependencies:
- ansi-styles "^4.1.0"
- supports-color "^7.1.0"
-
chalk@4.1.2, chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2, chalk@~4.1.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
@@ -15581,11 +15555,6 @@ diff-sequences@^26.6.2:
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1"
integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==
-diff-sequences@^27.5.1:
- version "27.5.1"
- resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327"
- integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==
-
diff-sequences@^29.4.3:
version "29.4.3"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2"
@@ -20316,16 +20285,6 @@ jake@^10.8.5:
filelist "^1.0.1"
minimatch "^3.0.4"
-jest-axe@^5.0.0:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/jest-axe/-/jest-axe-5.0.1.tgz#26c43643b2e5f2bd4900c1ab36f8283635957a6e"
- integrity sha512-MMOWA6gT4pcZGbTLS8ZEqABH08Lnj5bInfLPpn9ADWX2wFF++odbbh8csmSfkwKjHaioVPzlCtIypAtxFDx/rw==
- dependencies:
- axe-core "4.2.1"
- chalk "4.1.0"
- jest-matcher-utils "27.0.2"
- lodash.merge "4.6.2"
-
jest-canvas-mock@^2.5.2:
version "2.5.2"
resolved "https://registry.yarnpkg.com/jest-canvas-mock/-/jest-canvas-mock-2.5.2.tgz#7e21ebd75e05ab41c890497f6ba8a77f915d2ad6"
@@ -20424,16 +20383,6 @@ jest-diff@^26.0.0, jest-diff@^26.6.2:
jest-get-type "^26.3.0"
pretty-format "^26.6.2"
-jest-diff@^27.0.2:
- version "27.5.1"
- resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def"
- integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==
- dependencies:
- chalk "^4.0.0"
- diff-sequences "^27.5.1"
- jest-get-type "^27.5.1"
- pretty-format "^27.5.1"
-
jest-diff@^29.6.1:
version "29.6.1"
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.6.1.tgz#13df6db0a89ee6ad93c747c75c85c70ba941e545"
@@ -20493,11 +20442,6 @@ jest-get-type@^26.3.0:
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0"
integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==
-jest-get-type@^27.0.1, jest-get-type@^27.5.1:
- version "27.5.1"
- resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1"
- integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==
-
jest-get-type@^29.4.3:
version "29.4.3"
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.3.tgz#1ab7a5207c995161100b5187159ca82dd48b3dd5"
@@ -20551,16 +20495,6 @@ jest-leak-detector@^29.6.1:
jest-get-type "^29.4.3"
pretty-format "^29.6.1"
-jest-matcher-utils@27.0.2:
- version "27.0.2"
- resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.0.2.tgz#f14c060605a95a466cdc759acc546c6f4cbfc4f0"
- integrity sha512-Qczi5xnTNjkhcIB0Yy75Txt+Ez51xdhOxsukN7awzq2auZQGPHcQrJ623PZj0ECDEMOk2soxWx05EXdXGd1CbA==
- dependencies:
- chalk "^4.0.0"
- jest-diff "^27.0.2"
- jest-get-type "^27.0.1"
- pretty-format "^27.0.2"
-
jest-matcher-utils@^26.6.2:
version "26.6.2"
resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz#8e6fd6e863c8b2d31ac6472eeb237bc595e53e7a"
@@ -25310,7 +25244,7 @@ pretty-format@^26.0.0, pretty-format@^26.6.2:
ansi-styles "^4.0.0"
react-is "^17.0.1"
-pretty-format@^27.0.2, pretty-format@^27.5.1:
+pretty-format@^27.0.2:
version "27.5.1"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"
integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==
From a65c11b087fdf763fa9066d915d260f7eccd62b8 Mon Sep 17 00:00:00 2001
From: Tim Sullivan
Date: Thu, 25 Apr 2024 10:08:52 -0700
Subject: [PATCH 17/20] [Presentation/Dashboard] Remove usage of deprecated
React rendering utilities (#181597)
Pulled from https://github.com/elastic/kibana/pull/181356
---
.../kibana_context/root/root_provider.tsx | 2 +-
.../public/dashboard_app/dashboard_router.tsx | 47 ++++++++++---------
.../listing_page/dashboard_no_match.tsx | 19 +++++---
.../embeddable/api/show_settings.tsx | 9 ++--
.../embeddable/dashboard_container.tsx | 28 +++++------
.../dashboard_listing/confirm_overlays.tsx | 9 ++--
src/plugins/dashboard/public/plugin.tsx | 4 +-
.../public/services/settings/settings.stub.ts | 2 +
.../services/settings/settings_service.ts | 3 +-
.../public/services/settings/types.ts | 1 +
src/plugins/dashboard/tsconfig.json | 2 +
11 files changed, 70 insertions(+), 56 deletions(-)
diff --git a/packages/react/kibana_context/root/root_provider.tsx b/packages/react/kibana_context/root/root_provider.tsx
index a2af8f2eaa2f2..ad95cf8e437e5 100644
--- a/packages/react/kibana_context/root/root_provider.tsx
+++ b/packages/react/kibana_context/root/root_provider.tsx
@@ -17,7 +17,7 @@ export interface KibanaRootContextProviderProps extends KibanaEuiProviderProps {
/** The `I18nStart` API from `CoreStart`. */
i18n: I18nStart;
/** The `AnalyticsServiceStart` API from `CoreStart`. */
- analytics?: AnalyticsServiceStart;
+ analytics?: Pick;
}
/**
diff --git a/src/plugins/dashboard/public/dashboard_app/dashboard_router.tsx b/src/plugins/dashboard/public/dashboard_app/dashboard_router.tsx
index 2d479a8d9bf15..b9c6331bbb3ad 100644
--- a/src/plugins/dashboard/public/dashboard_app/dashboard_router.tsx
+++ b/src/plugins/dashboard/public/dashboard_app/dashboard_router.tsx
@@ -13,10 +13,9 @@ import { parse, ParsedQuery } from 'query-string';
import { render, unmountComponentAtNode } from 'react-dom';
import { HashRouter, RouteComponentProps, Redirect } from 'react-router-dom';
import { Routes, Route } from '@kbn/shared-ux-router';
-import { I18nProvider } from '@kbn/i18n-react';
import { ViewMode } from '@kbn/embeddable-plugin/public';
-import { AppMountParameters, CoreSetup } from '@kbn/core/public';
-import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
+import { AppMountParameters, CoreStart } from '@kbn/core/public';
+import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public';
import {
@@ -31,7 +30,6 @@ import { pluginServices } from '../services/plugin_services';
import { RedirectToProps } from '../dashboard_container/types';
import { createDashboardEditUrl } from '../dashboard_constants';
import { DashboardNoMatch } from './listing_page/dashboard_no_match';
-import { DashboardStart, DashboardStartDependencies } from '../plugin';
import { DashboardMountContext } from './hooks/dashboard_mount_context';
import { DashboardEmbedSettings, DashboardMountContextProps } from './types';
import { DashboardListingPage } from './listing_page/dashboard_listing_page';
@@ -47,11 +45,16 @@ export const dashboardUrlParams = {
export interface DashboardMountProps {
appUnMounted: () => void;
element: AppMountParameters['element'];
- core: CoreSetup;
+ coreStart: CoreStart;
mountContext: DashboardMountContextProps;
}
-export async function mountApp({ core, element, appUnMounted, mountContext }: DashboardMountProps) {
+export async function mountApp({
+ coreStart,
+ element,
+ appUnMounted,
+ mountContext,
+}: DashboardMountProps) {
const {
chrome: { setBadge, docTitle, setHelpExtension },
dashboardCapabilities: { showWriteControls },
@@ -145,25 +148,23 @@ export async function mountApp({ core, element, appUnMounted, mountContext }: Da
});
const app = (
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
+
);
setHelpExtension({
diff --git a/src/plugins/dashboard/public/dashboard_app/listing_page/dashboard_no_match.tsx b/src/plugins/dashboard/public/dashboard_app/listing_page/dashboard_no_match.tsx
index 895b7ce791fa7..bafc14ed5f5f6 100644
--- a/src/plugins/dashboard/public/dashboard_app/listing_page/dashboard_no_match.tsx
+++ b/src/plugins/dashboard/public/dashboard_app/listing_page/dashboard_no_match.tsx
@@ -12,7 +12,7 @@ import { RouteComponentProps } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { EuiCallOut } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
-import { toMountPoint } from '@kbn/kibana-react-plugin/public';
+import { toMountPoint } from '@kbn/react-kibana-mount';
import { LANDING_PAGE_PATH } from '../../dashboard_constants';
import { pluginServices } from '../../services/plugin_services';
@@ -23,9 +23,8 @@ let bannerId: string | undefined;
export const DashboardNoMatch = ({ history }: { history: RouteComponentProps['history'] }) => {
const { restorePreviousUrl } = useDashboardMountContext();
const {
- settings: {
- theme: { theme$ },
- },
+ analytics,
+ settings: { i18n: i18nStart, theme },
overlays: { banners },
urlForwarding: { navigateToLegacyKibanaUrl },
} = pluginServices.getServices();
@@ -55,7 +54,7 @@ export const DashboardNoMatch = ({ history }: { history: RouteComponentProps['hi
/>
,
- { theme$ }
+ { analytics, i18n: i18nStart, theme }
)
);
@@ -68,7 +67,15 @@ export const DashboardNoMatch = ({ history }: { history: RouteComponentProps['hi
history.replace(LANDING_PAGE_PATH);
}
- }, [restorePreviousUrl, navigateToLegacyKibanaUrl, banners, theme$, history]);
+ }, [
+ restorePreviousUrl,
+ navigateToLegacyKibanaUrl,
+ banners,
+ analytics,
+ i18nStart,
+ theme,
+ history,
+ ]);
return null;
};
diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/api/show_settings.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/api/show_settings.tsx
index 56798f5770dd1..564a05689dfe8 100644
--- a/src/plugins/dashboard/public/dashboard_container/embeddable/api/show_settings.tsx
+++ b/src/plugins/dashboard/public/dashboard_container/embeddable/api/show_settings.tsx
@@ -8,7 +8,7 @@
import React from 'react';
-import { toMountPoint } from '@kbn/kibana-react-plugin/public';
+import { toMountPoint } from '@kbn/react-kibana-mount';
import { pluginServices } from '../../../services/plugin_services';
import { DashboardSettings } from '../../component/settings/settings_flyout';
@@ -16,9 +16,8 @@ import { DashboardContainer, DashboardContainerContext } from '../dashboard_cont
export function showSettings(this: DashboardContainer) {
const {
- settings: {
- theme: { theme$ },
- },
+ analytics,
+ settings: { i18n, theme },
overlays,
} = pluginServices.getServices();
@@ -36,7 +35,7 @@ export function showSettings(this: DashboardContainer) {
}}
/>
,
- { theme$ }
+ { analytics, i18n, theme }
),
{
size: 's',
diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx
index 190462d69a2df..c2f9fd5184b17 100644
--- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx
+++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx
@@ -9,7 +9,7 @@
import { METRIC_TYPE } from '@kbn/analytics';
import { Reference } from '@kbn/content-management-utils';
import type { ControlGroupContainer } from '@kbn/controls-plugin/public';
-import type { KibanaExecutionContext, OverlayRef } from '@kbn/core/public';
+import type { I18nStart, KibanaExecutionContext, OverlayRef } from '@kbn/core/public';
import {
type PublishingSubject,
apiPublishesPanelTitle,
@@ -34,8 +34,7 @@ import {
type IEmbeddable,
} from '@kbn/embeddable-plugin/public';
import type { Filter, Query, TimeRange } from '@kbn/es-query';
-import { I18nProvider } from '@kbn/i18n-react';
-import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
+import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { TrackContentfulRender } from '@kbn/presentation-containers';
import { apiHasSerializableState, PanelPackage } from '@kbn/presentation-containers';
import { ReduxEmbeddableTools, ReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
@@ -166,7 +165,8 @@ export class DashboardContainer
private creationOptions?: DashboardCreationOptions;
private analyticsService: DashboardAnalyticsService;
private showWriteControls: DashboardCapabilitiesService['showWriteControls'];
- private theme$;
+ private i18n: I18nStart;
+ private theme;
private chrome;
private customBranding;
@@ -212,9 +212,7 @@ export class DashboardContainer
({
analytics: this.analyticsService,
- settings: {
- theme: { theme$: this.theme$ },
- },
+ settings: { theme: this.theme, i18n: this.i18n },
chrome: this.chrome,
customBranding: this.customBranding,
dashboardCapabilities: { showWriteControls: this.showWriteControls },
@@ -355,17 +353,19 @@ export class DashboardContainer
this.domNode.className = 'dashboardContainer';
ReactDOM.render(
-
+
-
-
-
-
-
+
+
+
- ,
+ ,
dom
);
}
diff --git a/src/plugins/dashboard/public/dashboard_listing/confirm_overlays.tsx b/src/plugins/dashboard/public/dashboard_listing/confirm_overlays.tsx
index 40f4367059eff..c8968ef3d7f78 100644
--- a/src/plugins/dashboard/public/dashboard_listing/confirm_overlays.tsx
+++ b/src/plugins/dashboard/public/dashboard_listing/confirm_overlays.tsx
@@ -21,7 +21,7 @@ import {
EUI_MODAL_CANCEL_BUTTON,
} from '@elastic/eui';
import { ViewMode } from '@kbn/embeddable-plugin/public';
-import { toMountPoint } from '@kbn/kibana-react-plugin/public';
+import { toMountPoint } from '@kbn/react-kibana-mount';
import { pluginServices } from '../services/plugin_services';
import { createConfirmStrings, resetConfirmStrings } from './_dashboard_listing_strings';
@@ -57,9 +57,8 @@ export const confirmCreateWithUnsaved = (
const descriptionId = 'confirmDiscardOrKeepDescription';
const {
- settings: {
- theme: { theme$ },
- },
+ analytics,
+ settings: { i18n, theme },
overlays: { openModal },
} = pluginServices.getServices();
@@ -120,7 +119,7 @@ export const confirmCreateWithUnsaved = (
,
- { theme$ }
+ { analytics, i18n, theme }
),
{
'data-test-subj': 'dashboardCreateConfirmModal',
diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx
index 1a99f99f8306e..a8e94ebf37e3e 100644
--- a/src/plugins/dashboard/public/plugin.tsx
+++ b/src/plugins/dashboard/public/plugin.tsx
@@ -257,6 +257,8 @@ export class DashboardPlugin
const { mountApp } = await import('./dashboard_app/dashboard_router');
appMounted();
+ const [coreStart] = await core.getStartServices();
+
const mountContext: DashboardMountContextProps = {
restorePreviousUrl,
scopedHistory: () => this.currentHistory!,
@@ -265,7 +267,7 @@ export class DashboardPlugin
};
return mountApp({
- core,
+ coreStart,
appUnMounted,
element: params.element,
mountContext,
diff --git a/src/plugins/dashboard/public/services/settings/settings.stub.ts b/src/plugins/dashboard/public/services/settings/settings.stub.ts
index 3b93cb65b4a73..6439ec290541d 100644
--- a/src/plugins/dashboard/public/services/settings/settings.stub.ts
+++ b/src/plugins/dashboard/public/services/settings/settings.stub.ts
@@ -9,12 +9,14 @@
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks';
import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public';
+import { i18nServiceMock } from '@kbn/core-i18n-browser-mocks';
import { DashboardSettingsService } from './types';
type SettingsServiceFactory = PluginServiceFactory
;
export const settingsServiceFactory: SettingsServiceFactory = () => {
return {
+ i18n: i18nServiceMock.createStartContract(),
uiSettings: uiSettingsServiceMock.createStartContract(),
theme: themeServiceMock.createStartContract(),
isProjectEnabledInLabs: jest.fn().mockReturnValue(true),
diff --git a/src/plugins/dashboard/public/services/settings/settings_service.ts b/src/plugins/dashboard/public/services/settings/settings_service.ts
index 71064991d0278..8676e1e8a87bb 100644
--- a/src/plugins/dashboard/public/services/settings/settings_service.ts
+++ b/src/plugins/dashboard/public/services/settings/settings_service.ts
@@ -16,7 +16,7 @@ export type SettingsServiceFactory = KibanaPluginServiceFactory<
>;
export const settingsServiceFactory: SettingsServiceFactory = ({ coreStart, startPlugins }) => {
- const { uiSettings, theme } = coreStart;
+ const { i18n, uiSettings, theme } = coreStart;
const {
presentationUtil: {
@@ -25,6 +25,7 @@ export const settingsServiceFactory: SettingsServiceFactory = ({ coreStart, star
} = startPlugins;
return {
+ i18n,
uiSettings,
theme,
isProjectEnabledInLabs: isProjectEnabled,
diff --git a/src/plugins/dashboard/public/services/settings/types.ts b/src/plugins/dashboard/public/services/settings/types.ts
index 299981ff6093d..90b64762b9e21 100644
--- a/src/plugins/dashboard/public/services/settings/types.ts
+++ b/src/plugins/dashboard/public/services/settings/types.ts
@@ -11,6 +11,7 @@ import type { PresentationLabsService } from '@kbn/presentation-util-plugin/publ
export interface DashboardSettingsService {
uiSettings: CoreStart['uiSettings'];
+ i18n: CoreStart['i18n'];
theme: CoreStart['theme'];
isProjectEnabledInLabs: PresentationLabsService['isProjectEnabled'];
}
diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json
index 2f4b399a727ef..2542cb77de613 100644
--- a/src/plugins/dashboard/tsconfig.json
+++ b/src/plugins/dashboard/tsconfig.json
@@ -76,6 +76,8 @@
"@kbn/managed-content-badge",
"@kbn/core-test-helpers-model-versions",
"@kbn/deeplinks-analytics",
+ "@kbn/react-kibana-context-render",
+ "@kbn/core-i18n-browser-mocks",
],
"exclude": ["target/**/*"]
}
From 35974ca8166ebc702308f04b9333c47b9ff04cfe Mon Sep 17 00:00:00 2001
From: Sander Philipse <94373878+sphilipse@users.noreply.github.com>
Date: Thu, 25 Apr 2024 19:44:49 +0200
Subject: [PATCH 18/20] [Search] Fix mapping tab breaking docLinks and refresh
(#181729)
## Summary
This fixes two issues on the mappings tab in Search:
- A frequent refresh caused by input changes unrelated to the mappings
component
- Doclinks breaking because they hadn't been initialized yet
---
.../search_index/index_mappings.tsx | 20 +++++++++++++++----
.../field_parameters/select_inference_id.tsx | 14 +++++++------
.../index_mapping_with_context.tsx | 2 ++
.../index_mappings_embeddable.tsx | 6 ------
4 files changed, 26 insertions(+), 16 deletions(-)
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_mappings.tsx
index b7e5a117c638f..b35415cc2e0e0 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_mappings.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_mappings.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
import { useActions, useValues } from 'kea';
@@ -46,7 +46,9 @@ import './index_mappings.scss';
export const SearchIndexIndexMappings: React.FC = () => {
const { indexName } = useValues(IndexNameLogic);
const { hasDocumentLevelSecurityFeature, isHiddenIndex } = useValues(IndexViewLogic);
- const { indexMappingComponent: IndexMappingComponent, productFeatures } = useValues(KibanaLogic);
+ const { indexMappingComponent, productFeatures } = useValues(KibanaLogic);
+
+ const IndexMappingComponent = useMemo(() => indexMappingComponent, []);
const [selectedIndexType, setSelectedIndexType] =
useState('content-index');
@@ -154,7 +156,12 @@ export const SearchIndexIndexMappings: React.FC = () => {
-
+
{i18n.translate('xpack.enterpriseSearch.content.searchIndex.mappings.docLink', {
defaultMessage: 'Learn how to customize index mappings and settings',
})}
@@ -187,7 +194,12 @@ export const SearchIndexIndexMappings: React.FC = () => {
-
+
{i18n.translate('xpack.enterpriseSearch.content.searchIndex.transform.docLink', {
defaultMessage: 'Learn more',
})}
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/select_inference_id.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/select_inference_id.tsx
index da1a5c4c33fc8..997920f5e8581 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/select_inference_id.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/select_inference_id.tsx
@@ -141,7 +141,7 @@ export const SelectInferenceId = ({ onChange, 'data-test-subj': dataTestSubj }:
return subscription.unsubscribe;
}, [subscribe, onChange]);
- const selectedOptions = options.filter((option) => option.checked).find((k) => k.label);
+ const selectedOptionLabel = options.find((option) => option.checked)?.label;
const [isInferencePopoverVisible, setIsInferencePopoverVisible] = useState(false);
const [inferenceEndpointError, setInferenceEndpointError] = useState(
undefined
@@ -180,11 +180,13 @@ export const SelectInferenceId = ({ onChange, 'data-test-subj': dataTestSubj }:
setIsInferencePopoverVisible(!isInferencePopoverVisible);
}}
>
-
+ {selectedOptionLabel ||
+ i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.parameters.inferenceId.popover.defaultLabel',
+ {
+ defaultMessage: 'No model selected',
+ }
+ )}
>
)}
diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/index_mapping_with_context.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/index_mapping_with_context.tsx
index 5f8eb213a2af5..7aa0f07e8e492 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/index_mapping_with_context.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/index_mapping_with_context.tsx
@@ -6,6 +6,7 @@
*/
import React from 'react';
+import { documentationService } from '../../../../services';
import { UIM_APP_NAME } from '../../../../../../common/constants/ui_metric';
import { httpService } from '../../../../services/http';
import { notificationService } from '../../../../services/notification';
@@ -28,6 +29,7 @@ export const IndexMappingWithContext: React.FC = (
httpService.setup(core.http);
notificationService.setup(core.notifications);
}
+ documentationService.setup(core.docLinks);
const newDependencies: AppDependencies = {
...dependencies,
diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/index_mappings_embeddable.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/index_mappings_embeddable.tsx
index 9102ba14859cd..8a9d4c1b2c4f4 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/index_mappings_embeddable.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/index_mappings_embeddable.tsx
@@ -10,12 +10,6 @@ import { dynamic } from '@kbn/shared-ux-utility';
import React, { Suspense, ComponentType } from 'react';
import { IndexMappingWithContextProps } from './index_mapping_with_context_types';
-// const IndexMappingWithContext = lazy>(async () => {
-// return {
-// default: (await import('./index_mapping_with_context')).IndexMappingWithContext,
-// };
-// });
-
const IndexMappingWithContext = dynamic>(() =>
import('./index_mapping_with_context').then((mod) => ({ default: mod.IndexMappingWithContext }))
);
From 90b4c1ca4a4038965134b96a229a59b9b1030936 Mon Sep 17 00:00:00 2001
From: Nicolas Chaulet
Date: Thu, 25 Apr 2024 14:32:55 -0400
Subject: [PATCH 19/20] [Fleet] Scroll to top when changing agent activity date
filter (#181727)
---
.../agent_activity_flyout/flyout_body.tsx | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_activity_flyout/flyout_body.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_activity_flyout/flyout_body.tsx
index fa8467b5c16ff..6880d0bd7747f 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_activity_flyout/flyout_body.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_activity_flyout/flyout_body.tsx
@@ -36,6 +36,12 @@ const ButtonsFlexGroup = styled(EuiFlexGroup)`
padding-left: 24px;
`;
+const ScrollAnchor = styled.div`
+ height: 0;
+ margin: 0;
+ padding: 0;
+`;
+
export const FlyoutBody: React.FunctionComponent<{
isFirstLoading: boolean;
currentActions: ActionStatus[];
@@ -57,6 +63,13 @@ export const FlyoutBody: React.FunctionComponent<{
onChangeDateFilter,
agentPolicies,
}) => {
+ const scrollToTopRef = React.useRef(null);
+ React.useEffect(() => {
+ // Condition needed for jest tests as scrollIntoView is not implemented in jsdom
+ if (scrollToTopRef.current?.scrollIntoView) {
+ scrollToTopRef.current.scrollIntoView();
+ }
+ }, [dateFilter]);
// Loading
if (isFirstLoading) {
return (
@@ -79,6 +92,7 @@ export const FlyoutBody: React.FunctionComponent<{
if (currentActions.length === 0) {
return (
+
+
From b92890a0511d3b0de1d62552dd0f658034f41a9f Mon Sep 17 00:00:00 2001
From: Elena Stoeva <59341489+ElenaStoeva@users.noreply.github.com>
Date: Thu, 25 Apr 2024 19:43:01 +0100
Subject: [PATCH 20/20] [Index Templates][Serverless] Update api integration
tests for _source field (#181742)
## Summary
This PR updates the Index templates API integration tests for serverless
to not use the `_source` property in the mock template as this property
is not supported in serverless.
---
.../index_management/lib/templates.helpers.ts | 31 ++++++++++----
.../index_management/index_templates.ts | 41 +++++++++++++++----
2 files changed, 57 insertions(+), 15 deletions(-)
diff --git a/x-pack/test/api_integration/apis/management/index_management/lib/templates.helpers.ts b/x-pack/test/api_integration/apis/management/index_management/lib/templates.helpers.ts
index c64198ae3adcc..ea1e9a2d83bb1 100644
--- a/x-pack/test/api_integration/apis/management/index_management/lib/templates.helpers.ts
+++ b/x-pack/test/api_integration/apis/management/index_management/lib/templates.helpers.ts
@@ -14,9 +14,6 @@ const templateMock = {
number_of_shards: 1,
},
mappings: {
- _source: {
- enabled: false,
- },
properties: {
host_name: {
type: 'keyword',
@@ -31,6 +28,22 @@ const templateMock = {
alias1: {},
},
};
+
+const getTemplateMock = (isMappingsSourceFieldEnabled: boolean) => {
+ if (isMappingsSourceFieldEnabled) {
+ return {
+ ...templateMock,
+ mappings: {
+ ...templateMock.mappings,
+ _source: {
+ enabled: false,
+ },
+ },
+ };
+ }
+ return templateMock;
+};
+
export function templatesHelpers(getService: FtrProviderContext['getService']) {
const es = getService('es');
@@ -39,13 +52,14 @@ export function templatesHelpers(getService: FtrProviderContext['getService']) {
const getTemplatePayload = (
name: string,
indexPatterns: string[] = INDEX_PATTERNS,
- isLegacy: boolean = false
+ isLegacy: boolean = false,
+ isMappingsSourceFieldEnabled: boolean = true
) => {
const baseTemplate: TemplateDeserialized = {
name,
indexPatterns,
version: 1,
- template: { ...templateMock },
+ template: { ...getTemplateMock(isMappingsSourceFieldEnabled) },
_kbnMeta: {
isLegacy,
type: 'default',
@@ -63,10 +77,13 @@ export function templatesHelpers(getService: FtrProviderContext['getService']) {
return baseTemplate;
};
- const getSerializedTemplate = (indexPatterns: string[] = INDEX_PATTERNS): TemplateSerialized => {
+ const getSerializedTemplate = (
+ indexPatterns: string[] = INDEX_PATTERNS,
+ isMappingsSourceFieldEnabled: boolean = true
+ ): TemplateSerialized => {
return {
index_patterns: indexPatterns,
- template: { ...templateMock },
+ template: { ...getTemplateMock(isMappingsSourceFieldEnabled) },
};
};
diff --git a/x-pack/test_serverless/api_integration/test_suites/common/index_management/index_templates.ts b/x-pack/test_serverless/api_integration/test_suites/common/index_management/index_templates.ts
index 1ec1cdc31b7c3..6509512171320 100644
--- a/x-pack/test_serverless/api_integration/test_suites/common/index_management/index_templates.ts
+++ b/x-pack/test_serverless/api_integration/test_suites/common/index_management/index_templates.ts
@@ -138,13 +138,18 @@ export default function ({ getService }: FtrProviderContext) {
describe('create', () => {
it('should create an index template', async () => {
- const payload = getTemplatePayload(`template-${getRandomString()}`, [getRandomString()]);
+ const payload = getTemplatePayload(
+ `template-${getRandomString()}`,
+ [getRandomString()],
+ undefined,
+ false
+ );
await createTemplate(payload).set('x-elastic-internal-origin', 'xxx').expect(200);
});
it('should throw a 409 conflict when trying to create 2 templates with the same name', async () => {
const templateName = `template-${getRandomString()}`;
- const payload = getTemplatePayload(templateName, [getRandomString()]);
+ const payload = getTemplatePayload(templateName, [getRandomString()], undefined, false);
await createTemplate(payload).set('x-elastic-internal-origin', 'xxx');
@@ -154,7 +159,12 @@ export default function ({ getService }: FtrProviderContext) {
it('should validate the request payload', async () => {
const templateName = `template-${getRandomString()}`;
// need to cast as any to avoid errors after deleting index patterns
- const payload = getTemplatePayload(templateName, [getRandomString()]) as any;
+ const payload = getTemplatePayload(
+ templateName,
+ [getRandomString()],
+ undefined,
+ false
+ ) as any;
delete payload.indexPatterns; // index patterns are required
@@ -166,7 +176,12 @@ export default function ({ getService }: FtrProviderContext) {
it('should parse the ES error and return the cause', async () => {
const templateName = `template-create-parse-es-error`;
- const payload = getTemplatePayload(templateName, ['create-parse-es-error']);
+ const payload = getTemplatePayload(
+ templateName,
+ ['create-parse-es-error'],
+ undefined,
+ false
+ );
const runtime = {
myRuntimeField: {
type: 'boolean',
@@ -190,7 +205,12 @@ export default function ({ getService }: FtrProviderContext) {
describe('update', () => {
it('should update an index template', async () => {
const templateName = `template-${getRandomString()}`;
- const indexTemplate = getTemplatePayload(templateName, [getRandomString()]);
+ const indexTemplate = getTemplatePayload(
+ templateName,
+ [getRandomString()],
+ undefined,
+ false
+ );
await createTemplate(indexTemplate).set('x-elastic-internal-origin', 'xxx').expect(200);
@@ -217,7 +237,12 @@ export default function ({ getService }: FtrProviderContext) {
it('should parse the ES error and return the cause', async () => {
const templateName = `template-update-parse-es-error`;
- const payload = getTemplatePayload(templateName, ['update-parse-es-error']);
+ const payload = getTemplatePayload(
+ templateName,
+ ['update-parse-es-error'],
+ undefined,
+ false
+ );
const runtime = {
myRuntimeField: {
type: 'keyword',
@@ -247,7 +272,7 @@ export default function ({ getService }: FtrProviderContext) {
describe('delete', () => {
it('should delete an index template', async () => {
const templateName = `template-${getRandomString()}`;
- const payload = getTemplatePayload(templateName, [getRandomString()]);
+ const payload = getTemplatePayload(templateName, [getRandomString()], undefined, false);
const { status: createStatus, body: createBody } = await createTemplate(payload).set(
'x-elastic-internal-origin',
@@ -283,7 +308,7 @@ export default function ({ getService }: FtrProviderContext) {
describe('simulate', () => {
it('should simulate an index template', async () => {
- const payload = getSerializedTemplate([getRandomString()]);
+ const payload = getSerializedTemplate([getRandomString()], false);
const { body } = await simulateTemplate(payload)
.set('x-elastic-internal-origin', 'xxx')