From 2bc5871633d3aaa539d119bd9aa378665af1fcdf Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Tue, 11 Mar 2025 16:51:30 +0100 Subject: [PATCH 1/2] [Security Solution] Siem migrations Onboarding UI changes (#212560) ## Summary 1/3 of https://github.com/elastic/security-team/issues/11696 **Done** - UI changes in the onboarding cards **Pending** - UI changes in the upload form - UI changes in the translated rules page ### Screenshots **Processing** Old ![processing_old](https://github.com/user-attachments/assets/7a757641-0a68-40bc-a808-e98b0b7ea755) New ![processing_new](https://github.com/user-attachments/assets/395246d1-42dc-4be2-9863-0af7c87e9aca) **Results** Old ![result_old](https://github.com/user-attachments/assets/149634fb-fec0-456c-83f4-d8d024941094) New ![result_new](https://github.com/user-attachments/assets/35a01483-f273-4710-9bb4-709eaf08bc21) **Connectors** Text changes when the EIS connector is selected https://github.com/user-attachments/assets/f819c379-42a1-4dc8-b320-aa5fd5b7639a (cherry picked from commit b7412d94e76407477250cec38c01242f9e771000) # Conflicts: # x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts --- .../shared/kbn-doc-links/src/get_doc_links.ts | 2 + .../shared/kbn-doc-links/src/types.ts | 2 + .../components/panel_text/panel_text.tsx | 40 +++--- .../onboarding_body/cards/alerts/index.ts | 6 +- .../cards/common/card_callout.styles.ts | 2 +- .../cards/common/card_icon.tsx | 66 --------- .../connectors/connector_selector_panel.tsx | 12 +- .../onboarding_body/cards/dashboards/index.ts | 6 +- .../cards/integrations/index.ts | 6 +- .../onboarding_body/cards/rules/index.ts | 6 +- .../ai_connector/ai_connector_card.tsx | 56 +++++++- .../ai_connector/translations.ts | 26 +++- .../siem_migrations/start_migration/index.ts | 9 +- .../start_migration/start_migration_card.tsx | 19 +-- .../start_migration_check_complete.test.ts | 11 +- .../start_migration_check_complete.ts | 17 ++- .../start_migration/translations.ts | 2 +- .../start_migration/upload_rules_panel.tsx | 3 +- .../onboarding_body/onboarding_body.test.tsx | 11 +- .../onboarding_body/onboarding_body.tsx | 5 +- .../onboarding_card_panel.test.tsx | 74 +++++++++- .../onboarding_body/onboarding_card_panel.tsx | 33 ++++- .../onboarding_card_panel_badge.tsx | 55 +++++++ .../components/onboarding_context.tsx | 38 +---- .../components/onboarding_telemetry.test.ts | 115 +++++++++++++++ .../components/onboarding_telemetry.ts | 61 ++++++++ .../public/onboarding/constants.ts | 2 +- .../public/onboarding/types.ts | 8 +- .../siem_migrations/common/icon/index.tsx | 14 +- .../common/icon/siem_migrations.svg | 68 ++++++--- .../common/icon/siem_migrations_dark.svg | 68 +++++++++ .../migration_progress_panel.tsx | 5 +- .../migration_result_panel.tsx | 36 +++-- .../migration_status_panels/read_more.tsx | 2 +- .../migration_status_panels/translations.ts | 30 ++-- .../upload_missing_panel.tsx | 135 +++++++++++------- .../rules/utils/translation_results/index.ts | 20 +-- 37 files changed, 789 insertions(+), 282 deletions(-) delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_icon.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel_badge.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_telemetry.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_telemetry.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/icon/siem_migrations_dark.svg diff --git a/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts b/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts index a121c39a9e984..e41ae419993e6 100644 --- a/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts +++ b/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts @@ -532,6 +532,8 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D signalsMigrationApi: `${SECURITY_SOLUTION_DOCS}signals-migration-api.html`, legacyEndpointManagementApiDeprecations: `${KIBANA_DOCS}breaking-changes-summary.html#breaking-199598`, legacyRuleManagementBulkApiDeprecations: `${KIBANA_DOCS}breaking-changes-summary.html#breaking-207091`, + siemMigrations: `${SECURITY_SOLUTION_DOCS}siem-migration.html`, + llmPerformanceMatrix: `${SECURITY_SOLUTION_DOCS}llm-performance-matrix.html`, }, query: { eql: `${ELASTICSEARCH_DOCS}eql.html`, diff --git a/src/platform/packages/shared/kbn-doc-links/src/types.ts b/src/platform/packages/shared/kbn-doc-links/src/types.ts index 04a6b5e0587f2..36c07d46e6159 100644 --- a/src/platform/packages/shared/kbn-doc-links/src/types.ts +++ b/src/platform/packages/shared/kbn-doc-links/src/types.ts @@ -390,6 +390,8 @@ export interface DocLinks { readonly signalsMigrationApi: string; readonly legacyEndpointManagementApiDeprecations: string; readonly legacyRuleManagementBulkApiDeprecations: string; + readonly siemMigrations: string; + readonly llmPerformanceMatrix: string; }; readonly query: { readonly eql: string; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/panel_text/panel_text.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/panel_text/panel_text.tsx index 5c1fc6746bcd0..b0bf57ad255f5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/panel_text/panel_text.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/panel_text/panel_text.tsx @@ -11,25 +11,31 @@ import { EuiText, useEuiTheme, COLOR_MODES_STANDARD, type EuiTextProps } from '@ export interface PanelTextProps extends PropsWithChildren { subdued?: true; semiBold?: true; + cursive?: true; } -export const PanelText = React.memo(({ children, subdued, semiBold, ...props }) => { - const { euiTheme, colorMode } = useEuiTheme(); - const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark; +export const PanelText = React.memo( + ({ children, subdued, semiBold, cursive, ...props }) => { + const { euiTheme, colorMode } = useEuiTheme(); + const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark; - let color; - if (subdued && !isDarkMode) { - color = 'subdued'; - } + let color; + if (subdued && !isDarkMode) { + color = 'subdued'; + } - const style: CSSInterpolation = {}; - if (semiBold) { - style.fontWeight = euiTheme.font.weight.semiBold; - } + const style: CSSInterpolation = {}; + if (semiBold) { + style.fontWeight = euiTheme.font.weight.semiBold; + } + if (cursive) { + style.fontStyle = 'italic'; + } - return ( - - {children} - - ); -}); + return ( + + {children} + + ); + } +); PanelText.displayName = 'PanelText'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/index.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/index.ts index 2cc2e7ecd45fe..97eb85636913f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/index.ts @@ -9,12 +9,14 @@ import React from 'react'; import type { OnboardingCardConfig } from '../../../../types'; import { OnboardingCardId } from '../../../../constants'; import { ALERTS_CARD_TITLE } from './translations'; -import { getCardIcon } from '../common/card_icon'; +import alertsIcon from './images/alerts_icon.png'; +import alertsDarkIcon from './images/alerts_icon_dark.png'; export const alertsCardConfig: OnboardingCardConfig = { id: OnboardingCardId.alerts, title: ALERTS_CARD_TITLE, - icon: () => getCardIcon(OnboardingCardId.alerts), + icon: alertsIcon, + iconDark: alertsDarkIcon, Component: React.lazy( () => import( diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_callout.styles.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_callout.styles.ts index c8806a7ca669b..809f8df916682 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_callout.styles.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_callout.styles.ts @@ -12,7 +12,7 @@ export const useCardCallOutStyles = () => { const { euiTheme } = useEuiTheme(); return css` padding: ${euiTheme.size.s}; - border: ${euiTheme.border.width.thin} solid ${euiTheme.colors.backgroundBaseSubdued}; + border: ${euiTheme.border.width.thin} solid ${euiTheme.colors.borderBaseSubdued}; border-radius: ${euiTheme.size.s}; `; }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_icon.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_icon.tsx deleted file mode 100644 index c60ca58404ccb..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_icon.tsx +++ /dev/null @@ -1,66 +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 React from 'react'; -import { useDarkMode } from '@kbn/kibana-react-plugin/public'; -import { OnboardingCardId } from '../../../../constants'; -import rulesIcon from '../rules/images/rules_icon.png'; -import rulesDarkIcon from '../rules/images/rules_icon_dark.png'; -import integrationsIcon from '../integrations/images/integrations_icon.png'; -import integrationsDarkIcon from '../integrations/images/integrations_icon_dark.png'; -import dashboardsIcon from '../dashboards/images/dashboards_icon.png'; -import dashboardsDarkIcon from '../dashboards/images/dashboards_icon_dark.png'; -import alertsIcon from '../alerts/images/alerts_icon.png'; -import alertsDarkIcon from '../alerts/images/alerts_icon_dark.png'; -import startMigrationIcon from '../siem_migrations/start_migration/images/start_migration_icon.png'; -import startMigrationDarkIcon from '../siem_migrations/start_migration/images/start_migration_icon_dark.png'; - -interface CardIcons { - [key: string]: { - light: string; - dark: string; - }; -} - -const cardIcons: CardIcons = { - [OnboardingCardId.rules]: { - light: rulesIcon, - dark: rulesDarkIcon, - }, - [OnboardingCardId.integrations]: { - light: integrationsIcon, - dark: integrationsDarkIcon, - }, - [OnboardingCardId.dashboards]: { - light: dashboardsIcon, - dark: dashboardsDarkIcon, - }, - [OnboardingCardId.alerts]: { - light: alertsIcon, - dark: alertsDarkIcon, - }, - [OnboardingCardId.siemMigrationsStart]: { - light: startMigrationIcon, - dark: startMigrationDarkIcon, - }, -}; - -interface CardIconProps { - cardId: OnboardingCardId; -} - -export const CardIcon = React.memo(({ cardId }) => { - const isDarkMode = useDarkMode(); - const icon = cardIcons[cardId]?.[isDarkMode ? 'dark' : 'light'] || ''; - - if (!icon) return null; - - return {`${cardId}-card-icon`}; -}); - -CardIcon.displayName = 'CardIcon'; - -export const getCardIcon = (cardId: OnboardingCardId) => ; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_selector_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_selector_panel.tsx index 01ad467b0086d..2008b86d1c284 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_selector_panel.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/connector_selector_panel.tsx @@ -33,10 +33,18 @@ export const ConnectorSelectorPanel = React.memo( ); useEffect(() => { - if (connectors.length === 1) { + if (selectedConnectorId || !connectors.length) { + return; + } + const inferenceConnector = connectors.find( + ({ actionTypeId }) => actionTypeId === '.inference' + ); + if (inferenceConnector) { + onConnectorSelected(inferenceConnector); + } else if (connectors.length === 1) { onConnectorSelected(connectors[0]); } - }, [connectors, onConnectorSelected]); + }, [selectedConnectorId, connectors, onConnectorSelected]); const connectorOptions = useMemo( () => diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts index 4d2c07f41b542..e744b999ecca1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts @@ -9,12 +9,14 @@ import React from 'react'; import type { OnboardingCardConfig } from '../../../../types'; import { OnboardingCardId } from '../../../../constants'; import { DASHBOARDS_CARD_TITLE } from './translations'; -import { getCardIcon } from '../common/card_icon'; +import dashboardsIcon from './images/dashboards_icon.png'; +import dashboardsDarkIcon from './images/dashboards_icon_dark.png'; export const dashboardsCardConfig: OnboardingCardConfig = { id: OnboardingCardId.dashboards, title: DASHBOARDS_CARD_TITLE, - icon: () => getCardIcon(OnboardingCardId.dashboards), + icon: dashboardsIcon, + iconDark: dashboardsDarkIcon, Component: React.lazy( () => import( diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts index 997207ba90066..ba7b89d5aba7b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts @@ -11,14 +11,16 @@ import type { OnboardingCardConfig } from '../../../../types'; import { checkIntegrationsCardComplete } from './integrations_check_complete'; import { OnboardingCardId } from '../../../../constants'; import type { IntegrationCardMetadata } from './types'; -import { getCardIcon } from '../common/card_icon'; +import integrationsIcon from './images/integrations_icon.png'; +import integrationsDarkIcon from './images/integrations_icon_dark.png'; export const integrationsCardConfig: OnboardingCardConfig = { id: OnboardingCardId.integrations, title: i18n.translate('xpack.securitySolution.onboarding.integrationsCard.title', { defaultMessage: 'Add data with integrations', }), - icon: () => getCardIcon(OnboardingCardId.integrations), + icon: integrationsIcon, + iconDark: integrationsDarkIcon, Component: React.lazy( () => import( diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/index.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/index.ts index 93e050bf28854..59b86df834646 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/index.ts @@ -10,12 +10,14 @@ import type { OnboardingCardConfig } from '../../../../types'; import { OnboardingCardId } from '../../../../constants'; import { RULES_CARD_TITLE } from './translations'; import { checkRulesComplete } from './rules_check_complete'; -import { getCardIcon } from '../common/card_icon'; +import rulesIcon from './images/rules_icon.png'; +import rulesDarkIcon from './images/rules_icon_dark.png'; export const rulesCardConfig: OnboardingCardConfig = { id: OnboardingCardId.rules, title: RULES_CARD_TITLE, - icon: () => getCardIcon(OnboardingCardId.rules), + icon: rulesIcon, + iconDark: rulesDarkIcon, Component: React.lazy( () => import( diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx index 682e853381770..5d06165c3e81a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import React, { useCallback } from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; import { CenteredLoadingSpinner } from '../../../../../../common/components/centered_loading_spinner'; import { useKibana } from '../../../../../../common/lib/kibana/kibana_react'; import { useDefinedLocalStorage } from '../../../../hooks/use_stored_state'; @@ -19,6 +20,26 @@ import { ConnectorsMissingPrivilegesCallOut } from '../../common/connectors/miss import type { AIConnector } from '../../common/connectors/types'; import type { AIConnectorCardMetadata } from './types'; +const LlmPerformanceMatrixDocsLink = React.memo<{ text: string }>(({ text }) => { + const { llmPerformanceMatrix } = useKibana().services.docLinks.links.securitySolution; + return ( + + {text} + + ); +}); +LlmPerformanceMatrixDocsLink.displayName = 'LlmPerformanceMatrixDocsLink'; + +const SiemMigrationDocsLink = React.memo<{ text: string }>(({ text }) => { + const { siemMigrations } = useKibana().services.docLinks.links.securitySolution; + return ( + + {text} + + ); +}); +SiemMigrationDocsLink.displayName = 'SiemMigrationDocsLink'; + export const AIConnectorCard: OnboardingCardComponent = ({ checkCompleteMetadata, checkComplete, @@ -38,6 +59,14 @@ export const AIConnectorCard: OnboardingCardComponent = [setComplete, setStoredConnectorId, siemMigrations] ); + const isInferenceConnector = useMemo(() => { + if (!checkCompleteMetadata?.connectors?.length || !storedConnectorId) { + return false; + } + const connector = checkCompleteMetadata.connectors.find((c) => c.id === storedConnectorId); + return connector?.actionTypeId === '.inference' ?? false; + }, [checkCompleteMetadata, storedConnectorId]); + if (!checkCompleteMetadata) { return ( @@ -53,7 +82,28 @@ export const AIConnectorCard: OnboardingCardComponent = {canExecuteConnectors ? ( - {i18n.AI_CONNECTOR_CARD_DESCRIPTION} + + {i18n.AI_CONNECTOR_CARD_DESCRIPTION_START} + {isInferenceConnector ? ( + , + docsLink: , + }} + /> + ) : ( + , + docsLink: , + }} + /> + )} + = { - id: OnboardingCardId.siemMigrationsStart, + id: OnboardingCardId.siemMigrationsRules, title: START_MIGRATION_CARD_TITLE, - icon: () => getCardIcon(OnboardingCardId.siemMigrationsStart), + badge: 'tech_preview', + icon: startMigrationIcon, + iconDark: startMigrationDarkIcon, Component: React.lazy( () => import( diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx index 10b91e57ed2c1..e360438b89d9d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx @@ -27,13 +27,13 @@ import { import { UploadRulesSectionPanel } from './upload_rules_panel'; const StartMigrationsBody: OnboardingCardComponent = React.memo( - ({ setComplete, isCardComplete, setExpandedCardId }) => { + ({ setComplete, isCardComplete, setExpandedCardId, checkComplete }) => { const styles = useStyles(); const { data: migrationsStats, isLoading, refreshStats } = useLatestStats(); useEffect(() => { // Set card complete if any migration is finished - if (!isCardComplete(OnboardingCardId.siemMigrationsStart) && migrationsStats) { + if (!isCardComplete(OnboardingCardId.siemMigrationsRules) && migrationsStats) { if (migrationsStats.some(({ status }) => status === SiemMigrationTaskStatus.FINISHED)) { setComplete(true); } @@ -49,13 +49,14 @@ const StartMigrationsBody: OnboardingCardComponent = React.memo( setExpandedCardId(OnboardingCardId.siemMigrationsAiConnectors); }, [setExpandedCardId]); + const onFlyoutClosed = useCallback(() => { + refreshStats(); + checkComplete(); + }, [refreshStats, checkComplete]); + return ( - - + + {isLoading ? ( ) : ( @@ -66,7 +67,7 @@ const StartMigrationsBody: OnboardingCardComponent = React.memo( /> )} - +

{i18n.START_MIGRATION_CARD_FOOTER_NOTE}

diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.test.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.test.ts index bd30310d054ae..03555b3792546 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.test.ts @@ -11,7 +11,7 @@ import type { SiemMigrationsService } from '../../../../../../siem_migrations/se import { checkStartMigrationCardComplete } from './start_migration_check_complete'; describe('startMigrationCheckComplete', () => { - test('should return default values if siem migrations are not available', async () => { + it('should return default values if siem migrations are not available', async () => { // Arrange const siemMigrations = { rules: { @@ -26,10 +26,14 @@ describe('startMigrationCheckComplete', () => { }; const result = await checkStartMigrationCardComplete(services); - expect(result).toEqual({ isComplete: false, metadata: { missingCapabilities: [] } }); + expect(result).toEqual({ + completeBadgeText: '0 migrations', + isComplete: false, + metadata: { missingCapabilities: [] }, + }); }); - test('should query Stats if siem migrations are available', async () => { + it('should query Stats if siem migrations are available', async () => { const siemMigrations = { rules: { getMissingCapabilities: jest.fn().mockReturnValue([]), @@ -52,6 +56,7 @@ describe('startMigrationCheckComplete', () => { expect(siemMigrations.rules.getRuleMigrationsStats).toHaveBeenCalled(); expect(result).toEqual({ + completeBadgeText: '1 migration', isComplete: true, metadata: { missingCapabilities: [] }, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts index ec5d100ff61fb..741783e1bf25b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts @@ -5,11 +5,19 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; import { SiemMigrationTaskStatus } from '../../../../../../../common/siem_migrations/constants'; import type { OnboardingCardCheckComplete } from '../../../../../types'; import type { StartMigrationCardMetadata } from './types'; +const COMPLETE_BADGE_TEXT = (migrationsCount: number) => + i18n.translate('xpack.securitySolution.onboarding.siemMigrations.startMigration.completeBadge', { + defaultMessage: + '{migrationsCount} {migrationsCount, plural, one {migration} other {migrations}}', + values: { migrationsCount }, + }); + export const checkStartMigrationCardComplete: OnboardingCardCheckComplete< StartMigrationCardMetadata > = async ({ siemMigrations }) => { @@ -18,12 +26,19 @@ export const checkStartMigrationCardComplete: OnboardingCardCheckComplete< .map(({ description }) => description); let isComplete = false; + let migrationsCount = 0; if (siemMigrations.rules.isAvailable()) { const migrationsStats = await siemMigrations.rules.getRuleMigrationsStats(); isComplete = migrationsStats.some( (migrationStats) => migrationStats.status === SiemMigrationTaskStatus.FINISHED ); + migrationsCount = migrationsStats.length; } - return { isComplete, metadata: { missingCapabilities } }; + + return { + isComplete, + completeBadgeText: COMPLETE_BADGE_TEXT(migrationsCount), + metadata: { missingCapabilities }, + }; }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts index b5c15b00e20e6..d2b0b38ffb2ea 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; export const START_MIGRATION_CARD_TITLE = i18n.translate( 'xpack.securitySolution.onboarding.startMigration.title', - { defaultMessage: 'Translate your existing SIEM Rules to Elastic' } + { defaultMessage: 'Migrate your existing SplunkĀ® SIEM rules to Elastic' } ); export const START_MIGRATION_CARD_FOOTER_NOTE = i18n.translate( 'xpack.securitySolution.onboarding.startMigration.footerNote', diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.tsx index f2e93c3796944..f3b4310c05052 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.tsx @@ -10,7 +10,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiText, - EuiIcon, EuiButton, EuiButtonEmpty, EuiPanel, @@ -45,7 +44,7 @@ export const UploadRulesSectionPanel = React.memo( gutterSize={isUploadMore ? 'm' : 'l'} > - + {isUploadMore ? ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.test.tsx index 52f44014a6651..3cdad1bf552f9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.test.tsx @@ -10,6 +10,7 @@ import { OnboardingBody } from './onboarding_body'; import { useBodyConfig } from './hooks/use_body_config'; import { useExpandedCard } from './hooks/use_expanded_card'; import { useCompletedCards } from './hooks/use_completed_cards'; +import { TestProviders } from '../../../common/mock'; jest.mock('../onboarding_context'); jest.mock('./hooks/use_body_config'); @@ -58,14 +59,14 @@ describe('OnboardingBody Component', () => { }); it('should render the OnboardingBody component with the correct content', () => { - render(); + render(, { wrapper: TestProviders }); expect(screen.getByText('Group 1')).toBeInTheDocument(); expect(screen.getByText('Card 1')).toBeInTheDocument(); }); describe('when the card is expanded', () => { beforeEach(() => { - render(); + render(, { wrapper: TestProviders }); fireEvent.click(screen.getByText('Card 1')); }); @@ -85,7 +86,7 @@ describe('OnboardingBody Component', () => { setExpandedCardId: mockSetExpandedCardId, }); - render(); + render(, { wrapper: TestProviders }); fireEvent.click(screen.getByText('Card 1')); }); @@ -112,7 +113,7 @@ describe('OnboardingBody Component', () => { setExpandedCardId: mockSetExpandedCardId, }); - render(); + render(, { wrapper: TestProviders }); act(() => { fireEvent.click(screen.getByText('Card 1')); }); @@ -136,7 +137,7 @@ describe('OnboardingBody Component', () => { setExpandedCardId: mockSetExpandedCardId, }); - render(); + render(, { wrapper: TestProviders }); act(() => { fireEvent.click(screen.getByText('Card 1')); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx index 947a0dff46343..8f759b31c8e67 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx @@ -61,7 +61,8 @@ export const OnboardingBody = React.memo(() => { - {group.cards.map(({ id, title, icon, Component: LazyCardComponent }) => { + {group.cards.map((card) => { + const { id, title, icon, iconDark, badge, Component: LazyCardComponent } = card; const cardCheckCompleteResult = getCardCheckCompleteResult(id); return ( @@ -69,6 +70,8 @@ export const OnboardingBody = React.memo(() => { id={id} title={title} icon={icon} + iconDark={iconDark} + badge={badge} checkCompleteResult={cardCheckCompleteResult} isExpanded={expandedCardId === id} isComplete={isCardComplete(id)} diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.test.tsx index 051b6f113f4f1..7d447dd3cee96 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.test.tsx @@ -10,12 +10,26 @@ import { render, screen, fireEvent } from '@testing-library/react'; import { OnboardingCardPanel } from './onboarding_card_panel'; import { CARD_COMPLETE_BADGE, EXPAND_CARD_BUTTON_LABEL } from './translations'; import type { OnboardingCardId } from '../../constants'; +import { TestProviders } from '../../../common/mock/test_providers'; + +const mockUseDarkMode = jest.fn(() => false); +jest.mock('@kbn/kibana-react-plugin/public', () => ({ + ...jest.requireActual('@kbn/kibana-react-plugin/public'), + useDarkMode: () => mockUseDarkMode(), +})); + +jest.mock('@elastic/eui', () => ({ + ...jest.requireActual('@elastic/eui'), + EuiIcon: jest.fn(({ type }: { type: string }) =>
), +})); describe('OnboardingCardPanel Component', () => { const defaultProps = { id: 'card-1' as OnboardingCardId, title: 'Test Card', icon: 'testIcon', + iconDark: undefined, + badge: undefined, isExpanded: false, isComplete: false, onToggleExpanded: jest.fn(), @@ -29,7 +43,8 @@ describe('OnboardingCardPanel Component', () => { render(
{'Test Card Content'}
-
+ , + { wrapper: TestProviders } ); // Verify that the card title and icon are rendered @@ -40,7 +55,8 @@ describe('OnboardingCardPanel Component', () => { render(
{'Test Card Content'}
-
+ , + { wrapper: TestProviders } ); // Verify that the complete badge is displayed @@ -51,7 +67,8 @@ describe('OnboardingCardPanel Component', () => { render(
{'Test Card Content'}
-
+ , + { wrapper: TestProviders } ); // Verify that the complete badge is not displayed @@ -62,7 +79,8 @@ describe('OnboardingCardPanel Component', () => { render(
{'Test Card Content'}
-
+ , + { wrapper: TestProviders } ); // Click on the card header @@ -76,7 +94,8 @@ describe('OnboardingCardPanel Component', () => { const { rerender } = render(
{'Test Card Content'}
-
+ , + { wrapper: TestProviders } ); // Check the button icon when card is not expanded @@ -94,4 +113,49 @@ describe('OnboardingCardPanel Component', () => { // Check the button icon when card is expanded expect(buttonIcon).toHaveAttribute('aria-expanded', 'true'); }); + + describe('when badge is defined', () => { + it('should render the badge', () => { + render( + +
{'Test Card Content'}
+
, + { wrapper: TestProviders } + ); + + expect(screen.getByTestId('onboardingCardBadge')).toBeInTheDocument(); + }); + }); + + describe('when iconDark is defined', () => { + const iconDark = 'testIconDark'; + + it('should render the dark icon with the dark theme', () => { + mockUseDarkMode.mockReturnValue(true); + + render( + +
{'Test Card Content'}
+
, + { wrapper: TestProviders } + ); + + expect(screen.queryByTestId('EuiIcon-testIconDark')).toBeInTheDocument(); + expect(screen.queryByTestId('EuiIcon-testIcon')).not.toBeInTheDocument(); + }); + + it('should not render the dark icon with the light theme', () => { + mockUseDarkMode.mockReturnValue(false); + + render( + +
{'Test Card Content'}
+
, + { wrapper: TestProviders } + ); + + expect(screen.queryByTestId('EuiIcon-testIconDark')).not.toBeInTheDocument(); + expect(screen.queryByTestId('EuiIcon-testIcon')).toBeInTheDocument(); + }); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.tsx index d6c5e76527afd..1768914aeaa73 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { type PropsWithChildren } from 'react'; +import React, { useMemo, type PropsWithChildren } from 'react'; import type { IconType } from '@elastic/eui'; import { EuiPanel, @@ -17,16 +17,20 @@ import { EuiTitle, } from '@elastic/eui'; import classnames from 'classnames'; +import { useDarkMode } from '@kbn/kibana-react-plugin/public'; import type { OnboardingCardId } from '../../constants'; -import type { CheckCompleteResult } from '../../types'; +import type { CheckCompleteResult, CardBadge } from '../../types'; import { CARD_COMPLETE_BADGE, EXPAND_CARD_BUTTON_LABEL } from './translations'; import { useCardPanelStyles } from './onboarding_card_panel.styles'; import { useDelayedVisibility } from './hooks/use_delayed_visibility'; +import { OnboardingCardBadge } from './onboarding_card_panel_badge'; interface OnboardingCardPanelProps { id: OnboardingCardId; title: string; icon: IconType; + iconDark: IconType | undefined; + badge: CardBadge | undefined; isExpanded: boolean; isComplete: boolean; onToggleExpanded: () => void; @@ -38,6 +42,8 @@ export const OnboardingCardPanel = React.memo (iconDark && isDarkMode ? iconDark : icon), + [isDarkMode, iconDark, icon] + ); + const isContentVisible = useDelayedVisibility({ isExpanded }); return ( @@ -70,13 +82,22 @@ export const OnboardingCardPanel = React.memo - + - -

{title}

-
+ + + +

{title}

+
+
+ {badge && ( + + + + )} +
{checkCompleteResult?.additionalBadges?.map((additionalBadge, index) => ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel_badge.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel_badge.tsx new file mode 100644 index 0000000000000..087965d75b416 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel_badge.tsx @@ -0,0 +1,55 @@ +/* + * 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 React, { type PropsWithChildren } from 'react'; +import { EuiBadge, EuiBetaBadge } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { CardBadge } from '../../types'; + +const label = { + beta: i18n.translate('xpack.securitySolution.onboarding.cardBadge.beta', { + defaultMessage: 'Beta', + }), + techPreview: i18n.translate('xpack.securitySolution.onboarding.cardBadge.techPreview', { + defaultMessage: 'Technical Preview', + }), +}; +const tooltip = { + beta: i18n.translate('xpack.securitySolution.onboarding.cardBadge.betaTooltip', { + defaultMessage: 'This feature is in beta and is not recommended for production use.', + }), + techPreview: i18n.translate('xpack.securitySolution.onboarding.cardBadge.techPreviewTooltip', { + defaultMessage: 'This feature is in technical preview and is subject to change.', + }), +}; + +export const OnboardingCardBadge = React.memo>( + ({ badge }) => { + if (badge === 'beta') { + return ( + + ); + } + if (badge === 'tech_preview') { + return ( + + ); + } + return ; + } +); +OnboardingCardBadge.displayName = 'OnboardingCardBadge'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_context.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_context.tsx index 21cbb029319bd..864804e0e4d40 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_context.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_context.tsx @@ -8,11 +8,9 @@ import type { PropsWithChildren } from 'react'; import React, { createContext, useCallback, useContext, useMemo } from 'react'; import { useKibana } from '../../common/lib/kibana/kibana_react'; -import type { OnboardingTopicId, OnboardingCardId } from '../constants'; -import { OnboardingHubEventTypes } from '../../common/lib/telemetry'; +import type { OnboardingTopicId } from '../constants'; import { useLicense } from '../../common/hooks/use_license'; import { ExperimentalFeaturesService } from '../../common/experimental_features_service'; - import { hasCapabilities } from '../../common/lib/capabilities'; import type { OnboardingConfigAvailabilityProps, @@ -20,12 +18,7 @@ import type { TopicConfig, } from '../types'; import { onboardingConfig } from '../config'; - -export interface OnboardingTelemetry { - reportCardOpen: (cardId: OnboardingCardId, options?: { auto?: boolean }) => void; - reportCardComplete: (cardId: OnboardingCardId, options?: { auto?: boolean }) => void; - reportCardLinkClicked: (cardId: OnboardingCardId, linkId: string) => void; -} +import { useOnboardingTelemetry, type OnboardingTelemetry } from './onboarding_telemetry'; export type OnboardingConfig = Map; export interface OnboardingContextValue { @@ -116,30 +109,3 @@ const useFilteredConfig = (): OnboardingConfig => { return filteredConfig; }; - -const useOnboardingTelemetry = (): OnboardingTelemetry => { - const { telemetry } = useKibana().services; - return useMemo( - () => ({ - reportCardOpen: (cardId, { auto = false } = {}) => { - telemetry.reportEvent(OnboardingHubEventTypes.OnboardingHubStepOpen, { - stepId: cardId, - trigger: auto ? 'navigation' : 'click', - }); - }, - reportCardComplete: (cardId, { auto = false } = {}) => { - telemetry.reportEvent(OnboardingHubEventTypes.OnboardingHubStepFinished, { - stepId: cardId, - trigger: auto ? 'auto_check' : 'click', - }); - }, - reportCardLinkClicked: (cardId, linkId: string) => { - telemetry.reportEvent(OnboardingHubEventTypes.OnboardingHubStepLinkClicked, { - originStepId: cardId, - stepLinkId: linkId, - }); - }, - }), - [telemetry] - ); -}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_telemetry.test.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_telemetry.test.ts new file mode 100644 index 0000000000000..719a49e2457dd --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_telemetry.test.ts @@ -0,0 +1,115 @@ +/* + * 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 { renderHook } from '@testing-library/react'; +import { useOnboardingTelemetry } from './onboarding_telemetry'; +import { useKibana } from '../../common/lib/kibana/kibana_react'; +import { OnboardingHubEventTypes } from '../../common/lib/telemetry'; +import type { OnboardingCardId } from '../constants'; + +jest.mock('../config', () => ({ + onboardingConfig: [ + { id: 'default', body: [{ cards: [{ id: 'testCard' }] }] }, + { id: 'testTopic', body: [{ cards: [{ id: 'testCard2' }] }] }, + ], +})); + +jest.mock('../../common/lib/kibana/kibana_react'); +const telemetryMock = { reportEvent: jest.fn() }; +(useKibana as jest.Mock).mockReturnValue({ + services: { telemetry: telemetryMock }, +}); + +describe('useOnboardingTelemetry', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('when opening a card', () => { + it('should report card open event on default topic', () => { + const { result } = renderHook(useOnboardingTelemetry); + result.current.reportCardOpen('testCard' as OnboardingCardId); + + expect(telemetryMock.reportEvent).toHaveBeenCalledWith( + OnboardingHubEventTypes.OnboardingHubStepOpen, + { stepId: 'testCard', trigger: 'click' } + ); + }); + + it('should report card open event on another topic', () => { + const { result } = renderHook(useOnboardingTelemetry); + result.current.reportCardOpen('testCard2' as OnboardingCardId); + expect(telemetryMock.reportEvent).toHaveBeenCalledWith( + OnboardingHubEventTypes.OnboardingHubStepOpen, + { stepId: 'testTopic#testCard2', trigger: 'click' } + ); + }); + + it('should report card auto open event', () => { + const { result } = renderHook(useOnboardingTelemetry); + result.current.reportCardOpen('testCard' as OnboardingCardId, { auto: true }); + expect(telemetryMock.reportEvent).toHaveBeenCalledWith( + OnboardingHubEventTypes.OnboardingHubStepOpen, + { stepId: 'testCard', trigger: 'navigation' } + ); + }); + }); + + describe('when completing a card', () => { + it('should report card complete event on the default topic', () => { + const { result } = renderHook(useOnboardingTelemetry); + result.current.reportCardComplete('testCard' as OnboardingCardId); + + expect(telemetryMock.reportEvent).toHaveBeenCalledWith( + OnboardingHubEventTypes.OnboardingHubStepFinished, + { stepId: 'testCard', trigger: 'click' } + ); + }); + + it('should report card complete event on the another topic', () => { + const { result } = renderHook(useOnboardingTelemetry); + result.current.reportCardComplete('testCard2' as OnboardingCardId); + + expect(telemetryMock.reportEvent).toHaveBeenCalledWith( + OnboardingHubEventTypes.OnboardingHubStepFinished, + { stepId: 'testTopic#testCard2', trigger: 'click' } + ); + }); + + it('should report card auto complete event', () => { + const { result } = renderHook(useOnboardingTelemetry); + result.current.reportCardComplete('testCard' as OnboardingCardId, { auto: true }); + + expect(telemetryMock.reportEvent).toHaveBeenCalledWith( + OnboardingHubEventTypes.OnboardingHubStepFinished, + { stepId: 'testCard', trigger: 'auto_check' } + ); + }); + }); + + describe('when clicking a card link', () => { + it('should report card link clicked event on the default topic', () => { + const { result } = renderHook(useOnboardingTelemetry); + result.current.reportCardLinkClicked('testCard' as OnboardingCardId, 'link1'); + + expect(telemetryMock.reportEvent).toHaveBeenCalledWith( + OnboardingHubEventTypes.OnboardingHubStepLinkClicked, + { originStepId: 'testCard', stepLinkId: 'link1' } + ); + }); + + it('should report card link clicked event on another topic', () => { + const { result } = renderHook(useOnboardingTelemetry); + result.current.reportCardLinkClicked('testCard2' as OnboardingCardId, 'link1'); + + expect(telemetryMock.reportEvent).toHaveBeenCalledWith( + OnboardingHubEventTypes.OnboardingHubStepLinkClicked, + { originStepId: 'testTopic#testCard2', stepLinkId: 'link1' } + ); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_telemetry.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_telemetry.ts new file mode 100644 index 0000000000000..3477387f0d4cd --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_telemetry.ts @@ -0,0 +1,61 @@ +/* + * 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 { useMemo } from 'react'; +import { useKibana } from '../../common/lib/kibana/kibana_react'; +import type { OnboardingCardId } from '../constants'; +import { OnboardingTopicId } from '../constants'; +import { OnboardingHubEventTypes } from '../../common/lib/telemetry'; +import { onboardingConfig } from '../config'; + +export interface OnboardingTelemetry { + reportCardOpen: (cardId: OnboardingCardId, options?: { auto?: boolean }) => void; + reportCardComplete: (cardId: OnboardingCardId, options?: { auto?: boolean }) => void; + reportCardLinkClicked: (cardId: OnboardingCardId, linkId: string) => void; +} + +export const useOnboardingTelemetry = (): OnboardingTelemetry => { + const { telemetry } = useKibana().services; + return useMemo( + () => ({ + reportCardOpen: (cardId, { auto = false } = {}) => { + telemetry.reportEvent(OnboardingHubEventTypes.OnboardingHubStepOpen, { + stepId: getStepId(cardId), + trigger: auto ? 'navigation' : 'click', + }); + }, + reportCardComplete: (cardId, { auto = false } = {}) => { + telemetry.reportEvent(OnboardingHubEventTypes.OnboardingHubStepFinished, { + stepId: getStepId(cardId), + trigger: auto ? 'auto_check' : 'click', + }); + }, + reportCardLinkClicked: (cardId, linkId: string) => { + telemetry.reportEvent(OnboardingHubEventTypes.OnboardingHubStepLinkClicked, { + originStepId: getStepId(cardId), + stepLinkId: linkId, + }); + }, + }), + [telemetry] + ); +}; + +/** + * Get the step id for a given card id. + * The stepId is used to track the onboarding card in telemetry, it is a combination of the topic id and the card id. + * To keep backwards compatibility, if the card is in the default topic, the stepId will be the card id only. + */ +const getStepId = (cardId: OnboardingCardId) => { + const cardTopic = onboardingConfig.find((topic) => + topic.body.some((group) => group.cards.some((card) => card.id === cardId)) + ); + if (!cardTopic || cardTopic.id === OnboardingTopicId.default) { + return cardId; // Do not add topic id for default topic to preserve existing events format + } + return `${cardTopic.id}#${cardId}`; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/constants.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/constants.ts index 94b87721513bc..0e6c94c7ed23f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/constants.ts @@ -21,5 +21,5 @@ export enum OnboardingCardId { // siem_migrations topic cards siemMigrationsAiConnectors = 'ai_connectors', - siemMigrationsStart = 'start', + siemMigrationsRules = 'migrate_rules', } diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/types.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/types.ts index a9dbabef12170..30e6d52c4fada 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/types.ts @@ -6,7 +6,7 @@ */ import type React from 'react'; -import type { IconType } from '@elastic/eui'; +import type { EuiBadgeProps, IconType } from '@elastic/eui'; import type { LicenseType } from '@kbn/licensing-plugin/public'; import type { ExperimentalFeatures } from '../../common'; @@ -48,6 +48,8 @@ export type SetExpandedCardId = ( options?: { scroll?: boolean } ) => void; +export type CardBadge = 'beta' | 'tech_preview' | EuiBadgeProps; + export type OnboardingCardComponent = React.ComponentType<{ /** * Function to set the current card completion status. @@ -130,6 +132,10 @@ export interface OnboardingCardConfig * @returns Promise for the complete status */ checkComplete?: OnboardingCardCheckComplete; + /** Optional icon for dark mode */ + iconDark?: IconType; + /** Optional badge to display on the card. */ + badge?: CardBadge; } export interface OnboardingGroupConfig { diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/icon/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/icon/index.tsx index c0528a9a04afe..8d72dde9e6e84 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/icon/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/icon/index.tsx @@ -4,5 +4,17 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import React from 'react'; +import { useDarkMode } from '@kbn/kibana-react-plugin/public'; +import { EuiIcon, type EuiIconProps } from '@elastic/eui'; import SiemMigrationsIconSVG from './siem_migrations.svg'; -export const SiemMigrationsIcon = SiemMigrationsIconSVG; +import SiemMigrationsIconDarkSVG from './siem_migrations_dark.svg'; + +export const SiemMigrationsIcon = React.memo>((props) => { + const isDark = useDarkMode(); + if (isDark) { + return ; + } + return ; +}); +SiemMigrationsIcon.displayName = 'SiemMigrationsIcon'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/icon/siem_migrations.svg b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/icon/siem_migrations.svg index d2656fa7e9a3a..b9559cc25e290 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/icon/siem_migrations.svg +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/icon/siem_migrations.svg @@ -1,47 +1,69 @@ - - - - + + + + + + - - + + - - - - - - - + + + + + + + - + + - + + + + + + + + + + + + + + + + + + + + - + - + - - + + - - + + - - + + diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/icon/siem_migrations_dark.svg b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/icon/siem_migrations_dark.svg new file mode 100644 index 0000000000000..d9c3637e6a0b3 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/icon/siem_migrations_dark.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_progress_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_progress_panel.tsx index 37b8a0fed66c0..a78d3933ee59a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_progress_panel.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_progress_panel.tsx @@ -15,6 +15,8 @@ import { EuiLoadingSpinner, EuiIcon, EuiSpacer, + useEuiTheme, + tint, } from '@elastic/eui'; import { AssistantIcon } from '@kbn/ai-assistant-icon'; import { PanelText } from '../../../../common/components/panel_text'; @@ -27,6 +29,7 @@ export interface MigrationProgressPanelProps { } export const MigrationProgressPanel = React.memo( ({ migrationStats }) => { + const { euiTheme } = useEuiTheme(); const finishedCount = migrationStats.rules.completed + migrationStats.rules.failed; const progressValue = (finishedCount / migrationStats.rules.total) * 100; @@ -66,7 +69,7 @@ export const MigrationProgressPanel = React.memo( value={progressValue} valueText={`${Math.floor(progressValue)}%`} max={100} - color="success" + color={tint(euiTheme.colors.success, 0.25)} /> diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx index f7571908854f8..25e3fb030bb29 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx @@ -18,8 +18,11 @@ import { EuiText, EuiAccordion, EuiButtonIcon, - type EuiBasicTableColumn, EuiSpacer, + EuiBadge, + type EuiBasicTableColumn, + useEuiTheme, + COLOR_MODES_STANDARD, } from '@elastic/eui'; import { Chart, BarSeries, Settings, ScaleType } from '@elastic/charts'; import { SecurityPageName } from '@kbn/security-solution-navigation'; @@ -48,6 +51,18 @@ const headerStyle = css` } `; +const useCompleteBadgeStyles = () => { + const { euiTheme, colorMode } = useEuiTheme(); + const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark; + return css` + background-color: ${isDarkMode + ? euiTheme.colors.success + : euiTheme.colors.backgroundBaseSuccess}; + color: ${isDarkMode ? euiTheme.colors.plainDark : euiTheme.colors.textSuccess}; + text-decoration: none; + `; +}; + export interface MigrationResultPanelProps { migrationStats: RuleMigrationStats; isCollapsed: boolean; @@ -59,6 +74,8 @@ export const MigrationResultPanel = React.memo( const { data: translationStats, isLoading: isLoadingTranslationStats } = useGetMigrationTranslationStats(migrationStats.id); + const completeBadgeStyles = useCompleteBadgeStyles(); + return ( @@ -67,7 +84,7 @@ export const MigrationResultPanel = React.memo( -

{i18n.RULE_MIGRATION_COMPLETE_TITLE(migrationStats.number)}

+

{i18n.RULE_MIGRATION_TITLE(migrationStats.number)}

@@ -82,6 +99,9 @@ export const MigrationResultPanel = React.memo( + + {i18n.RULE_MIGRATION_COMPLETE_BADGE} + > = [ { field: 'title', - name: i18n.RULE_MIGRATION_TABLE_COLUMN_RESULT, + name: i18n.RULE_MIGRATION_TABLE_COLUMN_STATUS, render: (title: string, { color }) => ( {title} diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/read_more.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/read_more.tsx index 4567026f3cc08..618b74d5cef21 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/read_more.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/read_more.tsx @@ -12,7 +12,7 @@ import { PanelText } from '../../../../common/components/panel_text'; import { useKibana } from '../../../../common/lib/kibana/kibana_react'; export const RuleMigrationsReadMore = React.memo(() => { - const docLink = useKibana().services.docLinks.links.siem.gettingStarted; + const docLink = useKibana().services.docLinks.links.securitySolution.siemMigrations; return (

diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts index e34f287409b97..57141e779a770 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts @@ -49,12 +49,6 @@ export const RULE_MIGRATION_TRANSLATING = i18n.translate( { defaultMessage: `Translating rules` } ); -export const RULE_MIGRATION_COMPLETE_TITLE = (number: number) => - i18n.translate('xpack.securitySolution.siemMigrations.rules.panel.result.title', { - defaultMessage: 'SIEM rules migration #{number} complete', - values: { number }, - }); - export const RULE_MIGRATION_COMPLETE_DESCRIPTION = (createdAt: string, finishedAt: string) => i18n.translate('xpack.securitySolution.siemMigrations.rules.panel.result.description', { defaultMessage: 'Export uploaded on {createdAt} and translation finished {finishedAt}.', @@ -77,7 +71,7 @@ export const RULE_MIGRATION_SUMMARY_CHART_TITLE = i18n.translate( export const RULE_MIGRATION_VIEW_TRANSLATED_RULES_BUTTON = i18n.translate( 'xpack.securitySolution.siemMigrations.rules.panel.result.summary.button', - { defaultMessage: 'View translated rules' } + { defaultMessage: 'View rules' } ); export const RULE_MIGRATION_TRANSLATION_FAILED = i18n.translate( @@ -85,9 +79,9 @@ export const RULE_MIGRATION_TRANSLATION_FAILED = i18n.translate( { defaultMessage: 'Failed' } ); -export const RULE_MIGRATION_TABLE_COLUMN_RESULT = i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.panel.result.summary.tableColumn.result', - { defaultMessage: 'Result' } +export const RULE_MIGRATION_TABLE_COLUMN_STATUS = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.panel.result.summary.tableColumn.status', + { defaultMessage: 'Status' } ); export const RULE_MIGRATION_TABLE_COLUMN_RULES = i18n.translate( 'xpack.securitySolution.siemMigrations.rules.panel.result.summary.tableColumn.rules', @@ -96,12 +90,16 @@ export const RULE_MIGRATION_TABLE_COLUMN_RULES = i18n.translate( export const RULE_MIGRATION_UPLOAD_MISSING_RESOURCES_TITLE = i18n.translate( 'xpack.securitySolution.siemMigrations.rules.panel.uploadMissingResources', - { defaultMessage: 'Upload missing Macros and Lookups.' } -); -export const RULE_MIGRATION_UPLOAD_MISSING_RESOURCES_DESCRIPTION = i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.panel.uploadMissingResourcesDescription', - { defaultMessage: 'Click upload for step-by-step guidance to finish partially translated rules.' } -); + { defaultMessage: 'Upload missing macros and lookup lists.' } +); +export const RULE_MIGRATION_UPLOAD_MISSING_RESOURCES_DESCRIPTION = (partialRulesCount: number) => + i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.panel.uploadMissingResourcesDescription', + { + defaultMessage: 'Click Upload to continue translating {partialRulesCount} rules', + values: { partialRulesCount }, + } + ); export const RULE_MIGRATION_UPLOAD_BUTTON = i18n.translate( 'xpack.securitySolution.siemMigrations.rules.panel.uploadMacros.button', diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing_panel.tsx index 58c85a623f4f7..6698dc5caffbf 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing_panel.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing_panel.tsx @@ -5,11 +5,12 @@ * 2.0. */ -import React, { useState, useCallback, useEffect } from 'react'; +import React, { useState, useCallback, useEffect, useMemo } from 'react'; import { EuiButton, EuiFlexGroup, EuiFlexItem, + EuiLoadingSpinner, EuiPanel, EuiSpacer, useEuiTheme, @@ -23,6 +24,7 @@ import { useGetMissingResources } from '../../service/hooks/use_get_missing_reso import * as i18n from './translations'; import { useRuleMigrationDataInputContext } from '../data_input_flyout/context'; import type { RuleMigrationStats } from '../../types'; +import { useGetMigrationTranslationStats } from '../../logic/use_get_migration_translation_stats'; interface RuleMigrationsUploadMissingPanelProps { migrationStats: RuleMigrationStats; @@ -30,9 +32,6 @@ interface RuleMigrationsUploadMissingPanelProps { } export const RuleMigrationsUploadMissingPanel = React.memo( ({ migrationStats, topSpacerSize }) => { - const { euiTheme } = useEuiTheme(); - const { telemetry } = useKibana().services.siemMigrations.rules; - const { openFlyout } = useRuleMigrationDataInputContext(); const [missingResources, setMissingResources] = useState([]); const { getMissingResources, isLoading } = useGetMissingResources(setMissingResources); @@ -40,56 +39,94 @@ export const RuleMigrationsUploadMissingPanel = React.memo { - openFlyout(migrationStats); - telemetry.reportSetupMigrationOpenResources({ - migrationId: migrationStats.id, - missingResourcesCount: missingResources.length, - }); - }, [migrationStats, openFlyout, missingResources, telemetry]); - if (isLoading || missingResources.length === 0) { return null; } + return ( - <> - {topSpacerSize && } - - - - - - - - {i18n.RULE_MIGRATION_UPLOAD_MISSING_RESOURCES_TITLE} - - - - - {i18n.RULE_MIGRATION_UPLOAD_MISSING_RESOURCES_DESCRIPTION} - - - - - {i18n.RULE_MIGRATION_UPLOAD_BUTTON} - - - - - + ); } ); RuleMigrationsUploadMissingPanel.displayName = 'RuleMigrationsUploadMissingPanel'; + +interface RuleMigrationsUploadMissingPanelContentProps + extends RuleMigrationsUploadMissingPanelProps { + missingResources: RuleMigrationResourceBase[]; +} +const RuleMigrationsUploadMissingPanelContent = + React.memo( + ({ migrationStats, topSpacerSize, missingResources }) => { + const { euiTheme } = useEuiTheme(); + const { telemetry } = useKibana().services.siemMigrations.rules; + const { openFlyout } = useRuleMigrationDataInputContext(); + + const { data: translationStats, isLoading: isLoadingTranslationStats } = + useGetMigrationTranslationStats(migrationStats.id); + + const onOpenFlyout = useCallback(() => { + openFlyout(migrationStats); + telemetry.reportSetupMigrationOpenResources({ + migrationId: migrationStats.id, + missingResourcesCount: missingResources.length, + }); + }, [migrationStats, openFlyout, missingResources, telemetry]); + + const totalRulesToRetry = useMemo(() => { + return ( + (translationStats?.rules.failed ?? 0) + + (translationStats?.rules.success.result.partial ?? 0) + + (translationStats?.rules.success.result.untranslatable ?? 0) + ); + }, [translationStats]); + + return ( + <> + {topSpacerSize && } + + + + + + + + {i18n.RULE_MIGRATION_UPLOAD_MISSING_RESOURCES_TITLE} + + + + {isLoadingTranslationStats ? ( + + ) : ( + + {i18n.RULE_MIGRATION_UPLOAD_MISSING_RESOURCES_DESCRIPTION(totalRulesToRetry)} + + )} + + + + {i18n.RULE_MIGRATION_UPLOAD_BUTTON} + + + + + + ); + } + ); +RuleMigrationsUploadMissingPanelContent.displayName = 'RuleMigrationsUploadMissingPanelContent'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/utils/translation_results/index.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/utils/translation_results/index.ts index 8571cdfe0b1a1..29c99a11dfa0d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/utils/translation_results/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/utils/translation_results/index.ts @@ -10,6 +10,13 @@ import { RuleTranslationResult } from '../../../../../common/siem_migrations/con import type { RuleMigrationTranslationResult } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import * as i18n from './translations'; +const COLORS = { + [RuleTranslationResult.FULL]: '#54B399', + [RuleTranslationResult.PARTIAL]: '#D6BF57', + [RuleTranslationResult.UNTRANSLATABLE]: '#DA8B45', + error: '#E7664C', +} as const; + export const useResultVisColors = () => { const { euiTheme } = useEuiTheme(); if (euiTheme.themeName === 'EUI_THEME_AMSTERDAM') { @@ -21,22 +28,17 @@ export const useResultVisColors = () => { }; } // Borealis - return { - [RuleTranslationResult.FULL]: euiTheme.colors.vis.euiColorVisSuccess0, - [RuleTranslationResult.PARTIAL]: euiTheme.colors.vis.euiColorSeverity7, - [RuleTranslationResult.UNTRANSLATABLE]: euiTheme.colors.vis.euiColorSeverity10, - error: euiTheme.colors.vis.euiColorSeverity14, - }; + return COLORS; }; export const convertTranslationResultIntoColor = (status?: RuleMigrationTranslationResult) => { switch (status) { case RuleTranslationResult.FULL: - return 'primary'; + return COLORS[RuleTranslationResult.FULL]; case RuleTranslationResult.PARTIAL: - return 'warning'; + return COLORS[RuleTranslationResult.PARTIAL]; case RuleTranslationResult.UNTRANSLATABLE: - return 'danger'; + return COLORS[RuleTranslationResult.UNTRANSLATABLE]; default: return 'subdued'; } From c573ce62ae5d4f956b8f5c3b08028d997fab0c89 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Wed, 12 Mar 2025 19:19:56 +0100 Subject: [PATCH 2/2] use a temporary parent link --- src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts b/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts index e41ae419993e6..538c5a4baed31 100644 --- a/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts +++ b/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts @@ -532,7 +532,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D signalsMigrationApi: `${SECURITY_SOLUTION_DOCS}signals-migration-api.html`, legacyEndpointManagementApiDeprecations: `${KIBANA_DOCS}breaking-changes-summary.html#breaking-199598`, legacyRuleManagementBulkApiDeprecations: `${KIBANA_DOCS}breaking-changes-summary.html#breaking-207091`, - siemMigrations: `${SECURITY_SOLUTION_DOCS}siem-migration.html`, + siemMigrations: `${SECURITY_SOLUTION_DOCS}ai-for-security.html`, // TODO: Update this link once the content is available llmPerformanceMatrix: `${SECURITY_SOLUTION_DOCS}llm-performance-matrix.html`, }, query: {