diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx index a240e937d4de6..17f4840e68dc4 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx @@ -40,7 +40,7 @@ export const OnboardingPage = React.memo(() => { restrictWidth={PAGE_CONTENT_WIDTH} paddingSize="xl" bottomBorder="extended" - style={{ backgroundColor: euiTheme.colors.lightestShade }} + style={{ backgroundColor: euiTheme.colors.body }} > diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/body_config.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/body_config.ts index d57b38a2e02f1..8191f392e97e6 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/body_config.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/body_config.ts @@ -9,6 +9,9 @@ import { i18n } from '@kbn/i18n'; import type { OnboardingGroupConfig } from '../../types'; import { integrationsCardConfig } from './cards/integrations'; import { dashboardsCardConfig } from './cards/dashboards'; +import { rulesCardConfig } from './cards/rules'; +import { alertsCardConfig } from './cards/alerts'; +import { assistantCardConfig } from './cards/assistant'; export const bodyConfig: OnboardingGroupConfig[] = [ { @@ -21,6 +24,12 @@ export const bodyConfig: OnboardingGroupConfig[] = [ title: i18n.translate('xpack.securitySolution.onboarding.alertsGroup.title', { defaultMessage: 'Configure rules and alerts', }), - cards: [], + cards: [rulesCardConfig, alertsCardConfig], + }, + { + title: i18n.translate('xpack.securitySolution.onboarding.discoverGroup.title', { + defaultMessage: 'Discover Elastic AI', + }), + cards: [assistantCardConfig], }, ]; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/alerts_card.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/alerts_card.test.tsx new file mode 100644 index 0000000000000..3e83bcb851f82 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/alerts_card.test.tsx @@ -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 React from 'react'; +import { render } from '@testing-library/react'; +import { AlertsCard } from './alerts_card'; +import { TestProviders } from '../../../../../common/mock'; + +const props = { + setComplete: jest.fn(), + checkComplete: jest.fn(), + isCardComplete: jest.fn(), + setExpandedCardId: jest.fn(), +}; + +describe('AlertsCard', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('description should be in the document', () => { + const { getByTestId } = render( + + + + ); + + expect(getByTestId('alertsCardDescription')).toBeInTheDocument(); + }); + + it('card callout should be rendered if integrations cards is not complete', () => { + props.isCardComplete.mockReturnValueOnce(false); + + const { getByText } = render( + + + + ); + + expect(getByText('To view alerts add integrations first.')).toBeInTheDocument(); + }); + + it('card button should be disabled if integrations cards is not complete', () => { + props.isCardComplete.mockReturnValueOnce(false); + + const { getByTestId } = render( + + + + ); + + expect(getByTestId('alertsCardButton').querySelector('button')).toBeDisabled(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/alerts_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/alerts_card.tsx new file mode 100644 index 0000000000000..c0369ed23d61c --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/alerts_card.tsx @@ -0,0 +1,82 @@ +/* + * 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, { useCallback, useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; +import { SecurityPageName } from '@kbn/security-solution-navigation'; +import { SecuritySolutionLinkButton } from '../../../../../common/components/links'; +import { OnboardingCardId } from '../../../../constants'; +import type { OnboardingCardComponent } from '../../../../types'; +import { OnboardingCardContentImagePanel } from '../common/card_content_image_panel'; +import { CardCallOut } from '../common/card_callout'; +import alertsImageSrc from './images/alerts.png'; +import * as i18n from './translations'; + +export const AlertsCard: OnboardingCardComponent = ({ + isCardComplete, + setExpandedCardId, + setComplete, +}) => { + const isIntegrationsCardComplete = useMemo( + () => isCardComplete(OnboardingCardId.integrations), + [isCardComplete] + ); + + const expandIntegrationsCard = useCallback(() => { + setExpandedCardId(OnboardingCardId.integrations, { scroll: true }); + }, [setExpandedCardId]); + + return ( + + + + + {i18n.ALERTS_CARD_DESCRIPTION} + + {!isIntegrationsCardComplete && ( + <> + + + + {i18n.ALERTS_CARD_CALLOUT_INTEGRATIONS_BUTTON} + + + + + + } + /> + + )} + + + setComplete(true)} + deepLinkId={SecurityPageName.alerts} + fill + isDisabled={!isIntegrationsCardComplete} + > + {i18n.ALERTS_CARD_VIEW_ALERTS_BUTTON} + + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export default AlertsCard; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/images/alerts.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/images/alerts.png new file mode 100644 index 0000000000000..6eaf13bfc7b53 Binary files /dev/null and b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/images/alerts.png differ diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/images/alerts_icon.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/images/alerts_icon.png new file mode 100644 index 0000000000000..e1013a6eae7fc Binary files /dev/null and b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/images/alerts_icon.png differ diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/index.ts new file mode 100644 index 0000000000000..5ed5f4d34ce39 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/index.ts @@ -0,0 +1,19 @@ +/* + * 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 type { OnboardingCardConfig } from '../../../../types'; +import { OnboardingCardId } from '../../../../constants'; +import { ALERTS_CARD_TITLE } from './translations'; +import alertsIcon from './images/alerts_icon.png'; + +export const alertsCardConfig: OnboardingCardConfig = { + id: OnboardingCardId.alerts, + title: ALERTS_CARD_TITLE, + icon: alertsIcon, + Component: React.lazy(() => import('./alerts_card')), +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/translations.ts new file mode 100644 index 0000000000000..3138f01d20b66 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/translations.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ALERTS_CARD_TITLE = i18n.translate( + 'xpack.securitySolution.onboarding.alertsCard.title', + { + defaultMessage: 'View alerts', + } +); + +export const ALERTS_CARD_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.onboarding.alertsCard.description', + { + defaultMessage: + 'Visualize, sort, filter, and investigate alerts from across your infrastructure. Examine individual alerts of interest, and discover general patterns in alert volume and severity.', + } +); + +export const ALERTS_CARD_CALLOUT_INTEGRATIONS_TEXT = i18n.translate( + 'xpack.securitySolution.onboarding.alertsCard.calloutIntegrationsText', + { + defaultMessage: 'To view alerts add integrations first.', + } +); + +export const ALERTS_CARD_CALLOUT_INTEGRATIONS_BUTTON = i18n.translate( + 'xpack.securitySolution.onboarding.alertsCard.calloutIntegrationsButton', + { + defaultMessage: 'Add integrations step', + } +); + +export const ALERTS_CARD_VIEW_ALERTS_BUTTON = i18n.translate( + 'xpack.securitySolution.onboarding.alertsCard.viewAlertsButton', + { + defaultMessage: 'View alerts', + } +); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx new file mode 100644 index 0000000000000..d2592ae01dea0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx @@ -0,0 +1,77 @@ +/* + * 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, { useCallback, useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiText } from '@elastic/eui'; +import { css } from '@emotion/css'; +import type { AIConnector } from '@kbn/elastic-assistant/impl/connectorland/connector_selector'; +import { OnboardingCardId } from '../../../../constants'; +import type { OnboardingCardComponent } from '../../../../types'; +import * as i18n from './translations'; +import { OnboardingCardContentPanel } from '../common/card_content_panel'; +import { ConnectorCards } from './components/connectors/connector_cards'; +import { CardCallOut } from '../common/card_callout'; + +export const AssistantCard: OnboardingCardComponent = ({ + isCardComplete, + setExpandedCardId, + checkCompleteMetadata, + checkComplete, +}) => { + const isIntegrationsCardComplete = useMemo( + () => isCardComplete(OnboardingCardId.integrations), + [isCardComplete] + ); + + const expandIntegrationsCard = useCallback(() => { + setExpandedCardId(OnboardingCardId.integrations, { scroll: true }); + }, [setExpandedCardId]); + + const aiConnectors = checkCompleteMetadata?.connectors as AIConnector[]; + + return ( + + + + + {i18n.ASSISTANT_CARD_DESCRIPTION} + + + + {isIntegrationsCardComplete ? ( + + ) : ( + + + + {i18n.ASSISTANT_CARD_CALLOUT_INTEGRATIONS_BUTTON} + + + + + + } + /> + + )} + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export default AssistantCard; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_check_complete.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_check_complete.ts new file mode 100644 index 0000000000000..eae9f13bec8a0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_check_complete.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loadAllActions as loadConnectors } from '@kbn/triggers-actions-ui-plugin/public/common/constants'; +import type { AIConnector } from '@kbn/elastic-assistant/impl/connectorland/connector_selector'; +import { i18n } from '@kbn/i18n'; +import type { OnboardingCardCheckComplete } from '../../../../types'; +import { AllowedActionTypeIds } from './constants'; + +export const checkAssistantCardComplete: OnboardingCardCheckComplete = async ({ http }) => { + const allConnectors = await loadConnectors({ http }); + + const aiConnectors = allConnectors.reduce((acc: AIConnector[], connector) => { + if (!connector.isMissingSecrets && AllowedActionTypeIds.includes(connector.actionTypeId)) { + acc.push(connector); + } + return acc; + }, []); + + const completeBadgeText = i18n.translate( + 'xpack.securitySolution.onboarding.assistantCard.badge.completeText', + { + defaultMessage: '{count} AI {count, plural, one {connector} other {connectors}} added', + values: { count: aiConnectors.length }, + } + ); + + return { + isComplete: aiConnectors.length > 0, + completeBadgeText, + metadata: { + connectors: aiConnectors, + }, + }; +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/components/connectors/connector_cards.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/components/connectors/connector_cards.tsx new file mode 100644 index 0000000000000..42b3a5b14f039 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/components/connectors/connector_cards.tsx @@ -0,0 +1,83 @@ +/* + * 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 { type AIConnector } from '@kbn/elastic-assistant/impl/connectorland/connector_selector'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiLoadingSpinner, + EuiText, + EuiBadge, + EuiSpacer, +} from '@elastic/eui'; +import { css } from '@emotion/css'; +import { useKibana } from '../../../../../../../common/lib/kibana'; +import { CreateConnectorPopover } from './create_connector_popover'; +import { ConnectorSetup } from './connector_setup'; + +interface ConnectorCardsProps { + connectors?: AIConnector[]; + onConnectorSaved: () => void; + onClose?: () => void; +} + +export const ConnectorCards = React.memo( + ({ connectors, onConnectorSaved, onClose }) => { + const { + triggersActionsUi: { actionTypeRegistry }, + } = useKibana().services; + + if (!connectors) return ; + + if (connectors.length > 0) { + return ( + <> + + {connectors?.map((connector) => ( + + + + + {connector.name} + + + + {actionTypeRegistry.get(connector.actionTypeId).actionTypeTitle} + + + + + + ))} + + + + + ); + } + + return ; + } +); +ConnectorCards.displayName = 'ConnectorCards'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/components/connectors/connector_setup.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/components/connectors/connector_setup.tsx new file mode 100644 index 0000000000000..c0a82049d98c0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/components/connectors/connector_setup.tsx @@ -0,0 +1,146 @@ +/* + * 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, { useCallback, useState } from 'react'; +import { + useEuiTheme, + EuiFlexGroup, + EuiFlexItem, + EuiListGroup, + EuiIcon, + EuiPanel, + EuiLoadingSpinner, + EuiText, + EuiLink, + EuiTextColor, +} from '@elastic/eui'; +import { css } from '@emotion/css'; +import { + ConnectorAddModal, + type ActionConnector, +} from '@kbn/triggers-actions-ui-plugin/public/common/constants'; +import type { ActionType } from '@kbn/actions-plugin/common'; +import { useKibana } from '../../../../../../../common/lib/kibana'; +import { useFilteredActionTypes } from './hooks/use_load_action_types'; + +const usePanelCss = () => { + const { euiTheme } = useEuiTheme(); + return css` + .connectorSelectorPanel { + height: 160px; + &.euiPanel:hover { + background-color: ${euiTheme.colors.lightestShade}; + } + } + `; +}; + +interface ConnectorSetupProps { + onConnectorSaved?: (savedAction: ActionConnector) => void; + onClose?: () => void; + compressed?: boolean; +} +export const ConnectorSetup = React.memo( + ({ onConnectorSaved, onClose, compressed = false }) => { + const panelCss = usePanelCss(); + const { + http, + triggersActionsUi: { actionTypeRegistry }, + notifications: { toasts }, + } = useKibana().services; + const [selectedActionType, setSelectedActionType] = useState(null); + + const onModalClose = useCallback(() => { + setSelectedActionType(null); + onClose?.(); + }, [onClose]); + + const actionTypes = useFilteredActionTypes(http, toasts); + + if (!actionTypes) { + return ; + } + + return ( + <> + {compressed ? ( + ({ + id: actionType.id, + label: actionType.name, + size: 's', + icon: ( + + ), + isDisabled: !actionType.enabled, + onClick: () => setSelectedActionType(actionType), + }))} + /> + ) : ( + + {actionTypes?.map((actionType: ActionType) => ( + + setSelectedActionType(actionType)} + data-test-subj={`actionType-${actionType.id}`} + className={panelCss} + > + + + + + + + + {actionType.name} + + + + + + + ))} + + )} + + {selectedActionType && ( + + )} + + ); + } +); +ConnectorSetup.displayName = 'ConnectorSetup'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/components/connectors/create_connector_popover.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/components/connectors/create_connector_popover.tsx new file mode 100644 index 0000000000000..ebfac618d195e --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/components/connectors/create_connector_popover.tsx @@ -0,0 +1,45 @@ +/* + * 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, { useCallback, useState } from 'react'; +import { css } from '@emotion/css'; +import { EuiPopover, EuiLink, EuiText } from '@elastic/eui'; +import { ConnectorSetup } from './connector_setup'; +import * as i18n from '../../translations'; + +interface CreateConnectorPopoverProps { + onConnectorSaved: () => void; +} + +export const CreateConnectorPopover = React.memo( + ({ onConnectorSaved }) => { + const [isOpen, setIsPopoverOpen] = useState(false); + const closePopover = useCallback(() => setIsPopoverOpen(false), []); + + const onButtonClick = () => setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen); + + return ( + + + {i18n.ASSISTANT_CARD_CREATE_NEW_CONNECTOR_POPOVER} + + + } + isOpen={isOpen} + closePopover={closePopover} + data-test-subj="createConnectorPopover" + > + + + ); + } +); +CreateConnectorPopover.displayName = 'CreateConnectorPopover'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/components/connectors/hooks/use_load_action_types.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/components/connectors/hooks/use_load_action_types.ts new file mode 100644 index 0000000000000..106ce537099a8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/components/connectors/hooks/use_load_action_types.ts @@ -0,0 +1,17 @@ +/* + * 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 { useLoadActionTypes as loadActionTypes } from '@kbn/elastic-assistant/impl/connectorland/use_load_action_types'; +import type { HttpSetup } from '@kbn/core-http-browser'; +import type { IToasts } from '@kbn/core-notifications-browser'; +import { AllowedActionTypeIds } from '../../../constants'; + +export const useFilteredActionTypes = (http: HttpSetup, toasts: IToasts) => { + const { data } = loadActionTypes({ http, toasts }); + return useMemo(() => data?.filter(({ id }) => AllowedActionTypeIds.includes(id)), [data]); +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/constants.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/constants.ts new file mode 100644 index 0000000000000..35811c18de471 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/constants.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const AllowedActionTypeIds = ['.bedrock', '.gen-ai', '.gemini']; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/index.ts new file mode 100644 index 0000000000000..c80c07604717f --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/index.ts @@ -0,0 +1,21 @@ +/* + * 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 { AssistantAvatar } from '@kbn/elastic-assistant'; +import type { OnboardingCardConfig } from '../../../../types'; +import { OnboardingCardId } from '../../../../constants'; +import { ASSISTANT_CARD_TITLE } from './translations'; +import { checkAssistantCardComplete } from './assistant_check_complete'; + +export const assistantCardConfig: OnboardingCardConfig = { + id: OnboardingCardId.assistant, + title: ASSISTANT_CARD_TITLE, + icon: AssistantAvatar, + Component: React.lazy(() => import('./assistant_card')), + checkComplete: checkAssistantCardComplete, +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/translations.ts new file mode 100644 index 0000000000000..41e73bdacf061 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/translations.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ASSISTANT_CARD_TITLE = i18n.translate( + 'xpack.securitySolution.onboarding.assistantCard.title', + { + defaultMessage: 'Configure AI Assistant', + } +); + +export const ASSISTANT_CARD_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.onboarding.assistantCard.description', + { + defaultMessage: + 'Choose and configure any AI provider available to use with Elastic AI Assistant.', + } +); + +export const ASSISTANT_CARD_CALLOUT_INTEGRATIONS_TEXT = i18n.translate( + 'xpack.securitySolution.onboarding.assistantCard.calloutIntegrationsText', + { + defaultMessage: 'To add Elastic rules add integrations first.', + } +); + +export const ASSISTANT_CARD_CALLOUT_INTEGRATIONS_BUTTON = i18n.translate( + 'xpack.securitySolution.onboarding.assistantCard.calloutIntegrationsButton', + { + defaultMessage: 'Add integrations step', + } +); + +export const ASSISTANT_CARD_CREATE_NEW_CONNECTOR_POPOVER = i18n.translate( + 'xpack.securitySolution.onboarding.assistantCard.createNewConnectorPopover', + { + defaultMessage: 'Create new connector', + } +); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/attack_discover_card.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/attack_discover_card.test.tsx new file mode 100644 index 0000000000000..19b327f77487c --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/attack_discover_card.test.tsx @@ -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 React from 'react'; +import { render } from '@testing-library/react'; +import { AttackDiscoveryCard } from './attack_discovery_card'; +import { TestProviders } from '../../../../../common/mock'; + +const props = { + setComplete: jest.fn(), + checkComplete: jest.fn(), + isCardComplete: jest.fn(), + setExpandedCardId: jest.fn(), +}; + +describe('RulesCard', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('description should be in the document', () => { + const { getByTestId } = render( + + + + ); + + expect(getByTestId('attackDiscoveryCardDescription')).toBeInTheDocument(); + }); + + it('card callout should be rendered if integrations cards is not complete', () => { + props.isCardComplete.mockReturnValueOnce(false); + + const { getByText } = render( + + + + ); + + expect(getByText('To use Attack Discovery add integrations first.')).toBeInTheDocument(); + }); + + it('card button should be disabled if integrations cards is not complete', () => { + props.isCardComplete.mockReturnValueOnce(false); + + const { getByTestId } = render( + + + + ); + + expect(getByTestId('attackDiscoveryCardButton').querySelector('button')).toBeDisabled(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/attack_discovery_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/attack_discovery_card.tsx new file mode 100644 index 0000000000000..b34aa4367b09c --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/attack_discovery_card.tsx @@ -0,0 +1,87 @@ +/* + * 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, { useCallback, useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; +import { SecurityPageName } from '@kbn/security-solution-navigation'; +import { SecuritySolutionLinkButton } from '../../../../../common/components/links'; +import { OnboardingCardId } from '../../../../constants'; +import type { OnboardingCardComponent } from '../../../../types'; +import { OnboardingCardContentImagePanel } from '../common/card_content_image_panel'; +import { CardCallOut } from '../common/card_callout'; +import attackDiscoveryImageSrc from './images/attack_discovery.png'; +import * as i18n from './translations'; + +export const AttackDiscoveryCard: OnboardingCardComponent = ({ + isCardComplete, + setExpandedCardId, + setComplete, +}) => { + const isIntegrationsCardComplete = useMemo( + () => isCardComplete(OnboardingCardId.integrations), + [isCardComplete] + ); + + const expandIntegrationsCard = useCallback(() => { + setExpandedCardId(OnboardingCardId.integrations, { scroll: true }); + }, [setExpandedCardId]); + + return ( + + + + + {i18n.ATTACK_DISCOVERY_CARD_DESCRIPTION} + + {!isIntegrationsCardComplete && ( + <> + + + + + {i18n.ATTACK_DISCOVERY_CARD_CALLOUT_INTEGRATIONS_BUTTON} + + + + + + + } + /> + + )} + + + setComplete(true)} + deepLinkId={SecurityPageName.attackDiscovery} + fill + isDisabled={!isIntegrationsCardComplete} + > + {i18n.ATTACK_DISCOVERY_CARD_START_ATTACK_DISCOVERY_BUTTON} + + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export default AttackDiscoveryCard; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/images/attack_discovery.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/images/attack_discovery.png new file mode 100644 index 0000000000000..0d6b551e09661 Binary files /dev/null and b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/images/attack_discovery.png differ diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/images/attack_discovery_icon.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/images/attack_discovery_icon.png new file mode 100644 index 0000000000000..912b0cae64733 Binary files /dev/null and b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/images/attack_discovery_icon.png differ diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/index.ts new file mode 100644 index 0000000000000..cb47512fd8b45 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/index.ts @@ -0,0 +1,19 @@ +/* + * 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 type { OnboardingCardConfig } from '../../../../types'; +import { OnboardingCardId } from '../../../../constants'; +import { ATTACK_DISCOVERY_CARD_TITLE } from './translations'; +import attackDiscoveryIcon from './images/attack_discovery_icon.png'; + +export const attackDiscoveryCardConfig: OnboardingCardConfig = { + id: OnboardingCardId.attackDiscovery, + title: ATTACK_DISCOVERY_CARD_TITLE, + icon: attackDiscoveryIcon, + Component: React.lazy(() => import('./attack_discovery_card')), +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/translations.ts new file mode 100644 index 0000000000000..be1334b35b217 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/translations.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ATTACK_DISCOVERY_CARD_TITLE = i18n.translate( + 'xpack.securitySolution.onboarding.attackDiscoveryCard.title', + { + defaultMessage: 'Start using Attack Discovery', + } +); + +export const ATTACK_DISCOVERY_CARD_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.onboarding.attackDiscoveryCard.description', + { + defaultMessage: + 'Visualize, sort, filter, and investigate alerts from across your infrastructure. Examine individual alerts of interest, and discover general patterns in alert volume and severity.', + } +); + +export const ATTACK_DISCOVERY_CARD_CALLOUT_INTEGRATIONS_TEXT = i18n.translate( + 'xpack.securitySolution.onboarding.attackDiscoveryCard.calloutIntegrationsText', + { + defaultMessage: 'To use Attack Discovery add integrations first.', + } +); + +export const ATTACK_DISCOVERY_CARD_CALLOUT_INTEGRATIONS_BUTTON = i18n.translate( + 'xpack.securitySolution.onboarding.attackDiscoveryCard.calloutIntegrationsButton', + { + defaultMessage: 'Add integrations step', + } +); + +export const ATTACK_DISCOVERY_CARD_START_ATTACK_DISCOVERY_BUTTON = i18n.translate( + 'xpack.securitySolution.onboarding.attackDiscoveryCard.startAttackDiscoveryButton', + { + defaultMessage: 'Start using Attack Discovery', + } +); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.styles.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.styles.ts index 16e3c3820b310..d8f6d6c278ee3 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.styles.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.styles.ts @@ -12,8 +12,9 @@ export const useCardContentImagePanelStyles = () => { const { euiTheme } = useEuiTheme(); const shadowStyles = useEuiShadow('m'); return css` + padding-top: 8px; .cardSpacer { - width: 10%; + width: 8%; } .cardImage { width: 50%; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.tsx index 8b9fd9d9cc00c..0110e001af7ac 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.tsx @@ -16,7 +16,7 @@ export const OnboardingCardContentImagePanel = React.memo< >(({ children, imageSrc, imageAlt }) => { const styles = useCardContentImagePanelStyles(); return ( - + {children} diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx index 981a60a648508..3d5489b9be1cc 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx @@ -5,20 +5,24 @@ * 2.0. */ import React, { type PropsWithChildren } from 'react'; -import { EuiPanel, type EuiPanelProps } from '@elastic/eui'; +import { COLOR_MODES_STANDARD, EuiPanel, useEuiTheme, type EuiPanelProps } from '@elastic/eui'; import { css } from '@emotion/react'; export const OnboardingCardContentPanel = React.memo>( ({ children, ...panelProps }) => { + const { euiTheme, colorMode } = useEuiTheme(); + const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark; return ( - - + + {children} diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.test.tsx new file mode 100644 index 0000000000000..f7aa198eccab4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.test.tsx @@ -0,0 +1,76 @@ +/* + * 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 { fireEvent, render } from '@testing-library/react'; +import { DashboardsCard } from './dashboards_card'; +import { TestProviders } from '../../../../../common/mock'; +import { OnboardingCardId } from '../../../../constants'; + +jest.mock('../../../onboarding_context'); + +const props = { + setComplete: jest.fn(), + checkComplete: jest.fn(), + isCardComplete: jest.fn(), + setExpandedCardId: jest.fn(), +}; + +describe('RulesCard', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('description should be in the document', () => { + const { getByTestId } = render( + + + + ); + + expect(getByTestId('dashboardsDescription')).toBeInTheDocument(); + }); + + it('card callout should be rendered if integrations cards is not complete', () => { + props.isCardComplete.mockReturnValueOnce(false); + + const { getByText } = render( + + + + ); + + expect(getByText('To view dashboards add integrations first.')).toBeInTheDocument(); + }); + + it('card button should be disabled if integrations cards is not complete', () => { + props.isCardComplete.mockReturnValueOnce(false); + + const { getByTestId } = render( + + + + ); + + expect(getByTestId('dashboardsCardButton').querySelector('button')).toBeDisabled(); + }); + it('should expand integrations card when callout link is clicked', () => { + props.isCardComplete.mockReturnValueOnce(false); // To show the callout + + const { getByTestId } = render( + + + + ); + + const link = getByTestId('dashboardsCardCalloutLink'); + fireEvent.click(link); + + expect(props.setExpandedCardId).toHaveBeenCalledWith(OnboardingCardId.integrations, { + scroll: true, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.tsx index 6e46380a8e300..df98800d83f32 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.tsx @@ -42,7 +42,7 @@ export const DashboardsCard: OnboardingCardComponent = ({ alignItems="flexStart" > - + {i18n.DASHBOARDS_CARD_DESCRIPTION} {!isIntegrationsCardComplete && ( @@ -53,7 +53,10 @@ export const DashboardsCard: OnboardingCardComponent = ({ icon="iInCircle" text={i18n.DASHBOARDS_CARD_CALLOUT_INTEGRATIONS_TEXT} action={ - + {i18n.DASHBOARDS_CARD_CALLOUT_INTEGRATIONS_BUTTON} @@ -66,11 +69,9 @@ export const DashboardsCard: OnboardingCardComponent = ({ )} - + { - setComplete(true); - }} + onClick={() => setComplete(true)} linkId="goToDashboardsButton" cardId={OnboardingCardId.dashboards} deepLinkId={SecurityPageName.dashboards} diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/images/dashboards_icon.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/images/dashboards_icon.png new file mode 100644 index 0000000000000..ddc024696e224 Binary files /dev/null and b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/images/dashboards_icon.png differ diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts index b09a458847475..7c625b92feb67 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts @@ -9,11 +9,12 @@ import React from 'react'; import type { OnboardingCardConfig } from '../../../../types'; import { OnboardingCardId } from '../../../../constants'; import { DASHBOARDS_CARD_TITLE } from './translations'; +import dashboardsIcon from './images/dashboards_icon.png'; export const dashboardsCardConfig: OnboardingCardConfig = { id: OnboardingCardId.dashboards, title: DASHBOARDS_CARD_TITLE, - icon: 'dashboardApp', + icon: dashboardsIcon, Component: React.lazy( () => import('./dashboards_card' /* webpackChunkName: "onboarding_dashboards_card" */) ), diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/translations.ts index 33d7a2a9be98b..cf1a280122d79 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/translations.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/translations.ts @@ -30,7 +30,7 @@ export const DASHBOARDS_CARD_CALLOUT_INTEGRATIONS_TEXT = i18n.translate( export const DASHBOARDS_CARD_CALLOUT_INTEGRATIONS_BUTTON = i18n.translate( 'xpack.securitySolution.onboarding.dashboardsCard.calloutIntegrationsButton', { - defaultMessage: 'Add integrations', + defaultMessage: 'Add integrations step', } ); export const DASHBOARDS_CARD_GO_TO_DASHBOARDS_BUTTON = i18n.translate( diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/images/integrations_icon.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/images/integrations_icon.png new file mode 100644 index 0000000000000..438e220084c46 Binary files /dev/null and b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/images/integrations_icon.png differ diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts index 6da21d4e54581..509037edea985 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts @@ -10,13 +10,14 @@ import { i18n } from '@kbn/i18n'; import type { OnboardingCardConfig } from '../../../../types'; import { checkIntegrationsCardComplete } from './integrations_check_complete'; import { OnboardingCardId } from '../../../../constants'; +import integrationsIcon from './images/integrations_icon.png'; export const integrationsCardConfig: OnboardingCardConfig = { id: OnboardingCardId.integrations, title: i18n.translate('xpack.securitySolution.onboarding.integrationsCard.title', { defaultMessage: 'Add data with integrations', }), - icon: 'fleetApp', + icon: integrationsIcon, Component: React.lazy( () => import('./integrations_card' /* webpackChunkName: "onboarding_integrations_card" */) ), diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/images/rules.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/images/rules.png new file mode 100644 index 0000000000000..5d88e8c95d43c Binary files /dev/null and b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/images/rules.png differ diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/images/rules_icon.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/images/rules_icon.png new file mode 100644 index 0000000000000..b2b4848e0be1d Binary files /dev/null and b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/images/rules_icon.png differ diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/index.ts new file mode 100644 index 0000000000000..09a9380516339 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/index.ts @@ -0,0 +1,21 @@ +/* + * 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 type { OnboardingCardConfig } from '../../../../types'; +import { OnboardingCardId } from '../../../../constants'; +import { RULES_CARD_TITLE } from './translations'; +import rulesIcon from './images/rules_icon.png'; +import { checkRulesComplete } from './rules_check_complete'; + +export const rulesCardConfig: OnboardingCardConfig = { + id: OnboardingCardId.rules, + title: RULES_CARD_TITLE, + icon: rulesIcon, + Component: React.lazy(() => import('./rules_card')), + checkComplete: checkRulesComplete, +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/rules_card.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/rules_card.test.tsx new file mode 100644 index 0000000000000..f7156adc34eba --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/rules_card.test.tsx @@ -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 React from 'react'; +import { render } from '@testing-library/react'; +import { RulesCard } from './rules_card'; +import { TestProviders } from '../../../../../common/mock'; + +const props = { + setComplete: jest.fn(), + checkComplete: jest.fn(), + isCardComplete: jest.fn(), + setExpandedCardId: jest.fn(), +}; + +describe('RulesCard', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('description should be in the document', () => { + const { getByTestId } = render( + + + + ); + + expect(getByTestId('rulesCardDescription')).toBeInTheDocument(); + }); + + it('card callout should be rendered if integrations cards is not complete', () => { + props.isCardComplete.mockReturnValueOnce(false); + + const { getByText } = render( + + + + ); + + expect(getByText('To add Elastic rules add integrations first.')).toBeInTheDocument(); + }); + + it('card button should be disabled if integrations cards is not complete', () => { + props.isCardComplete.mockReturnValueOnce(false); + + const { getByTestId } = render( + + + + ); + + expect(getByTestId('rulesCardButton').querySelector('button')).toBeDisabled(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/rules_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/rules_card.tsx new file mode 100644 index 0000000000000..7f283c0ffbc78 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/rules_card.tsx @@ -0,0 +1,77 @@ +/* + * 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, { useCallback, useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; +import { SecurityPageName } from '@kbn/security-solution-navigation'; +import { SecuritySolutionLinkButton } from '../../../../../common/components/links'; +import { OnboardingCardId } from '../../../../constants'; +import type { OnboardingCardComponent } from '../../../../types'; +import { OnboardingCardContentImagePanel } from '../common/card_content_image_panel'; +import { CardCallOut } from '../common/card_callout'; +import rulesImageSrc from './images/rules.png'; +import * as i18n from './translations'; + +export const RulesCard: OnboardingCardComponent = ({ isCardComplete, setExpandedCardId }) => { + const isIntegrationsCardComplete = useMemo( + () => isCardComplete(OnboardingCardId.integrations), + [isCardComplete] + ); + + const expandIntegrationsCard = useCallback(() => { + setExpandedCardId(OnboardingCardId.integrations, { scroll: true }); + }, [setExpandedCardId]); + + return ( + + + + + {i18n.RULES_CARD_DESCRIPTION} + + {!isIntegrationsCardComplete && ( + <> + + + + {i18n.RULES_CARD_CALLOUT_INTEGRATIONS_BUTTON} + + + + + + } + /> + + )} + + + + {i18n.RULES_CARD_ADD_RULES_BUTTON} + + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export default RulesCard; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/rules_check_complete.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/rules_check_complete.ts new file mode 100644 index 0000000000000..3679141a255b4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/rules_check_complete.ts @@ -0,0 +1,62 @@ +/* + * 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 { HttpSetup } from '@kbn/core/public'; +import type { OnboardingCardCheckComplete } from '../../../../types'; +import { ENABLED_FIELD } from '../../../../../../common'; +import { DETECTION_ENGINE_RULES_URL_FIND } from '../../../../../../common/constants'; +import type { FetchRulesResponse } from '../../../../../detection_engine/rule_management/logic'; + +export const checkRulesComplete: OnboardingCardCheckComplete = async ({ + http, + notifications: { toasts }, +}) => { + // Check if there are any rules installed and enabled + try { + const data = await fetchRuleManagementFilters({ + http, + query: { + page: 1, + per_page: 20, + sort_field: 'enabled', + sort_order: 'desc', + filter: `${ENABLED_FIELD}: true`, + }, + }); + return { + isComplete: data?.total > 0, + }; + } catch (e) { + toasts.addError(e, { title: `Failed to check Card Rules completion.` }); + + return { + isComplete: false, + }; + } +}; + +const fetchRuleManagementFilters = async ({ + http, + signal, + query, +}: { + http: HttpSetup; + signal?: AbortSignal; + query?: { + page: number; + per_page: number; + sort_field: string; + sort_order: string; + filter: string; + }; +}): Promise => + http.fetch(DETECTION_ENGINE_RULES_URL_FIND, { + method: 'GET', + version: '2023-10-31', + signal, + query, + }); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/translations.ts new file mode 100644 index 0000000000000..81e0919cd7184 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/translations.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const RULES_CARD_TITLE = i18n.translate( + 'xpack.securitySolution.onboarding.rulesCard.title', + { + defaultMessage: 'Enable rules', + } +); + +export const RULES_CARD_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.onboarding.rulesCard.description', + { + defaultMessage: + 'Elastic Security comes with prebuilt detection rules that run in the background and create alerts when their conditions are met.', + } +); + +export const RULES_CARD_CALLOUT_INTEGRATIONS_TEXT = i18n.translate( + 'xpack.securitySolution.onboarding.rulesCard.calloutIntegrationsText', + { + defaultMessage: 'To add Elastic rules add integrations first.', + } +); + +export const RULES_CARD_CALLOUT_INTEGRATIONS_BUTTON = i18n.translate( + 'xpack.securitySolution.onboarding.rulesCard.calloutIntegrationsButton', + { + defaultMessage: 'Add integrations step', + } +); + +export const RULES_CARD_ADD_RULES_BUTTON = i18n.translate( + 'xpack.securitySolution.onboarding.rulesCard.addRulesButton', + { + defaultMessage: 'Add Elastic rules', + } +); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.ts index 98eb48a02365c..5903af89b5642 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.ts @@ -6,16 +6,16 @@ */ import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useKibana } from '../../../../common/lib/kibana'; import { useStoredCompletedCardIds } from '../../../hooks/use_stored_state'; import type { OnboardingCardId } from '../../../constants'; import type { CheckCompleteResult, CheckCompleteResponse, - OnboardingCardConfig, OnboardingGroupConfig, + OnboardingCardConfig, } from '../../../types'; import { useOnboardingContext } from '../../onboarding_context'; -import { useKibana } from '../../../../common/lib/kibana'; export type IsCardComplete = (cardId: OnboardingCardId) => boolean; export type SetCardComplete = ( @@ -118,7 +118,7 @@ export const useCompletedCards = (cardsGroupConfig: OnboardingGroupConfig[]) => }); } }, - [cardsWithAutoCheck, services, processCardCheckCompleteResult] + [cardsWithAutoCheck, processCardCheckCompleteResult, services] ); // Initial auto-check for all cards, it should run only once, after cardsGroupConfig is properly populated @@ -128,7 +128,7 @@ export const useCompletedCards = (cardsGroupConfig: OnboardingGroupConfig[]) => processCardCheckCompleteResult(card.id, checkCompleteResult); }) ); - }, [cardsWithAutoCheck, services, processCardCheckCompleteResult]); + }, [cardsWithAutoCheck, processCardCheckCompleteResult, services]); return { isCardComplete, diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx index d363bb702d192..3209028e1f0cd 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx @@ -41,6 +41,13 @@ export const OnboardingBody = React.memo(() => { [setCardComplete] ); + const createCheckCardComplete = useCallback( + (cardId: OnboardingCardId) => () => { + checkCardComplete(cardId); + }, + [checkCardComplete] + ); + return ( {bodyConfig.map((group, index) => ( @@ -64,6 +71,7 @@ export const OnboardingBody = React.memo(() => { }> { - const { euiTheme } = useEuiTheme(); + const { euiTheme, colorMode } = useEuiTheme(); const successBackgroundColor = useEuiBackgroundColor('success'); + const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark; return css` .onboardingCardHeader { @@ -22,7 +23,7 @@ export const useCardPanelStyles = () => { .onboardingCardIcon { padding: ${euiTheme.size.m}; border-radius: 50%; - background-color: ${euiTheme.colors.lightestShade}; + background-color: ${isDarkMode ? euiTheme.colors.body : euiTheme.colors.lightestShade}; } .onboardingCardHeaderTitle { font-weight: ${euiTheme.font.weight.semiBold}; @@ -56,5 +57,11 @@ export const useCardPanelStyles = () => { background-color: ${successBackgroundColor}; } } + ${isDarkMode + ? ` + background-color: ${euiTheme.colors.lightestShade}; + border: 1px solid ${euiTheme.colors.mediumShade}; + ` + : ''} `; }; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.styles.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.styles.ts index c39c9b458f478..8643c3254a6ee 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.styles.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.styles.ts @@ -14,7 +14,6 @@ export const useCardStyles = () => { return css` min-width: 315px; - /* We needed to add the "headerCard" class to make it take priority over the default EUI card styles */ &.headerCard:hover { *:not(.headerCardLink) { diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header.tsx index 63314bd4c9864..0210c88186a9a 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header.tsx @@ -4,12 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.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 { @@ -22,7 +16,6 @@ import { EuiTitle, useEuiTheme, } from '@elastic/eui'; - import { useCurrentUser } from '../../../common/lib/kibana/hooks'; import { useOnboardingHeaderStyles } from './onboarding_header.styles'; import rocketImage from './images/header_rocket.png'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/translations.ts index 99ae806031643..c1f8ca8695bb6 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/translations.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/translations.ts @@ -23,6 +23,6 @@ export const GET_STARTED_DATA_INGESTION_HUB_SUBTITLE = i18n.translate( export const GET_STARTED_DATA_INGESTION_HUB_DESCRIPTION = i18n.translate( 'xpack.securitySolution.onboarding.description', { - defaultMessage: `Follow these steps to set up your workspace.`, + defaultMessage: `A SIEM with AI-driven security analytics, XDR and Cloud Security.`, } ); diff --git a/x-pack/plugins/security_solution/public/onboarding/constants.ts b/x-pack/plugins/security_solution/public/onboarding/constants.ts index 039b83f754093..0b3a1f885838c 100644 --- a/x-pack/plugins/security_solution/public/onboarding/constants.ts +++ b/x-pack/plugins/security_solution/public/onboarding/constants.ts @@ -11,4 +11,6 @@ export enum OnboardingCardId { dashboards = 'dashboards', rules = 'rules', alerts = 'alerts', + assistant = 'asistant', + attackDiscovery = 'attack_discovery', } diff --git a/x-pack/plugins/security_solution/public/onboarding/types.ts b/x-pack/plugins/security_solution/public/onboarding/types.ts index 1f7e220a5c06b..7142949eb29b4 100644 --- a/x-pack/plugins/security_solution/public/onboarding/types.ts +++ b/x-pack/plugins/security_solution/public/onboarding/types.ts @@ -49,6 +49,10 @@ export type OnboardingCardComponent = React.ComponentType<{ * Function to set the current card completion status. */ setComplete: SetComplete; + /** + * Function to check the current card completion status again. + */ + checkComplete: () => void; /** * Function to check if a specific card is complete. */