[SECURITY_SOLUTION][ENDPOINT] Trusted Apps List page Empty State when no trusted apps exist (#87252)

* Show loading spinner while trying to determine if entries exist
* Handle display of the 3 conditions: loading, entries exist, no entries
This commit is contained in:
Paul Tavares 2021-01-07 11:22:23 -05:00 committed by GitHub
parent b203eaf370
commit 14df31b6a0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 465 additions and 44 deletions

View file

@ -32,6 +32,10 @@ export interface TrustedAppsListPageLocation {
}
export interface TrustedAppsListPageState {
/** Represents if trusted apps entries exist, regardless of whether the list is showing results
* or not (which could use filtering in the future)
*/
entriesExist: AsyncResourceState<boolean>;
listView: {
listResourceState: AsyncResourceState<TrustedAppsListData>;
freshDataTimestamp: number;

View file

@ -54,6 +54,10 @@ export type TrustedAppCreationDialogConfirmed = Action<'trustedAppCreationDialog
export type TrustedAppCreationDialogClosed = Action<'trustedAppCreationDialogClosed'>;
export type TrustedAppsExistResponse = Action<'trustedAppsExistStateChanged'> & {
payload: AsyncResourceState<boolean>;
};
export type TrustedAppsPageAction =
| TrustedAppsListDataOutdated
| TrustedAppsListResourceStateChanged
@ -65,4 +69,5 @@ export type TrustedAppsPageAction =
| TrustedAppCreationDialogStarted
| TrustedAppCreationDialogFormStateUpdated
| TrustedAppCreationDialogConfirmed
| TrustedAppsExistResponse
| TrustedAppCreationDialogClosed;

View file

@ -40,6 +40,7 @@ export const initialCreationDialogState = (): TrustedAppsListPageState['creation
});
export const initialTrustedAppsPageState = (): TrustedAppsListPageState => ({
entriesExist: { type: 'UninitialisedResourceState' },
listView: {
listResourceState: { type: 'UninitialisedResourceState' },
freshDataTimestamp: Date.now(),

View file

@ -66,6 +66,26 @@ const createStoreSetup = (trustedAppsService: TrustedAppsService) => {
};
describe('middleware', () => {
type TrustedAppsEntriesExistState = Pick<TrustedAppsListPageState, 'entriesExist'>;
const entriesExistLoadedState = (): TrustedAppsEntriesExistState => {
return {
entriesExist: {
data: true,
type: 'LoadedResourceState',
},
};
};
const entriesExistLoadingState = (): TrustedAppsEntriesExistState => {
return {
entriesExist: {
previousState: {
type: 'UninitialisedResourceState',
},
type: 'LoadingResourceState',
},
};
};
beforeEach(() => {
dateNowMock.mockReturnValue(initialNow);
});
@ -106,6 +126,7 @@ describe('middleware', () => {
expect(store.getState()).toStrictEqual({
...initialState,
...entriesExistLoadingState(),
listView: createLoadedListViewWithPagination(initialNow, pagination),
active: true,
location,
@ -126,9 +147,10 @@ describe('middleware', () => {
store.dispatch(createUserChangedUrlAction('/trusted_apps', '?page_index=2&page_size=50'));
expect(service.getTrustedAppsList).toBeCalledTimes(1);
expect(service.getTrustedAppsList).toBeCalledTimes(2);
expect(store.getState()).toStrictEqual({
...initialState,
...entriesExistLoadingState(),
listView: createLoadedListViewWithPagination(initialNow, pagination),
active: true,
location,
@ -154,6 +176,7 @@ describe('middleware', () => {
expect(store.getState()).toStrictEqual({
...initialState,
...entriesExistLoadingState(),
listView: {
listResourceState: {
type: 'LoadingResourceState',
@ -169,6 +192,7 @@ describe('middleware', () => {
expect(store.getState()).toStrictEqual({
...initialState,
...entriesExistLoadedState(),
listView: createLoadedListViewWithPagination(newNow, pagination),
active: true,
location,
@ -189,6 +213,7 @@ describe('middleware', () => {
expect(store.getState()).toStrictEqual({
...initialState,
...entriesExistLoadingState(),
listView: {
listResourceState: {
type: 'FailedResourceState',
@ -218,7 +243,13 @@ describe('middleware', () => {
const getTrustedAppsListResponse = createGetTrustedListAppsResponse(pagination);
const listView = createLoadedListViewWithPagination(initialNow, pagination);
const listViewNew = createLoadedListViewWithPagination(newNow, pagination);
const testStartState = { ...initialState, listView, active: true, location };
const testStartState = {
...initialState,
...entriesExistLoadingState(),
listView,
active: true,
location,
};
it('does not submit when entry is undefined', async () => {
const service = createTrustedAppsServiceMock();
@ -270,7 +301,11 @@ describe('middleware', () => {
await spyMiddleware.waitForAction('trustedAppDeletionSubmissionResourceStateChanged');
await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged');
expect(store.getState()).toStrictEqual({ ...testStartState, listView: listViewNew });
expect(store.getState()).toStrictEqual({
...testStartState,
...entriesExistLoadedState(),
listView: listViewNew,
});
expect(service.deleteTrustedApp).toBeCalledWith({ id: '3' });
expect(service.deleteTrustedApp).toBeCalledTimes(1);
});
@ -307,7 +342,11 @@ describe('middleware', () => {
await spyMiddleware.waitForAction('trustedAppDeletionSubmissionResourceStateChanged');
await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged');
expect(store.getState()).toStrictEqual({ ...testStartState, listView: listViewNew });
expect(store.getState()).toStrictEqual({
...testStartState,
...entriesExistLoadedState(),
listView: listViewNew,
});
expect(service.deleteTrustedApp).toBeCalledWith({ id: '3' });
expect(service.deleteTrustedApp).toBeCalledTimes(1);
});
@ -342,6 +381,7 @@ describe('middleware', () => {
expect(store.getState()).toStrictEqual({
...testStartState,
...entriesExistLoadedState(),
deletionDialog: {
entry,
confirmed: true,

View file

@ -21,6 +21,8 @@ import { TrustedAppsHttpService, TrustedAppsService } from '../service';
import {
AsyncResourceState,
getLastLoadedResourceState,
isLoadedResourceState,
isLoadingResourceState,
isStaleResourceState,
StaleResourceState,
TrustedAppsListData,
@ -47,6 +49,10 @@ import {
getCreationDialogFormEntry,
isCreationDialogLocation,
isCreationDialogFormValid,
entriesExist,
getListTotalItemsCount,
trustedAppsListPageActive,
entriesExistState,
} from './selectors';
const createTrustedAppsListResourceStateChangedAction = (
@ -217,6 +223,50 @@ const submitDeletionIfNeeded = async (
}
};
const checkTrustedAppsExistIfNeeded = async (
store: ImmutableMiddlewareAPI<TrustedAppsListPageState, AppAction>,
trustedAppsService: TrustedAppsService
) => {
const currentState = store.getState();
const currentEntriesExistState = entriesExistState(currentState);
if (
trustedAppsListPageActive(currentState) &&
!isLoadingResourceState(currentEntriesExistState)
) {
const currentListTotal = getListTotalItemsCount(currentState);
const currentDoEntriesExist = entriesExist(currentState);
if (
!isLoadedResourceState(currentEntriesExistState) ||
(currentListTotal === 0 && currentDoEntriesExist) ||
(currentListTotal > 0 && !currentDoEntriesExist)
) {
store.dispatch({
type: 'trustedAppsExistStateChanged',
payload: { type: 'LoadingResourceState', previousState: currentEntriesExistState },
});
let doTheyExist: boolean;
try {
const { total } = await trustedAppsService.getTrustedAppsList({
page: 1,
per_page: 1,
});
doTheyExist = total > 0;
} catch (e) {
// If a failure occurs, lets assume entries exits so that the UI is not blocked to the user
doTheyExist = true;
}
store.dispatch({
type: 'trustedAppsExistStateChanged',
payload: { type: 'LoadedResourceState', data: doTheyExist },
});
}
}
};
export const createTrustedAppsPageMiddleware = (
trustedAppsService: TrustedAppsService
): ImmutableMiddleware<TrustedAppsListPageState, AppAction> => {
@ -226,6 +276,7 @@ export const createTrustedAppsPageMiddleware = (
// TODO: need to think if failed state is a good condition to consider need for refresh
if (action.type === 'userChangedUrl' || action.type === 'trustedAppsListDataOutdated') {
await refreshListIfNeeded(store, trustedAppsService);
await checkTrustedAppsExistIfNeeded(store, trustedAppsService);
}
if (action.type === 'userChangedUrl') {

View file

@ -27,6 +27,7 @@ import {
TrustedAppCreationDialogFormStateUpdated,
TrustedAppCreationDialogConfirmed,
TrustedAppCreationDialogClosed,
TrustedAppsExistResponse,
} from './action';
import { TrustedAppsListPageState } from '../state';
@ -35,6 +36,7 @@ import {
initialDeletionDialogState,
initialTrustedAppsPageState,
} from './builders';
import { entriesExistState } from './selectors';
type StateReducer = ImmutableReducer<TrustedAppsListPageState, AppAction>;
type CaseReducer<T extends AppAction> = (
@ -142,6 +144,16 @@ const userChangedUrl: CaseReducer<UserChangedUrl> = (state, action) => {
}
};
const updateEntriesExists: CaseReducer<TrustedAppsExistResponse> = (state, { payload }) => {
if (entriesExistState(state) !== payload) {
return {
...state,
entriesExist: payload,
};
}
return state;
};
export const trustedAppsPageReducer: StateReducer = (
state = initialTrustedAppsPageState(),
action
@ -182,6 +194,9 @@ export const trustedAppsPageReducer: StateReducer = (
case 'userChangedUrl':
return userChangedUrl(state, action);
case 'trustedAppsExistStateChanged':
return updateEntriesExists(state, action);
}
return state;

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { createSelector } from 'reselect';
import { ServerApiError } from '../../../../common/types';
import { Immutable, NewTrustedApp, TrustedApp } from '../../../../../common/endpoint/types';
import { MANAGEMENT_PAGE_SIZE_OPTIONS } from '../../../common/constants';
@ -162,3 +163,24 @@ export const getCreationError = (
return isFailedResourceState(submissionResourceState) ? submissionResourceState.error : undefined;
};
export const entriesExistState: (
state: Immutable<TrustedAppsListPageState>
) => Immutable<TrustedAppsListPageState['entriesExist']> = (state) => state.entriesExist;
export const checkingIfEntriesExist: (
state: Immutable<TrustedAppsListPageState>
) => boolean = createSelector(entriesExistState, (doEntriesExists) => {
return !isLoadedResourceState(doEntriesExists);
});
export const entriesExist: (state: Immutable<TrustedAppsListPageState>) => boolean = createSelector(
entriesExistState,
(doEntriesExists) => {
return isLoadedResourceState(doEntriesExists) && doEntriesExists.data;
}
);
export const trustedAppsListPageActive: (state: Immutable<TrustedAppsListPageState>) => boolean = (
state
) => state.active;

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { memo } from 'react';
import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
export const EmptyState = memo<{
onAdd: () => void;
/** Should the Add button be disabled */
isAddDisabled?: boolean;
}>(({ onAdd, isAddDisabled = false }) => {
return (
<EuiEmptyPrompt
data-test-subj="trustedAppEmptyState"
iconType="plusInCircle"
title={
<h2>
<FormattedMessage
id="xpack.securitySolution.trustedapps.listEmptyState.title"
defaultMessage="Add your first trusted application"
/>
</h2>
}
body={
<FormattedMessage
id="xpack.securitySolution.trustedapps.listEmptyState.message"
defaultMessage="There are currently no trusted applications on your endpoint."
/>
}
actions={
<EuiButton
fill
isDisabled={isAddDisabled}
onClick={onAdd}
data-test-subj="trustedAppsListAddButton"
>
<FormattedMessage
id="xpack.securitySolution.trustedapps.list.addButton"
defaultMessage="Add Trusted Application"
/>
</EuiButton>
}
/>
);
});
EmptyState.displayName = 'EmptyState';

View file

@ -9,8 +9,16 @@ import { TrustedAppsPage } from './trusted_apps_page';
import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint';
import { fireEvent } from '@testing-library/dom';
import { MiddlewareActionSpyHelper } from '../../../../common/store/test_utils';
import { NewTrustedApp, PostTrustedAppCreateResponse } from '../../../../../common/endpoint/types';
import {
ConditionEntryField,
GetTrustedListAppsResponse,
NewTrustedApp,
OperatingSystem,
PostTrustedAppCreateResponse,
TrustedApp,
} from '../../../../../common/endpoint/types';
import { HttpFetchOptions } from 'kibana/public';
import { TRUSTED_APPS_LIST_API } from '../../../../../common/endpoint/constants';
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
htmlIdGenerator: () => () => 'mockId',
@ -20,11 +28,52 @@ describe('When on the Trusted Apps Page', () => {
const expectedAboutInfo =
'Add a trusted application to improve performance or alleviate conflicts with other applications running on your hosts. Trusted applications will be applied to hosts running Endpoint Security.';
let mockedContext: AppContextTestRender;
let history: AppContextTestRender['history'];
let coreStart: AppContextTestRender['coreStart'];
let waitForAction: MiddlewareActionSpyHelper['waitForAction'];
let render: () => ReturnType<AppContextTestRender['render']>;
const originalScrollTo = window.scrollTo;
const act = reactTestingLibrary.act;
const getFakeTrustedApp = (): TrustedApp => ({
id: '1111-2222-3333-4444',
name: 'one app',
os: OperatingSystem.WINDOWS,
created_at: '2021-01-04T13:55:00.561Z',
created_by: 'me',
description: 'a good one',
entries: [
{
field: ConditionEntryField.PATH,
value: 'one/two',
operator: 'included',
type: 'match',
},
],
});
const mockListApis = (http: AppContextTestRender['coreStart']['http']) => {
const currentGetHandler = http.get.getMockImplementation();
http.get.mockImplementation(async (...args) => {
const path = (args[0] as unknown) as string;
// @ts-ignore
const httpOptions = args[1] as HttpFetchOptions;
if (path === TRUSTED_APPS_LIST_API) {
return {
data: [getFakeTrustedApp()],
total: 50, // << Should be a value large enough to fulfill two pages
page: httpOptions?.query?.page ?? 1,
per_page: httpOptions?.query?.per_page ?? 20,
};
}
if (currentGetHandler) {
return currentGetHandler(...args);
}
});
};
beforeAll(() => {
window.scrollTo = () => {};
@ -35,7 +84,7 @@ describe('When on the Trusted Apps Page', () => {
});
beforeEach(() => {
const mockedContext = createAppRootMockRenderer();
mockedContext = createAppRootMockRenderer();
history = mockedContext.history;
coreStart = mockedContext.coreStart;
@ -47,15 +96,27 @@ describe('When on the Trusted Apps Page', () => {
window.scrollTo = jest.fn();
});
it('should display subtitle info about trusted apps', async () => {
const { getByTestId } = render();
expect(getByTestId('header-panel-subtitle').textContent).toEqual(expectedAboutInfo);
});
describe('and there is trusted app entries', () => {
const renderWithListData = async () => {
const renderResult = render();
await act(async () => {
await waitForAction('trustedAppsListResourceStateChanged');
});
return renderResult;
};
it('should display a Add Trusted App button', async () => {
const { getByTestId } = render();
const addButton = await getByTestId('trustedAppsListAddButton');
expect(addButton.textContent).toBe('Add Trusted Application');
beforeEach(() => mockListApis(coreStart.http));
it('should display subtitle info about trusted apps', async () => {
const { getByTestId } = await renderWithListData();
expect(getByTestId('header-panel-subtitle').textContent).toEqual(expectedAboutInfo);
});
it('should display a Add Trusted App button', async () => {
const { getByTestId } = await renderWithListData();
const addButton = await getByTestId('trustedAppsListAddButton');
expect(addButton.textContent).toBe('Add Trusted Application');
});
});
describe('when the Add Trusted App button is clicked', () => {
@ -63,6 +124,9 @@ describe('When on the Trusted Apps Page', () => {
ReturnType<AppContextTestRender['render']>
> => {
const renderResult = render();
await act(async () => {
await waitForAction('trustedAppsListResourceStateChanged');
});
const addButton = renderResult.getByTestId('trustedAppsListAddButton');
reactTestingLibrary.act(() => {
fireEvent.click(addButton, { button: 1 });
@ -70,6 +134,8 @@ describe('When on the Trusted Apps Page', () => {
return renderResult;
};
beforeEach(() => mockListApis(coreStart.http));
it('should display the create flyout', async () => {
const { getByTestId } = await renderAndClickAddButton();
const flyout = getByTestId('addTrustedAppFlyout');
@ -245,7 +311,7 @@ describe('When on the Trusted Apps Page', () => {
});
it('should trigger the List to reload', async () => {
expect(coreStart.http.get.mock.calls[0][0]).toEqual('/api/endpoint/trusted_apps');
expect(coreStart.http.get.mock.calls[0][0]).toEqual(TRUSTED_APPS_LIST_API);
});
});
@ -296,4 +362,136 @@ describe('When on the Trusted Apps Page', () => {
});
});
});
describe('and there are no trusted apps', () => {
const releaseExistsResponse: jest.MockedFunction<
() => Promise<GetTrustedListAppsResponse>
> = jest.fn(async () => {
return {
data: [],
total: 0,
page: 1,
per_page: 1,
};
});
const releaseListResponse: jest.MockedFunction<
() => Promise<GetTrustedListAppsResponse>
> = jest.fn(async () => {
return {
data: [],
total: 0,
page: 1,
per_page: 20,
};
});
beforeEach(() => {
// @ts-ignore
coreStart.http.get.mockImplementation(async (path, options) => {
if (path === TRUSTED_APPS_LIST_API) {
const { page, per_page: perPage } = options.query as { page: number; per_page: number };
if (page === 1 && perPage === 1) {
return releaseExistsResponse();
} else {
return releaseListResponse();
}
}
});
});
afterEach(() => {
releaseExistsResponse.mockClear();
releaseListResponse.mockClear();
});
it('should show a loader until trusted apps existence can be confirmed', async () => {
// Make the call that checks if Trusted Apps exists not respond back
releaseExistsResponse.mockImplementationOnce(() => new Promise(() => {}));
const renderResult = render();
expect(await renderResult.findByTestId('trustedAppsListLoader')).not.toBeNull();
});
it('should show Empty Prompt if not entries exist', async () => {
const renderResult = render();
await act(async () => {
await waitForAction('trustedAppsExistStateChanged');
});
expect(await renderResult.findByTestId('trustedAppEmptyState')).not.toBeNull();
});
it('should hide empty prompt and show list after one trusted app is added', async () => {
const renderResult = render();
await act(async () => {
await waitForAction('trustedAppsExistStateChanged');
});
expect(await renderResult.findByTestId('trustedAppEmptyState')).not.toBeNull();
releaseListResponse.mockResolvedValueOnce({
data: [getFakeTrustedApp()],
total: 1,
page: 1,
per_page: 20,
});
releaseExistsResponse.mockResolvedValueOnce({
data: [getFakeTrustedApp()],
total: 1,
page: 1,
per_page: 1,
});
await act(async () => {
mockedContext.store.dispatch({
type: 'trustedAppsListDataOutdated',
});
await waitForAction('trustedAppsListResourceStateChanged');
});
expect(await renderResult.findByTestId('trustedAppsListPageContent')).not.toBeNull();
});
it('should should show empty prompt once the last trusted app entry is deleted', async () => {
releaseListResponse.mockResolvedValueOnce({
data: [getFakeTrustedApp()],
total: 1,
page: 1,
per_page: 20,
});
releaseExistsResponse.mockResolvedValueOnce({
data: [getFakeTrustedApp()],
total: 1,
page: 1,
per_page: 1,
});
const renderResult = render();
await act(async () => {
await waitForAction('trustedAppsExistStateChanged');
});
expect(await renderResult.findByTestId('trustedAppsListPageContent')).not.toBeNull();
releaseListResponse.mockResolvedValueOnce({
data: [],
total: 0,
page: 1,
per_page: 20,
});
releaseExistsResponse.mockResolvedValueOnce({
data: [],
total: 0,
page: 1,
per_page: 1,
});
await act(async () => {
mockedContext.store.dispatch({
type: 'trustedAppsListDataOutdated',
});
await waitForAction('trustedAppsListResourceStateChanged');
});
expect(await renderResult.findByTestId('trustedAppEmptyState')).not.toBeNull();
});
});
});

View file

@ -10,14 +10,21 @@ import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiButton,
EuiButtonEmpty,
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiLoadingSpinner,
EuiSpacer,
} from '@elastic/eui';
import { ViewType } from '../state';
import { getCurrentLocation, getListTotalItemsCount } from '../store/selectors';
import {
checkingIfEntriesExist,
entriesExist,
getCurrentLocation,
getListTotalItemsCount,
} from '../store/selectors';
import { useTrustedAppsNavigateCallback, useTrustedAppsSelector } from './hooks';
import { AdministrationListPage } from '../../../components/administration_list_page';
import { CreateTrustedAppFlyout } from './components/create_trusted_app_flyout';
@ -29,17 +36,22 @@ import { TrustedAppsNotifications } from './trusted_apps_notifications';
import { TrustedAppsListPageRouteState } from '../../../../../common/endpoint/types';
import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler';
import { ABOUT_TRUSTED_APPS } from './translations';
import { EmptyState } from './components/empty_state';
export const TrustedAppsPage = memo(() => {
const { state: routeState } = useLocation<TrustedAppsListPageRouteState | undefined>();
const location = useTrustedAppsSelector(getCurrentLocation);
const totalItemsCount = useTrustedAppsSelector(getListTotalItemsCount);
const isCheckingIfEntriesExists = useTrustedAppsSelector(checkingIfEntriesExist);
const doEntriesExist = useTrustedAppsSelector(entriesExist) === true;
const handleAddButtonClick = useTrustedAppsNavigateCallback(() => ({ show: 'create' }));
const handleAddFlyoutClose = useTrustedAppsNavigateCallback(() => ({ show: undefined }));
const handleViewTypeChange = useTrustedAppsNavigateCallback((viewType: ViewType) => ({
view_type: viewType,
}));
const showCreateFlyout = location.show === 'create';
const backButton = useMemo(() => {
if (routeState && routeState.onBackButtonNavigateTo) {
return <BackToExternalAppButton {...routeState} />;
@ -51,7 +63,7 @@ export const TrustedAppsPage = memo(() => {
<EuiButton
fill
iconType="plusInCircle"
isDisabled={location.show === 'create'}
isDisabled={showCreateFlyout}
onClick={handleAddButtonClick}
data-test-subj="trustedAppsListAddButton"
>
@ -62,6 +74,46 @@ export const TrustedAppsPage = memo(() => {
</EuiButton>
);
const content = (
<>
<TrustedAppDeletionDialog />
{showCreateFlyout && (
<CreateTrustedAppFlyout
onClose={handleAddFlyoutClose}
size="m"
data-test-subj="addTrustedAppFlyout"
/>
)}
{doEntriesExist ? (
<EuiFlexGroup
direction="column"
gutterSize="none"
data-test-subj="trustedAppsListPageContent"
>
<EuiFlexItem grow={false}>
<ControlPanel
totalItemCount={totalItemsCount}
currentViewType={location.view_type}
onViewTypeChange={handleViewTypeChange}
/>
<EuiSpacer size="m" />
</EuiFlexItem>
<EuiFlexItem>
<EuiHorizontalRule margin="none" />
{location.view_type === 'grid' && <TrustedAppsGrid />}
{location.view_type === 'list' && <TrustedAppsList />}
</EuiFlexItem>
</EuiFlexGroup>
) : (
<EmptyState onAdd={handleAddButtonClick} isAddDisabled={showCreateFlyout} />
)}
</>
);
return (
<AdministrationListPage
data-test-subj="trustedAppsListPage"
@ -74,34 +126,18 @@ export const TrustedAppsPage = memo(() => {
}
headerBackComponent={backButton}
subtitle={ABOUT_TRUSTED_APPS}
actions={addButton}
actions={doEntriesExist ? addButton : <></>}
>
<TrustedAppsNotifications />
<TrustedAppDeletionDialog />
{location.show === 'create' && (
<CreateTrustedAppFlyout
onClose={handleAddFlyoutClose}
size="m"
data-test-subj="addTrustedAppFlyout"
{isCheckingIfEntriesExists ? (
<EuiEmptyPrompt
data-test-subj="trustedAppsListLoader"
body={<EuiLoadingSpinner className="essentialAnimation" size="xl" />}
/>
) : (
content
)}
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem grow={false}>
<ControlPanel
totalItemCount={totalItemsCount}
currentViewType={location.view_type}
onViewTypeChange={handleViewTypeChange}
/>
<EuiSpacer size="m" />
</EuiFlexItem>
<EuiFlexItem>
<EuiHorizontalRule margin="none" />
{location.view_type === 'grid' && <TrustedAppsGrid />}
{location.view_type === 'list' && <TrustedAppsList />}
</EuiFlexItem>
</EuiFlexGroup>
</AdministrationListPage>
);
});

View file

@ -45,9 +45,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await testSubjects.click('trustedAppDeleteButton');
await testSubjects.click('trustedAppDeletionConfirm');
await testSubjects.waitForDeleted('trustedAppDeletionConfirm');
expect(await testSubjects.getVisibleText('trustedAppsListViewCountLabel')).to.equal(
'0 trusted applications'
);
expect(await testSubjects.existOrFail('trustedAppEmptyState'));
});
});
};