Skip to content

Commit

Permalink
Merge pull request elastic#9 from agusruidiazgd/feat/onboarding_hub_n…
Browse files Browse the repository at this point in the history
…ew_cards

Onboarding hub new cards content
  • Loading branch information
semd authored Oct 10, 2024
2 parents b16a1f7 + 13b2178 commit f59ae53
Show file tree
Hide file tree
Showing 48 changed files with 1,294 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
>
<OnboardingHeader />
<OnboardingBody />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
{
Expand All @@ -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],
},
];
Original file line number Diff line number Diff line change
@@ -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(
<TestProviders>
<AlertsCard {...props} />
</TestProviders>
);

expect(getByTestId('alertsCardDescription')).toBeInTheDocument();
});

it('card callout should be rendered if integrations cards is not complete', () => {
props.isCardComplete.mockReturnValueOnce(false);

const { getByText } = render(
<TestProviders>
<AlertsCard {...props} />
</TestProviders>
);

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(
<TestProviders>
<AlertsCard {...props} />
</TestProviders>
);

expect(getByTestId('alertsCardButton').querySelector('button')).toBeDisabled();
});
});
Original file line number Diff line number Diff line change
@@ -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 (
<OnboardingCardContentImagePanel imageSrc={alertsImageSrc} imageAlt={i18n.ALERTS_CARD_TITLE}>
<EuiFlexGroup
direction="column"
gutterSize="xl"
justifyContent="flexStart"
alignItems="flexStart"
>
<EuiFlexItem grow={false}>
<EuiText data-test-subj="alertsCardDescription" size="s" color="subdued">
{i18n.ALERTS_CARD_DESCRIPTION}
</EuiText>
{!isIntegrationsCardComplete && (
<>
<EuiSpacer size="m" />
<CardCallOut
color="primary"
icon="iInCircle"
text={i18n.ALERTS_CARD_CALLOUT_INTEGRATIONS_TEXT}
action={
<EuiLink onClick={expandIntegrationsCard}>
<EuiFlexGroup direction="row" gutterSize="xs" alignItems="center">
<EuiFlexItem>{i18n.ALERTS_CARD_CALLOUT_INTEGRATIONS_BUTTON}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIcon type="arrowRight" color="primary" size="s" />
</EuiFlexItem>
</EuiFlexGroup>
</EuiLink>
}
/>
</>
)}
</EuiFlexItem>
<EuiFlexItem data-test-subj="alertsCardButton" grow={false}>
<SecuritySolutionLinkButton
onClick={() => setComplete(true)}
deepLinkId={SecurityPageName.alerts}
fill
isDisabled={!isIntegrationsCardComplete}
>
{i18n.ALERTS_CARD_VIEW_ALERTS_BUTTON}
</SecuritySolutionLinkButton>
</EuiFlexItem>
</EuiFlexGroup>
</OnboardingCardContentImagePanel>
);
};

// eslint-disable-next-line import/no-default-export
export default AlertsCard;
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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')),
};
Original file line number Diff line number Diff line change
@@ -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',
}
);
Original file line number Diff line number Diff line change
@@ -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 (
<OnboardingCardContentPanel style={{ paddingTop: 0 }}>
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<EuiText size="s" color="subdued">
{i18n.ASSISTANT_CARD_DESCRIPTION}
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
{isIntegrationsCardComplete ? (
<ConnectorCards connectors={aiConnectors} onConnectorSaved={checkComplete} />
) : (
<EuiFlexItem
className={css`
width: 45%;
`}
>
<CardCallOut
color="primary"
icon="iInCircle"
text={i18n.ASSISTANT_CARD_CALLOUT_INTEGRATIONS_TEXT}
action={
<EuiLink onClick={expandIntegrationsCard}>
<EuiFlexGroup direction="row" gutterSize="xs" alignItems="center">
<EuiFlexItem>{i18n.ASSISTANT_CARD_CALLOUT_INTEGRATIONS_BUTTON}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIcon type="arrowRight" color="primary" size="s" />
</EuiFlexItem>
</EuiFlexGroup>
</EuiLink>
}
/>
</EuiFlexItem>
)}
</EuiFlexItem>
</EuiFlexGroup>
</OnboardingCardContentPanel>
);
};

// eslint-disable-next-line import/no-default-export
export default AssistantCard;
Original file line number Diff line number Diff line change
@@ -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,
},
};
};
Loading

0 comments on commit f59ae53

Please sign in to comment.