[Security Solution] [Onboarding] t1_analyst role blocked from interacting with cards due to missing integration privileges (#202413)

## Summary

This PR temporarily fixes the #201799 issue for Serverless.



https://github.com/user-attachments/assets/604128cb-49b0-4a93-9a15-2a5a0c511883



### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
This commit is contained in:
Agustina Nahir Ruidiaz 2024-12-02 15:23:56 +01:00 committed by GitHub
parent 97dab1030d
commit 3daaaa5b8c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 149 additions and 21 deletions

View file

@ -14,6 +14,7 @@ const props = {
checkComplete: jest.fn(),
isCardComplete: jest.fn(),
setExpandedCardId: jest.fn(),
isCardAvailable: jest.fn(),
};
describe('AlertsCard', () => {
@ -31,7 +32,8 @@ describe('AlertsCard', () => {
expect(getByTestId('alertsCardDescription')).toBeInTheDocument();
});
it('card callout should be rendered if integrations cards is not complete', () => {
it('card callout should be rendered if integrations card is available but not complete', () => {
props.isCardAvailable.mockReturnValueOnce(true);
props.isCardComplete.mockReturnValueOnce(false);
const { getByText } = render(
@ -43,7 +45,20 @@ describe('AlertsCard', () => {
expect(getByText('To view alerts add integrations first.')).toBeInTheDocument();
});
it('card button should be disabled if integrations cards is not complete', () => {
it('card callout should not be rendered if integrations card is not available', () => {
props.isCardAvailable.mockReturnValueOnce(false);
const { queryByText } = render(
<TestProviders>
<AlertsCard {...props} />
</TestProviders>
);
expect(queryByText('To view alerts add integrations first.')).not.toBeInTheDocument();
});
it('card button should be disabled if integrations card is available but not complete', () => {
props.isCardAvailable.mockReturnValueOnce(true);
props.isCardComplete.mockReturnValueOnce(false);
const { getByTestId } = render(
@ -54,4 +69,17 @@ describe('AlertsCard', () => {
expect(getByTestId('alertsCardButton').querySelector('button')).toBeDisabled();
});
it('card button should be enabled if integrations card is complete', () => {
props.isCardAvailable.mockReturnValueOnce(true);
props.isCardComplete.mockReturnValueOnce(true);
const { getByTestId } = render(
<TestProviders>
<AlertsCard {...props} />
</TestProviders>
);
expect(getByTestId('alertsCardButton').querySelector('button')).not.toBeDisabled();
});
});

View file

@ -21,12 +21,18 @@ export const AlertsCard: OnboardingCardComponent = ({
isCardComplete,
setExpandedCardId,
setComplete,
isCardAvailable,
}) => {
const isIntegrationsCardComplete = useMemo(
() => isCardComplete(OnboardingCardId.integrations),
[isCardComplete]
);
const isIntegrationsCardAvailable = useMemo(
() => isCardAvailable(OnboardingCardId.integrations),
[isCardAvailable]
);
const expandIntegrationsCard = useCallback(() => {
setExpandedCardId(OnboardingCardId.integrations, { scroll: true });
}, [setExpandedCardId]);
@ -43,7 +49,7 @@ export const AlertsCard: OnboardingCardComponent = ({
<CardSubduedText data-test-subj="alertsCardDescription" size="s">
{i18n.ALERTS_CARD_DESCRIPTION}
</CardSubduedText>
{!isIntegrationsCardComplete && (
{isIntegrationsCardAvailable && !isIntegrationsCardComplete && (
<>
<EuiSpacer size="m" />
<CardCallOut
@ -69,7 +75,7 @@ export const AlertsCard: OnboardingCardComponent = ({
onClick={() => setComplete(true)}
deepLinkId={SecurityPageName.alerts}
fill
isDisabled={!isIntegrationsCardComplete}
isDisabled={isIntegrationsCardAvailable && !isIntegrationsCardComplete}
>
{i18n.ALERTS_CARD_VIEW_ALERTS_BUTTON}
</SecuritySolutionLinkButton>

View file

@ -23,12 +23,18 @@ export const AssistantCard: OnboardingCardComponent<AssistantCardMetadata> = ({
setExpandedCardId,
checkCompleteMetadata,
checkComplete,
isCardAvailable,
}) => {
const isIntegrationsCardComplete = useMemo(
() => isCardComplete(OnboardingCardId.integrations),
[isCardComplete]
);
const isIntegrationsCardAvailable = useMemo(
() => isCardAvailable(OnboardingCardId.integrations),
[isCardAvailable]
);
const expandIntegrationsCard = useCallback(() => {
setExpandedCardId(OnboardingCardId.integrations, { scroll: true });
}, [setExpandedCardId]);
@ -45,13 +51,7 @@ export const AssistantCard: OnboardingCardComponent<AssistantCardMetadata> = ({
<CardSubduedText size="s">{i18n.ASSISTANT_CARD_DESCRIPTION}</CardSubduedText>
</EuiFlexItem>
<EuiFlexItem>
{isIntegrationsCardComplete ? (
<ConnectorCards
canCreateConnectors={canCreateConnectors}
connectors={connectors}
onConnectorSaved={checkComplete}
/>
) : (
{isIntegrationsCardAvailable && !isIntegrationsCardComplete ? (
<EuiFlexItem
className={css`
width: 45%;
@ -73,6 +73,12 @@ export const AssistantCard: OnboardingCardComponent<AssistantCardMetadata> = ({
}
/>
</EuiFlexItem>
) : (
<ConnectorCards
canCreateConnectors={canCreateConnectors}
connectors={connectors}
onConnectorSaved={checkComplete}
/>
)}
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -14,6 +14,7 @@ const props = {
checkComplete: jest.fn(),
isCardComplete: jest.fn(),
setExpandedCardId: jest.fn(),
isCardAvailable: jest.fn(),
};
describe('RulesCard', () => {

View file

@ -17,9 +17,10 @@ const props = {
checkComplete: jest.fn(),
isCardComplete: jest.fn(),
setExpandedCardId: jest.fn(),
isCardAvailable: jest.fn(),
};
describe('RulesCard', () => {
describe('DashboardsCard', () => {
beforeEach(() => {
jest.clearAllMocks();
});
@ -34,7 +35,8 @@ describe('RulesCard', () => {
expect(getByTestId('dashboardsDescription')).toBeInTheDocument();
});
it('card callout should be rendered if integrations cards is not complete', () => {
it('card callout should be rendered if integrations card is available but not complete', () => {
props.isCardAvailable.mockReturnValueOnce(true);
props.isCardComplete.mockReturnValueOnce(false);
const { getByText } = render(
@ -46,7 +48,20 @@ describe('RulesCard', () => {
expect(getByText('To view dashboards add integrations first.')).toBeInTheDocument();
});
it('card button should be disabled if integrations cards is not complete', () => {
it('card callout should not be rendered if integrations card is not available', () => {
props.isCardAvailable.mockReturnValueOnce(false);
const { queryByText } = render(
<TestProviders>
<DashboardsCard {...props} />
</TestProviders>
);
expect(queryByText('To view dashboards add integrations first.')).not.toBeInTheDocument();
});
it('card button should be disabled if integrations card is available but not complete', () => {
props.isCardAvailable.mockReturnValueOnce(true);
props.isCardComplete.mockReturnValueOnce(false);
const { getByTestId } = render(
@ -57,7 +72,22 @@ describe('RulesCard', () => {
expect(getByTestId('dashboardsCardButton').querySelector('button')).toBeDisabled();
});
it('card button should be enabled if integrations card is complete', () => {
props.isCardAvailable.mockReturnValueOnce(true);
props.isCardComplete.mockReturnValueOnce(true);
const { getByTestId } = render(
<TestProviders>
<DashboardsCard {...props} />
</TestProviders>
);
expect(getByTestId('dashboardsCardButton').querySelector('button')).not.toBeDisabled();
});
it('should expand integrations card when callout link is clicked', () => {
props.isCardAvailable.mockReturnValueOnce(true);
props.isCardComplete.mockReturnValueOnce(false); // To show the callout
const { getByTestId } = render(

View file

@ -21,12 +21,18 @@ export const DashboardsCard: OnboardingCardComponent = ({
isCardComplete,
setComplete,
setExpandedCardId,
isCardAvailable,
}) => {
const isIntegrationsCardComplete = useMemo(
() => isCardComplete(OnboardingCardId.integrations),
[isCardComplete]
);
const isIntegrationsCardAvailable = useMemo(
() => isCardAvailable(OnboardingCardId.integrations),
[isCardAvailable]
);
const expandIntegrationsCard = useCallback(() => {
setExpandedCardId(OnboardingCardId.integrations, { scroll: true });
}, [setExpandedCardId]);
@ -46,7 +52,7 @@ export const DashboardsCard: OnboardingCardComponent = ({
<CardSubduedText data-test-subj="dashboardsDescription" size="s">
{i18n.DASHBOARDS_CARD_DESCRIPTION}
</CardSubduedText>
{!isIntegrationsCardComplete && (
{isIntegrationsCardAvailable && !isIntegrationsCardComplete && (
<>
<EuiSpacer size="m" />
<CardCallOut
@ -77,7 +83,7 @@ export const DashboardsCard: OnboardingCardComponent = ({
cardId={OnboardingCardId.dashboards}
deepLinkId={SecurityPageName.dashboards}
fill
isDisabled={!isIntegrationsCardComplete}
isDisabled={isIntegrationsCardAvailable && !isIntegrationsCardComplete}
>
{i18n.DASHBOARDS_CARD_GO_TO_DASHBOARDS_BUTTON}
</CardLinkButton>

View file

@ -15,6 +15,7 @@ const props = {
checkComplete: jest.fn(),
isCardComplete: jest.fn(),
setExpandedCardId: jest.fn(),
isCardAvailable: jest.fn(),
};
describe('IntegrationsCard', () => {

View file

@ -13,6 +13,7 @@ const props = {
setComplete: jest.fn(),
checkComplete: jest.fn(),
isCardComplete: jest.fn(),
isCardAvailable: jest.fn(),
setExpandedCardId: jest.fn(),
};
@ -31,7 +32,8 @@ describe('RulesCard', () => {
expect(getByTestId('rulesCardDescription')).toBeInTheDocument();
});
it('card callout should be rendered if integrations cards is not complete', () => {
it('card callout should be rendered if integrations card is available but not complete', () => {
props.isCardAvailable.mockReturnValueOnce(true);
props.isCardComplete.mockReturnValueOnce(false);
const { getByText } = render(
@ -43,7 +45,20 @@ describe('RulesCard', () => {
expect(getByText('To add Elastic rules add integrations first.')).toBeInTheDocument();
});
it('card button should be disabled if integrations cards is not complete', () => {
it('card callout should not be rendered if integrations card is not available', () => {
props.isCardAvailable.mockReturnValueOnce(false);
const { queryByText } = render(
<TestProviders>
<RulesCard {...props} />
</TestProviders>
);
expect(queryByText('To add Elastic rules add integrations first.')).not.toBeInTheDocument();
});
it('card button should be disabled if integrations card is available but not complete', () => {
props.isCardAvailable.mockReturnValueOnce(true);
props.isCardComplete.mockReturnValueOnce(false);
const { getByTestId } = render(
@ -54,4 +69,17 @@ describe('RulesCard', () => {
expect(getByTestId('rulesCardButton').querySelector('button')).toBeDisabled();
});
it('card button should be enabled if integrations card is complete', () => {
props.isCardAvailable.mockReturnValueOnce(true);
props.isCardComplete.mockReturnValueOnce(true);
const { getByTestId } = render(
<TestProviders>
<RulesCard {...props} />
</TestProviders>
);
expect(getByTestId('rulesCardButton').querySelector('button')).not.toBeDisabled();
});
});

View file

@ -17,12 +17,21 @@ import { CardSubduedText } from '../common/card_subdued_text';
import rulesImageSrc from './images/rules.png';
import * as i18n from './translations';
export const RulesCard: OnboardingCardComponent = ({ isCardComplete, setExpandedCardId }) => {
export const RulesCard: OnboardingCardComponent = ({
isCardComplete,
setExpandedCardId,
isCardAvailable,
}) => {
const isIntegrationsCardComplete = useMemo(
() => isCardComplete(OnboardingCardId.integrations),
[isCardComplete]
);
const isIntegrationsCardAvailable = useMemo(
() => isCardAvailable(OnboardingCardId.integrations),
[isCardAvailable]
);
const expandIntegrationsCard = useCallback(() => {
setExpandedCardId(OnboardingCardId.integrations, { scroll: true });
}, [setExpandedCardId]);
@ -39,7 +48,7 @@ export const RulesCard: OnboardingCardComponent = ({ isCardComplete, setExpanded
<CardSubduedText data-test-subj="rulesCardDescription" size="s">
{i18n.RULES_CARD_DESCRIPTION}
</CardSubduedText>
{!isIntegrationsCardComplete && (
{isIntegrationsCardAvailable && !isIntegrationsCardComplete && (
<>
<EuiSpacer size="m" />
<CardCallOut
@ -64,7 +73,7 @@ export const RulesCard: OnboardingCardComponent = ({ isCardComplete, setExpanded
<SecuritySolutionLinkButton
deepLinkId={SecurityPageName.rules}
fill
isDisabled={!isIntegrationsCardComplete}
isDisabled={isIntegrationsCardAvailable && !isIntegrationsCardComplete}
>
{i18n.RULES_CARD_ADD_RULES_BUTTON}
</SecuritySolutionLinkButton>

View file

@ -14,6 +14,7 @@ import { OnboardingCardGroup } from './onboarding_card_group';
import { OnboardingCardPanel } from './onboarding_card_panel';
import { useExpandedCard } from './hooks/use_expanded_card';
import { useCompletedCards } from './hooks/use_completed_cards';
import type { IsCardAvailable } from '../../types';
export const OnboardingBody = React.memo(() => {
const bodyConfig = useBodyConfig();
@ -47,6 +48,12 @@ export const OnboardingBody = React.memo(() => {
[checkCardComplete]
);
const isCardAvailable = useCallback<IsCardAvailable>(
(cardId: OnboardingCardId) =>
bodyConfig.some((group) => group.cards.some((card) => card.id === cardId)),
[bodyConfig]
);
return (
<EuiFlexGroup direction="column" gutterSize="xl">
{bodyConfig.map((group, index) => (
@ -72,6 +79,7 @@ export const OnboardingBody = React.memo(() => {
setComplete={createSetCardComplete(id)}
checkComplete={createCheckCardComplete(id)}
isCardComplete={isCardComplete}
isCardAvailable={isCardAvailable}
setExpandedCardId={setExpandedCardId}
checkCompleteMetadata={cardCheckCompleteResult?.metadata}
/>

View file

@ -42,6 +42,7 @@ export type CheckCompleteResponse<TMetadata extends {} = {}> =
export type SetComplete = (isComplete: boolean) => void;
export type IsCardComplete = (cardId: OnboardingCardId) => boolean;
export type IsCardAvailable = (cardId: OnboardingCardId) => boolean;
export type SetExpandedCardId = (
cardId: OnboardingCardId | null,
options?: { scroll?: boolean }
@ -60,6 +61,10 @@ export type OnboardingCardComponent<TMetadata extends {} = {}> = React.Component
* Function to check if a specific card is complete.
*/
isCardComplete: IsCardComplete;
/**
* Function to check if a specific card is rendered.
*/
isCardAvailable: IsCardAvailable;
/**
* Function to expand a specific card ID and scroll to it.
*/