Skip to content

Commit

Permalink
[Endpoint] add policy empty state (#69449)
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinlog authored Jun 19, 2020
1 parent 84f8b43 commit af7e2d2
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,18 @@ describe('when on the policies page', () => {
render = () => mockedContext.render(<PolicyList />);
});

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(() => {
Expand All @@ -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];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';
Expand Down Expand Up @@ -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};
`;
Expand Down Expand Up @@ -410,24 +417,50 @@ export const PolicyList = React.memo(() => {
</EuiButton>
}
bodyHeader={
<EuiText color="subdued" data-test-subj="policyTotalCount">
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.viewTitleTotalCount"
defaultMessage="{totalItemCount, plural, one {# Policy} other {# Policies}}"
values={{ totalItemCount }}
/>
</EuiText>
policyItems &&
policyItems.length > 0 && (
<EuiText color="subdued" data-test-subj="policyTotalCount">
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.viewTitleTotalCount"
defaultMessage="{totalItemCount, plural, one {# Policy} other {# Policies}}"
values={{ totalItemCount }}
/>
</EuiText>
)
}
>
<EuiBasicTable
items={useMemo(() => [...policyItems], [policyItems])}
columns={columns}
loading={loading}
pagination={paginationSetup}
onChange={handleTableChange}
data-test-subj="policyTable"
hasActions={false}
/>
{useMemo(() => {
return (
<>
{policyItems && policyItems.length > 0 ? (
<EuiBasicTable
items={[...policyItems]}
columns={columns}
loading={loading}
pagination={paginationSetup}
onChange={handleTableChange}
data-test-subj="policyTable"
hasActions={false}
/>
) : (
<EmptyPolicyTable
loading={loading}
onActionClick={handleCreatePolicyClick}
actionDisabled={isFetchingPackageInfo}
dataTestSubj="emptyPolicyTable"
/>
)}
</>
);
}, [
policyItems,
loading,
isFetchingPackageInfo,
columns,
handleCreatePolicyClick,
handleTableChange,
paginationSetup,
])}
<SpyRoute />
</ManagementPageView>
</>
Expand All @@ -436,6 +469,107 @@ export const PolicyList = React.memo(() => {

PolicyList.displayName = 'PolicyList';

const EmptyPolicyTable = React.memo<{
loading: boolean;
onActionClick: (event: MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => 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: (
<EuiText color="subdued" size="xs">
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.stepOne"
defaultMessage="Here, you’ll add the Elastic Endpoint Security Integration to your Agent Configuration."
/>
</EuiText>
),
},
{
title: i18n.translate('xpack.securitySolution.endpoint.policyList.stepTwoTitle', {
defaultMessage: 'We’ll create a recommended security policy for you.',
}),
children: (
<EuiText color="subdued" size="xs">
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.stepTwo"
defaultMessage="You can edit this policy in the “Policies” tab after you’ve added the Elastic Endpoint integration."
/>
</EuiText>
),
},
{
title: i18n.translate('xpack.securitySolution.endpoint.policyList.stepThreeTitle', {
defaultMessage: 'Enroll your agents through Fleet.',
}),
children: (
<EuiText color="subdued" size="xs">
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.stepThree"
defaultMessage="If you haven’t already, enroll your agents through Fleet using the same agent configuration."
/>
</EuiText>
),
},
],
[]
);
return (
<div data-test-subj={dataTestSubj}>
{loading ? (
<EuiProgress size="xs" color="accent" className="essentialAnimation" />
) : (
<>
<EuiSpacer size="xxl" />
<EuiTitle size="m">
<h2 style={TEXT_ALIGN_CENTER}>
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.noPoliciesPrompt"
defaultMessage="Looks like you're not using Elastic Endpoint"
/>
</h2>
</EuiTitle>
<EuiSpacer size="xxl" />
<EuiText textAlign="center" color="subdued" size="s">
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.noPoliciesInstructions"
defaultMessage="Elastic Endpoint Security gives you the power to keep your endpoints safe from attack, as well as unparalleled visibility into any threat in your environment."
/>
</EuiText>
<EuiSpacer size="xxl" />
<EuiFlexGroup alignItems="center" justifyContent="center">
<EuiFlexItem grow={false}>
<EuiSteps steps={policySteps} data-test-subj={'onboardingSteps'} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup alignItems="center" justifyContent="center">
<EuiFlexItem grow={false}>
<EuiButton
fill
onClick={onActionClick}
isDisabled={actionDisabled}
data-test-subj="onboardingStartButton"
>
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.emptyCreateNewButton"
defaultMessage="Click here to get started"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>
)}
</div>
);
});

EmptyPolicyTable.displayName = 'EmptyPolicyTable';

const ConfirmDelete = React.memo<{
hostCount: number;
isDeleting: boolean;
Expand Down
53 changes: 27 additions & 26 deletions x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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
Expand Down Expand Up @@ -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);
});
});

Expand Down Expand Up @@ -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();
});
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -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');
},
};
}

0 comments on commit af7e2d2

Please sign in to comment.