Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Endpoint] add policy empty state #69449

Merged
merged 12 commits into from
Jun 19, 2020
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
54 changes: 28 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 @@ -18,6 +18,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const policyTestResources = getService('policyTestResources');
const RELATIVE_DATE_FORMAT = /\d (?:seconds|minutes) ago/i;
const retry = getService('retry');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you forget to delete this? (you might get a ESLint error

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, I sure did


describe('When on the Endpoint Policy List', function () {
this.tags(['ciGroup7']);
Expand All @@ -36,29 +37,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 +56,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 +101,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 +142,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');
},
};
}