Skip to content

Commit bdc4790

Browse files
Asset Inventory Onboarding and Context Integration (#212315)
### Summary It closes #210713 This PR introduces the **onboarding flow for Asset Inventory**, ensuring users are guided through an enablement process when accessing the Asset Inventory page for the first time. #### Changes: **Asset Inventory API Forwarding** - The Asset Inventory API now proxies enablement requests to the **Entity Store API** (`/api/entity_store/engines/enable`). - This ensures that any future **enhancements for Asset Inventory enablement** are already handled on the server side. **Asset Inventory Context** - Introduced the `AssetInventoryContext` to centralize **Asset Inventory status management** based on the `/api/entity_store/engines/status` data (`disabled`, `initializing`, `ready`, etc.). - Allows any component to **consume the onboarding state** and react accordingly. **"Get Started" Onboarding Experience** - Implemented a **new onboarding screen** that appears when Asset Inventory is disabled. - Includes: - Informative **title and description** about the feature. - A **call-to-action button** to enable Asset Inventory. - **Loading states and error handling** for the API call. **API Integration and Hooks** - Created `useEnableAssetInventory` to abstract and handle enablement logic via **React Query**. - Created `useAssetInventoryRoutes` to abstract API calls for fetching and enabling Asset Inventory. **HoverForExplanation Component** - Introduced `HoverForExplanation`, a **tooltip-based helper component** that enhances the onboarding description. - Provides **inline explanations** for key terms like **identity providers, cloud services, MDMs, and databases**, ensuring users understand **data sources** in Asset Inventory. **Testing & Error Handling** - Added **unit tests** for the onboarding component and hooks. - Implemented error handling for failed API requests (e.g., permission errors, server failures). #### Screenshots ![image](https://github.com/user-attachments/assets/b2e08497-6ca1-47bd-8627-f32b7c3172f3) https://github.com/user-attachments/assets/1280404e-9cb3-4288-91a7-640f8f1b458a #### How to test it locally - Ensure the `assetInventoryUXEnabled` feature flag is enabled on kibana.yml file: ``` xpack.securitySolution.enableExperimental: ['assetInventoryUXEnabled'] ``` - Ensure the Entity Store is Off and data is removed (initial state), so the onboarding is visible (If the Entity Store is installed by other means the onboarding will direct users to the empty state component or to the all assets page) --------- Co-authored-by: kibanamachine <[email protected]>
1 parent f8ba372 commit bdc4790

25 files changed

+883
-24
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
export const ASSET_INVENTORY_ENABLE_API_PATH = '/api/asset_inventory/enable';
9+
export const ASSET_INVENTORY_STATUS_API_PATH = '/api/asset_inventory/status';
10+
export const ASSET_INVENTORY_DELETE_API_PATH = '/api/asset_inventory/delete';
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import type { ServerApiError } from '../../../public/common/types';
9+
import type { EntityAnalyticsPrivileges } from '../entity_analytics';
10+
import type { InitEntityStoreResponse } from '../entity_analytics/entity_store/enable.gen';
11+
12+
export type AssetInventoryStatus =
13+
| 'disabled'
14+
| 'initializing'
15+
| 'empty'
16+
| 'permission_denied'
17+
| 'ready';
18+
19+
export interface AssetInventoryStatusResponse {
20+
status: AssetInventoryStatus;
21+
privileges?: EntityAnalyticsPrivileges['privileges'];
22+
}
23+
24+
export type AssetInventoryEnableResponse = InitEntityStoreResponse;
25+
26+
export interface AssetInventoryServerApiError {
27+
body: ServerApiError;
28+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
import React from 'react';
8+
import { EuiFlexGroup, EuiFlexItem, EuiLoadingLogo, EuiSpacer } from '@elastic/eui';
9+
import { InventoryTitle } from './inventory_title';
10+
import { CenteredWrapper } from './onboarding/centered_wrapper';
11+
12+
/**
13+
* A loading state for the asset inventory page.
14+
*/
15+
export const AssetInventoryLoading = () => (
16+
<EuiFlexGroup>
17+
<EuiFlexItem>
18+
<InventoryTitle />
19+
<EuiSpacer size="l" />
20+
<CenteredWrapper>
21+
<EuiLoadingLogo logo="logoSecurity" size="xl" />
22+
</CenteredWrapper>
23+
</EuiFlexItem>
24+
</EuiFlexGroup>
25+
);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { css } from '@emotion/react';
9+
import React from 'react';
10+
11+
// width of the illustration used in the empty states
12+
const DEFAULT_ILLUSTRATION_WIDTH = 360;
13+
14+
/**
15+
* A container component that maintains a fixed width for SVG elements,
16+
* this prevents the EmptyState component from flickering while the SVGs are loading.
17+
*/
18+
export const EmptyStateIllustrationContainer: React.FC<{ children: React.ReactNode }> = ({
19+
children,
20+
}) => (
21+
<div
22+
css={css`
23+
width: ${DEFAULT_ILLUSTRATION_WIDTH}px;
24+
`}
25+
>
26+
{children}
27+
</div>
28+
);
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
import React from 'react';
8+
import { EuiTitle } from '@elastic/eui';
9+
import { FormattedMessage } from '@kbn/i18n-react';
10+
11+
export const InventoryTitle = () => {
12+
return (
13+
<EuiTitle size="l" data-test-subj="inventory-title">
14+
<h1>
15+
<FormattedMessage
16+
id="xpack.securitySolution.assetInventory.inventoryTitle"
17+
defaultMessage="Inventory"
18+
/>
19+
</h1>
20+
</EuiTitle>
21+
);
22+
};
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React from 'react';
9+
import type { FC, PropsWithChildren } from 'react';
10+
import { GetStarted } from './get_started';
11+
import { AssetInventoryLoading } from '../asset_inventory_loading';
12+
import { useAssetInventoryStatus } from '../../hooks/use_asset_inventory_status';
13+
14+
/**
15+
* This component serves as a wrapper to render appropriate onboarding screens
16+
* based on the current onboarding status. If no specific onboarding status
17+
* matches, it will render the child components.
18+
*/
19+
export const AssetInventoryOnboarding: FC<PropsWithChildren> = ({ children }) => {
20+
const { data, isLoading } = useAssetInventoryStatus();
21+
22+
if (isLoading || !data) {
23+
return <AssetInventoryLoading />;
24+
}
25+
26+
const { status, privileges } = data;
27+
28+
// Render different screens based on the onboarding status.
29+
switch (status) {
30+
case 'disabled': // The user has not yet started the onboarding process.
31+
return <GetStarted />;
32+
case 'initializing': // Todo: The onboarding process is currently initializing.
33+
return <div>{'Initializing...'}</div>;
34+
case 'empty': // Todo: Onboarding cannot proceed because no relevant data was found.
35+
return <div>{'No data found.'}</div>;
36+
case 'permission_denied': // Todo: User lacks the necessary permissions to proceed.
37+
return (
38+
<div>
39+
{'Permission denied.'}
40+
<pre>{JSON.stringify(privileges)}</pre>
41+
</div>
42+
);
43+
default:
44+
// If no onboarding status matches, render the child components.
45+
return children;
46+
}
47+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
import { EuiFlexGroup, EuiFlexItem, type CommonProps } from '@elastic/eui';
8+
import { css } from '@emotion/react';
9+
import React from 'react';
10+
11+
/**
12+
* A wrapper that centers its children both horizontally and vertically.
13+
*/
14+
export const CenteredWrapper = ({
15+
children,
16+
...rest
17+
}: { children: React.ReactNode } & CommonProps) => (
18+
<EuiFlexGroup
19+
css={css`
20+
// 250px is roughly the Kibana chrome with a page title and tabs
21+
min-height: calc(100vh - 250px);
22+
`}
23+
justifyContent="center"
24+
alignItems="center"
25+
direction="column"
26+
{...rest}
27+
>
28+
<EuiFlexItem>{children}</EuiFlexItem>
29+
</EuiFlexGroup>
30+
);
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React from 'react';
9+
import { render, screen, waitFor } from '@testing-library/react';
10+
import { GetStarted } from './get_started';
11+
import { useEnableAssetInventory } from './hooks/use_enable_asset_inventory';
12+
import { TestProvider } from '../../test/test_provider';
13+
import { userEvent } from '@testing-library/user-event';
14+
15+
jest.mock('./hooks/use_enable_asset_inventory', () => ({
16+
useEnableAssetInventory: jest.fn(),
17+
}));
18+
19+
const mockGetStarted = {
20+
isEnabling: false,
21+
error: null,
22+
reset: jest.fn(),
23+
enableAssetInventory: jest.fn(),
24+
};
25+
26+
const renderWithProvider = (children: React.ReactNode) => {
27+
return render(<TestProvider>{children}</TestProvider>);
28+
};
29+
30+
describe('GetStarted Component', () => {
31+
beforeEach(() => {
32+
jest.resetAllMocks();
33+
(useEnableAssetInventory as jest.Mock).mockReturnValue(mockGetStarted);
34+
});
35+
36+
it('renders the component', () => {
37+
renderWithProvider(<GetStarted />);
38+
39+
expect(screen.getByText(/get started with asset inventory/i)).toBeInTheDocument();
40+
expect(screen.getByRole('button', { name: /enable asset inventory/i })).toBeInTheDocument();
41+
42+
expect(screen.getByText(/read documentation/i).closest('a')).toHaveAttribute(
43+
'href',
44+
'https://ela.st/asset-inventory'
45+
);
46+
});
47+
48+
it('calls enableAssetInventory when enable asset inventory button is clicked', async () => {
49+
renderWithProvider(<GetStarted />);
50+
51+
await userEvent.click(screen.getByRole('button', { name: /enable asset inventory/i }));
52+
53+
expect(mockGetStarted.enableAssetInventory).toHaveBeenCalled();
54+
});
55+
56+
it('shows a loading spinner when enabling', () => {
57+
(useEnableAssetInventory as jest.Mock).mockReturnValue({
58+
...mockGetStarted,
59+
isEnabling: true,
60+
});
61+
62+
renderWithProvider(<GetStarted />);
63+
64+
expect(screen.getByRole('progressbar')).toBeInTheDocument();
65+
expect(screen.getByRole('button', { name: /enabling asset inventory/i })).toBeInTheDocument();
66+
});
67+
68+
it('displays an error message when there is an error', () => {
69+
const errorMessage = 'Task Manager is not available';
70+
(useEnableAssetInventory as jest.Mock).mockReturnValue({
71+
...mockGetStarted,
72+
error: errorMessage,
73+
});
74+
75+
renderWithProvider(<GetStarted />);
76+
77+
expect(screen.getByText(/sorry, there was an error/i)).toBeInTheDocument();
78+
expect(screen.getByText(errorMessage)).toBeInTheDocument();
79+
});
80+
81+
it('calls reset when error message is dismissed', async () => {
82+
(useEnableAssetInventory as jest.Mock).mockReturnValue({
83+
...mockGetStarted,
84+
error: 'Task Manager is not available',
85+
});
86+
87+
renderWithProvider(<GetStarted />);
88+
89+
await userEvent.click(screen.getByRole('button', { name: /dismiss/i }));
90+
91+
await waitFor(() => expect(mockGetStarted.reset).toHaveBeenCalled());
92+
});
93+
});

0 commit comments

Comments
 (0)