diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx index a2901ab6bfe5c..63a5cd0d67ae6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx @@ -23,12 +23,18 @@ describe('when on the policies page', () => { render = () => mockedContext.render(); }); - it('should show a table', async () => { + it('should show the empty state', async () => { const renderResult = render(); - const table = await renderResult.findByTestId('policyTable'); + const table = await renderResult.findByTestId('emptyPolicyTable'); expect(table).not.toBeNull(); }); + it('should display the onboarding steps', async () => { + const renderResult = render(); + const onboardingSteps = await renderResult.findByTestId('onboardingSteps'); + expect(onboardingSteps).not.toBeNull(); + }); + describe('when list data loads', () => { let firstPolicyID: string; beforeEach(() => { @@ -50,11 +56,13 @@ describe('when on the policies page', () => { }); }); }); + it('should display rows in the table', async () => { const renderResult = render(); const rows = await renderResult.findAllByRole('row'); expect(rows).toHaveLength(4); }); + it('should display policy name value as a link', async () => { const renderResult = render(); const policyNameLink = (await renderResult.findAllByTestId('policyNameLink'))[0]; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx index 090a16a763664..2d4abd6de0e42 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useEffect, useMemo, CSSProperties, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, CSSProperties, useState, MouseEvent } from 'react'; import { EuiBasicTable, EuiText, @@ -22,6 +22,9 @@ import { EuiCallOut, EuiSpacer, EuiButton, + EuiSteps, + EuiTitle, + EuiProgress, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -61,6 +64,10 @@ const NO_WRAP_TRUNCATE_STYLE: CSSProperties = Object.freeze({ whiteSpace: 'nowrap', }); +const TEXT_ALIGN_CENTER: CSSProperties = Object.freeze({ + textAlign: 'center', +}); + const DangerEuiContextMenuItem = styled(EuiContextMenuItem)` color: ${(props) => props.theme.eui.textColors.danger}; `; @@ -410,24 +417,50 @@ export const PolicyList = React.memo(() => { } bodyHeader={ - - - + policyItems && + policyItems.length > 0 && ( + + + + ) } > - [...policyItems], [policyItems])} - columns={columns} - loading={loading} - pagination={paginationSetup} - onChange={handleTableChange} - data-test-subj="policyTable" - hasActions={false} - /> + {useMemo(() => { + return ( + <> + {policyItems && policyItems.length > 0 ? ( + + ) : ( + + )} + + ); + }, [ + policyItems, + loading, + isFetchingPackageInfo, + columns, + handleCreatePolicyClick, + handleTableChange, + paginationSetup, + ])} @@ -436,6 +469,107 @@ export const PolicyList = React.memo(() => { PolicyList.displayName = 'PolicyList'; +const EmptyPolicyTable = React.memo<{ + loading: boolean; + onActionClick: (event: MouseEvent) => void; + actionDisabled: boolean; + dataTestSubj: string; +}>(({ loading, onActionClick, actionDisabled, dataTestSubj }) => { + const policySteps = useMemo( + () => [ + { + title: i18n.translate('xpack.securitySolution.endpoint.policyList.stepOneTitle', { + defaultMessage: 'Head over to Ingest Manager.', + }), + children: ( + + + + ), + }, + { + title: i18n.translate('xpack.securitySolution.endpoint.policyList.stepTwoTitle', { + defaultMessage: 'We’ll create a recommended security policy for you.', + }), + children: ( + + + + ), + }, + { + title: i18n.translate('xpack.securitySolution.endpoint.policyList.stepThreeTitle', { + defaultMessage: 'Enroll your agents through Fleet.', + }), + children: ( + + + + ), + }, + ], + [] + ); + return ( +
+ {loading ? ( + + ) : ( + <> + + +

+ +

+
+ + + + + + + + + + + + + + + + + + + )} +
+ ); +}); + +EmptyPolicyTable.displayName = 'EmptyPolicyTable'; + const ConfirmDelete = React.memo<{ hostCount: number; isDeleting: boolean; diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts index 1f5b6889f8fed..941a100416740 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts @@ -36,29 +36,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const createButtonTitle = await testSubjects.getVisibleText('headerCreateNewPolicyButton'); expect(createButtonTitle).to.equal('Create new policy'); }); - it('shows policy count total', async () => { - const policyTotal = await testSubjects.getVisibleText('policyTotalCount'); - expect(policyTotal).to.equal('0 Policies'); - }); - it('has correct table headers', async () => { - const allHeaderCells = await pageObjects.endpointPageUtils.tableHeaderVisibleText( - 'policyTable' - ); - expect(allHeaderCells).to.eql([ - 'Policy Name', - 'Created By', - 'Created Date', - 'Last Updated By', - 'Last Updated', - 'Version', - 'Actions', - ]); - }); - it('should show empty table results message', async () => { - const [, [noItemsFoundMessage]] = await pageObjects.endpointPageUtils.tableData( - 'policyTable' - ); - expect(noItemsFoundMessage).to.equal('No items found'); + it('shows empty state', async () => { + await testSubjects.existOrFail('emptyPolicyTable'); }); describe('and policies exists', () => { @@ -76,6 +55,21 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { } }); + it('has correct table headers', async () => { + const allHeaderCells = await pageObjects.endpointPageUtils.tableHeaderVisibleText( + 'policyTable' + ); + expect(allHeaderCells).to.eql([ + 'Policy Name', + 'Created By', + 'Created Date', + 'Last Updated By', + 'Last Updated', + 'Version', + 'Actions', + ]); + }); + it('should show policy on the list', async () => { const [, policyRow] = await pageObjects.endpointPageUtils.tableData('policyTable'); // Validate row data with the exception of the Date columns - since those are initially @@ -106,9 +100,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await pageObjects.policy.launchAndFindDeleteModal(); await testSubjects.existOrFail('policyListDeleteModal'); await pageObjects.common.clickConfirmOnModal(); - await pageObjects.endpoint.waitForTableToNotHaveData('policyTable'); - const policyTotal = await testSubjects.getVisibleText('policyTotalCount'); - expect(policyTotal).to.equal('0 Policies'); + const emptyPolicyTable = await testSubjects.find('emptyPolicyTable'); + expect(emptyPolicyTable).not.to.be(null); }); }); @@ -148,5 +141,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await policyTestResources.deletePolicyByName(newPolicyName); }); }); + + describe('and user clicks on page header create button', () => { + it('should direct users to the ingest management integrations add datasource', async () => { + await pageObjects.policy.navigateToPolicyList(); + await (await pageObjects.policy.findOnboardingStartButton()).click(); + await pageObjects.ingestManagerCreateDatasource.ensureOnCreatePageOrFail(); + }); + }); }); } diff --git a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts index a2b0f9a671039..3ffd7eb032c22 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts @@ -101,5 +101,13 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr async findDatasourceEndpointCustomConfiguration(onEditPage: boolean = false) { return await testSubjects.find(`endpointDatasourceConfig_${onEditPage ? 'edit' : 'create'}`); }, + + /** + * Finds and returns the onboarding button displayed in empty List pages + */ + async findOnboardingStartButton() { + await testSubjects.waitForEnabled('onboardingStartButton'); + return await testSubjects.find('onboardingStartButton'); + }, }; }