mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Asset Inventory] Onboarding Permission Denied component (#216113)
## Summary This PR adds the Permission Denied screen to the Asset Inventory onboarding to handle users with insufficient privileges attempting to enable the entity store. It currently relies on the same mechanisms and reuses the same callout message used by the Entity Store page. this pr also updates the following: - update texts on the get started and initializing screens to match the latest updates - update the MissingPrivilegesCallout component to allow displaying full message instead of line clamping - adds MissingPrivilegesCallout unit tests ## Screenshots <img width="1551" alt="image" src="https://github.com/user-attachments/assets/4bde6f6f-8feb-49da-a8e0-c68e324cb782" />
This commit is contained in:
parent
a382856f71
commit
a467201723
16 changed files with 192 additions and 57 deletions
|
@ -13,12 +13,12 @@ export type AssetInventoryStatus =
|
|||
| 'disabled'
|
||||
| 'initializing'
|
||||
| 'empty'
|
||||
| 'permission_denied'
|
||||
| 'insufficient_privileges'
|
||||
| 'ready';
|
||||
|
||||
export interface AssetInventoryStatusResponse {
|
||||
status: AssetInventoryStatus;
|
||||
privileges?: EntityAnalyticsPrivileges['privileges'];
|
||||
privileges?: EntityAnalyticsPrivileges;
|
||||
}
|
||||
|
||||
export type AssetInventoryEnableResponse = InitEntityStoreResponse;
|
||||
|
|
|
@ -12,6 +12,7 @@ import { AssetInventoryLoading } from '../asset_inventory_loading';
|
|||
import { useAssetInventoryStatus } from '../../hooks/use_asset_inventory_status';
|
||||
import { Initializing } from './initializing';
|
||||
import { NoDataFound } from './no_data_found';
|
||||
import { PermissionDenied } from './permission_denied';
|
||||
|
||||
/**
|
||||
* This component serves as a wrapper to render appropriate onboarding screens
|
||||
|
@ -35,13 +36,8 @@ export const AssetInventoryOnboarding: FC<PropsWithChildren> = ({ children }) =>
|
|||
return <Initializing />;
|
||||
case 'empty': // Onboarding cannot proceed because no relevant data was found.
|
||||
return <NoDataFound />;
|
||||
case 'permission_denied': // Todo: User lacks the necessary permissions to proceed.
|
||||
return (
|
||||
<div>
|
||||
{'Permission denied.'}
|
||||
<pre>{JSON.stringify(privileges)}</pre>
|
||||
</div>
|
||||
);
|
||||
case 'insufficient_privileges': // User lacks the necessary permissions to proceed.
|
||||
return <PermissionDenied privileges={privileges} />;
|
||||
default:
|
||||
// If no onboarding status matches, render the child components.
|
||||
return children;
|
||||
|
|
|
@ -30,11 +30,7 @@ describe('GetStarted Component', () => {
|
|||
|
||||
expect(screen.getByText(/get started with asset inventory/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /enable asset inventory/i })).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(/read documentation/i).closest('a')).toHaveAttribute(
|
||||
'href',
|
||||
'https://ela.st/asset-inventory'
|
||||
);
|
||||
expect(screen.getByText(/need help?/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls enableAssetInventory when enable asset inventory button is clicked', async () => {
|
||||
|
|
|
@ -10,8 +10,6 @@ import {
|
|||
EuiImage,
|
||||
EuiEmptyPrompt,
|
||||
EuiButton,
|
||||
EuiLink,
|
||||
EuiTitle,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiCallOut,
|
||||
|
@ -25,8 +23,7 @@ import { HoverForExplanationTooltip } from './hover_for_explanation_tooltip';
|
|||
import { EmptyStateIllustrationContainer } from '../empty_state_illustration_container';
|
||||
import { useEnableAssetInventory } from './hooks/use_enable_asset_inventory';
|
||||
import { TEST_SUBJ_ONBOARDING_GET_STARTED } from '../../constants';
|
||||
|
||||
const ASSET_INVENTORY_DOCS_URL = 'https://ela.st/asset-inventory';
|
||||
import { NeedHelp } from './need_help';
|
||||
|
||||
export const GetStarted = () => {
|
||||
const { isEnabling, error, reset, enableAssetInventory } = useEnableAssetInventory();
|
||||
|
@ -67,7 +64,7 @@ export const GetStarted = () => {
|
|||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.assetInventory.onboarding.getStarted.description"
|
||||
defaultMessage="Asset Inventory gives you a unified view of all assets detected by Elastic Security, including those observed in logs, events, or discovered through integrations with sources like {identity_providers}, {cloud_services}, {mdms}, and configuration management {databases}."
|
||||
defaultMessage="Asset Inventory provides a unified view of all your organizations assets in one place. See everything detected by Elastic Security — whether from logs, {identity_providers}, {cloud_services}, {mdms} or configuration {databases} — all in a structured, searchable inventory. Enable Asset Inventory to gain complete visibility across your environment."
|
||||
values={{
|
||||
identity_providers: (
|
||||
<HoverForExplanationTooltip
|
||||
|
@ -167,24 +164,7 @@ export const GetStarted = () => {
|
|||
)}
|
||||
</EuiButton>,
|
||||
]}
|
||||
footer={
|
||||
<>
|
||||
<EuiTitle size="xxs">
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.assetInventory.emptyState.needHelp"
|
||||
defaultMessage="Need help?"
|
||||
/>
|
||||
</strong>
|
||||
</EuiTitle>{' '}
|
||||
<EuiLink href={ASSET_INVENTORY_DOCS_URL} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.assetInventory.emptyState.readDocumentation"
|
||||
defaultMessage="Read documentation"
|
||||
/>
|
||||
</EuiLink>
|
||||
</>
|
||||
}
|
||||
footer={<NeedHelp />}
|
||||
/>
|
||||
</CenteredWrapper>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -26,7 +26,7 @@ describe('Initializing', () => {
|
|||
it('should navigate to the integrations page when clicking "Add integration" button', async () => {
|
||||
renderWithTestProvider(<Initializing />);
|
||||
|
||||
expect(screen.getByText(/initializing asset inventory/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: /discovering your assets/i })).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /add integration/i }));
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ export const Initializing = () => {
|
|||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.assetInventory.onboarding.initializing.title"
|
||||
defaultMessage="Initializing Asset Inventory"
|
||||
defaultMessage="Discovering Your Assets"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ export const Initializing = () => {
|
|||
body={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.assetInventory.onboarding.initializing.description"
|
||||
defaultMessage="Your Asset Inventory is being set up. This may take a few moments as we prepare to provide you with centralized visibility into your assets. Check back shortly to start exploring your assets."
|
||||
defaultMessage="We're currently analyzing your connected data sources to build a comprehensive inventory of your assets. This typically takes just a few minutes to complete. You'll be automatically redirected when your inventory is ready to explore."
|
||||
/>
|
||||
}
|
||||
footer={
|
||||
|
@ -63,7 +63,7 @@ export const Initializing = () => {
|
|||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.assetInventory.initializing.exploreTitle"
|
||||
defaultMessage="Explore Asset Integrations"
|
||||
defaultMessage="Explore Asset Discovery Integrations"
|
||||
/>
|
||||
</strong>
|
||||
</EuiTitle>
|
||||
|
@ -72,7 +72,7 @@ export const Initializing = () => {
|
|||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.assetInventory.initializing.exploreDescription"
|
||||
defaultMessage="Explore the out-of-the-box integrations we provide to connect your data sources."
|
||||
defaultMessage="Discover assets across cloud, identity, and other environments for deeper visibility."
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { screen } from '@testing-library/react';
|
||||
import { NeedHelp } from './need_help';
|
||||
import { renderWithTestProvider } from '../../test/test_provider';
|
||||
|
||||
describe('NeedHelp Component', () => {
|
||||
it('renders the component with correct title and link', () => {
|
||||
renderWithTestProvider(<NeedHelp />);
|
||||
|
||||
expect(screen.getByText(/need help?/i)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(/read documentation/i).closest('a')).toHaveAttribute(
|
||||
'href',
|
||||
'https://ela.st/asset-inventory'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { EuiLink, EuiTitle } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { DOCS_URL } from '../../constants';
|
||||
|
||||
export const NeedHelp = () => {
|
||||
return (
|
||||
<>
|
||||
<EuiTitle size="xxs">
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.assetInventory.emptyState.needHelp"
|
||||
defaultMessage="Need help?"
|
||||
/>
|
||||
</strong>
|
||||
</EuiTitle>{' '}
|
||||
<EuiLink href={DOCS_URL} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.assetInventory.emptyState.readDocumentation"
|
||||
defaultMessage="Read documentation"
|
||||
/>
|
||||
</EuiLink>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -8,18 +8,13 @@ import React from 'react';
|
|||
import { screen } from '@testing-library/react';
|
||||
import { useSpaceId } from '../../../common/hooks/use_space_id';
|
||||
import { NoDataFound } from './no_data_found';
|
||||
import { TEST_SUBJ_ONBOARDING_SUCCESS_CALLOUT } from '../../constants';
|
||||
import { renderWithTestProvider } from '../../test/test_provider';
|
||||
|
||||
// Mocking components which implementation details are out of scope for this unit test
|
||||
jest.mock('../../../onboarding/components/onboarding_context', () => ({
|
||||
OnboardingContextProvider: () => <div data-test-subj="onboarding-grid" />,
|
||||
}));
|
||||
jest.mock('./onboarding_success_callout', () => ({
|
||||
OnboardingSuccessCallout: () => (
|
||||
<div data-test-subj="asset-inventory-onboarding-success-callout" />
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../../../common/hooks/use_space_id');
|
||||
|
||||
describe('NoDataFound Component', () => {
|
||||
|
@ -41,9 +36,9 @@ describe('NoDataFound Component', () => {
|
|||
it('should render the No Data Found related content when spaceId is available', () => {
|
||||
renderWithTestProvider(<NoDataFound />);
|
||||
|
||||
expect(screen.getByText(/start onboarding your assets/i)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId(TEST_SUBJ_ONBOARDING_SUCCESS_CALLOUT)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('heading', { name: /connect sources to discover assets/i })
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId('onboarding-grid')).toBeInTheDocument();
|
||||
});
|
||||
|
|
|
@ -22,7 +22,6 @@ import { AssetInventoryTitle } from '../asset_inventory_title';
|
|||
import { AssetInventoryLoading } from '../asset_inventory_loading';
|
||||
import illustration from '../../../common/images/integrations_light.png';
|
||||
import { IntegrationsCardGridTabs } from '../../../onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs';
|
||||
import { OnboardingSuccessCallout } from './onboarding_success_callout';
|
||||
import { TEST_SUBJ_ONBOARDING_NO_DATA_FOUND } from '../../constants';
|
||||
|
||||
export const NoDataFound = () => {
|
||||
|
@ -36,7 +35,6 @@ export const NoDataFound = () => {
|
|||
<>
|
||||
<AssetInventoryTitle />
|
||||
<EuiSpacer size="l" />
|
||||
<OnboardingSuccessCallout />
|
||||
<EuiPanel data-test-subj={TEST_SUBJ_ONBOARDING_NO_DATA_FOUND}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
|
@ -44,7 +42,7 @@ export const NoDataFound = () => {
|
|||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.onboarding.startOnboardingAssets"
|
||||
defaultMessage="Start onboarding your assets"
|
||||
defaultMessage="Connect Sources to Discover Assets"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 { PermissionDenied } from './permission_denied';
|
||||
import { renderWithTestProvider } from '../../test/test_provider';
|
||||
import { screen } from '@testing-library/dom';
|
||||
import { TEST_SUBJ_ONBOARDING_PERMISSION_DENIED } from '../../constants';
|
||||
import type { AssetInventoryStatusResponse } from '../../../../common/api/asset_inventory/types';
|
||||
|
||||
describe('PermissionDenied Component', () => {
|
||||
it('should render the component correctly', () => {
|
||||
renderWithTestProvider(<PermissionDenied />);
|
||||
|
||||
expect(screen.getByTestId(TEST_SUBJ_ONBOARDING_PERMISSION_DENIED)).toBeInTheDocument();
|
||||
expect(screen.getByText(/permission denied/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/need help?/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the insufficient permissions callout when the privileges are passed', () => {
|
||||
const privileges: AssetInventoryStatusResponse['privileges'] = {
|
||||
has_all_required: false,
|
||||
has_read_permissions: true,
|
||||
has_write_permissions: false,
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
index: { 'logs-*': { read: false, view_index_metadata: false } },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithTestProvider(<PermissionDenied privileges={privileges} />);
|
||||
|
||||
expect(screen.getByText(/missing elasticsearch index privileges/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 { EuiImage, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { AssetInventoryStatusResponse } from '../../../../common/api/asset_inventory/types';
|
||||
import { MissingPrivilegesCallout } from '../../../entity_analytics/components/entity_store/components/missing_privileges_callout';
|
||||
import illustration from '../../../common/images/lock_light.png';
|
||||
import { CenteredWrapper } from './centered_wrapper';
|
||||
import { EmptyStateIllustrationContainer } from '../empty_state_illustration_container';
|
||||
import { TEST_SUBJ_ONBOARDING_PERMISSION_DENIED } from '../../constants';
|
||||
import { NeedHelp } from './need_help';
|
||||
import { AssetInventoryTitle } from '../asset_inventory_title';
|
||||
|
||||
interface PermissionDeniedProps {
|
||||
privileges?: AssetInventoryStatusResponse['privileges'];
|
||||
}
|
||||
|
||||
export const PermissionDenied = ({ privileges }: PermissionDeniedProps) => {
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<AssetInventoryTitle />
|
||||
<CenteredWrapper>
|
||||
<EuiEmptyPrompt
|
||||
data-test-subj={TEST_SUBJ_ONBOARDING_PERMISSION_DENIED}
|
||||
icon={
|
||||
<EmptyStateIllustrationContainer>
|
||||
<EuiImage
|
||||
url={illustration}
|
||||
size="fullWidth"
|
||||
alt={i18n.translate(
|
||||
'xpack.securitySolution.assetInventory.permissionDenied.illustrationAlt',
|
||||
{
|
||||
defaultMessage: 'Permission Denied',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EmptyStateIllustrationContainer>
|
||||
}
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.assetInventory.onboarding.permissionDenied.title"
|
||||
defaultMessage="Permission Denied"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
layout="horizontal"
|
||||
color="plain"
|
||||
body={
|
||||
<>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.assetInventory.onboarding.permissionDenied.description"
|
||||
defaultMessage="You do not have the necessary permissions to enable or view the Asset Inventory. To access this feature, please contact your administrator to request the appropriate permissions."
|
||||
/>
|
||||
</p>
|
||||
{privileges ? <MissingPrivilegesCallout privileges={privileges} /> : null}
|
||||
</>
|
||||
}
|
||||
footer={<NeedHelp />}
|
||||
/>
|
||||
</CenteredWrapper>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -31,6 +31,8 @@ export const TEST_SUBJ_ONBOARDING_GET_STARTED = 'asset-inventory-onboarding-get-
|
|||
export const TEST_SUBJ_ONBOARDING_INITIALIZING = 'asset-inventory-onboarding-initializing';
|
||||
export const TEST_SUBJ_ONBOARDING_NO_DATA_FOUND = 'asset-inventory-onboarding-no-data-found';
|
||||
export const TEST_SUBJ_ONBOARDING_SUCCESS_CALLOUT = 'asset-inventory-onboarding-success-callout';
|
||||
export const TEST_SUBJ_ONBOARDING_PERMISSION_DENIED =
|
||||
'asset-inventory-onboarding-permission-denied';
|
||||
export const TEST_SUBJ_GROUPING = 'asset-inventory-grouping';
|
||||
export const TEST_SUBJ_GROUPING_LOADING = 'asset-inventory-grouping-loading';
|
||||
export const TEST_SUBJ_GROUPING_COUNTER = 'asset-inventory-grouping-counter';
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 63 KiB |
|
@ -38,7 +38,7 @@ describe('AssetInventoryDataClient', () => {
|
|||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns INSUFFICIENT_PRIVILEGES when user lacks required privileges', async () => {
|
||||
it('returns INSUFFICIENT_PRIVILEGES with missing privileges when user lacks required privileges', async () => {
|
||||
const noPrivileges = {
|
||||
...mockEntityStorePrivileges,
|
||||
has_all_required: false,
|
||||
|
@ -48,7 +48,7 @@ describe('AssetInventoryDataClient', () => {
|
|||
|
||||
expect(result).toEqual({
|
||||
status: 'insufficient_privileges',
|
||||
privileges: noPrivileges.privileges,
|
||||
privileges: noPrivileges,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -111,7 +111,7 @@ export class AssetInventoryDataClient {
|
|||
if (!entityStorePrivileges.has_all_required) {
|
||||
return {
|
||||
status: ASSET_INVENTORY_STATUS.INSUFFICIENT_PRIVILEGES,
|
||||
privileges: entityStorePrivileges.privileges,
|
||||
privileges: entityStorePrivileges,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue