diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts index 93253062641d7..42a1497a7ed0a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts @@ -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; listView: { listResourceState: AsyncResourceState; freshDataTimestamp: number; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts index 98554bd7c4d17..4cfeb79283f82 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts @@ -54,6 +54,10 @@ export type TrustedAppCreationDialogConfirmed = Action<'trustedAppCreationDialog export type TrustedAppCreationDialogClosed = Action<'trustedAppCreationDialogClosed'>; +export type TrustedAppsExistResponse = Action<'trustedAppsExistStateChanged'> & { + payload: AsyncResourceState; +}; + export type TrustedAppsPageAction = | TrustedAppsListDataOutdated | TrustedAppsListResourceStateChanged @@ -65,4 +69,5 @@ export type TrustedAppsPageAction = | TrustedAppCreationDialogStarted | TrustedAppCreationDialogFormStateUpdated | TrustedAppCreationDialogConfirmed + | TrustedAppsExistResponse | TrustedAppCreationDialogClosed; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts index c71253a8b8875..aa4e03a71f40a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts @@ -40,6 +40,7 @@ export const initialCreationDialogState = (): TrustedAppsListPageState['creation }); export const initialTrustedAppsPageState = (): TrustedAppsListPageState => ({ + entriesExist: { type: 'UninitialisedResourceState' }, listView: { listResourceState: { type: 'UninitialisedResourceState' }, freshDataTimestamp: Date.now(), diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts index 735e63f8e084b..6f9c76e4325ae 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts @@ -66,6 +66,26 @@ const createStoreSetup = (trustedAppsService: TrustedAppsService) => { }; describe('middleware', () => { + type TrustedAppsEntriesExistState = Pick; + 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, diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts index 4508e25d3db33..d60028b6d1554 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts @@ -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, + 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 => { @@ -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') { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts index 61ac476c2b98b..219d1b8cdc5f1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts @@ -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; type CaseReducer = ( @@ -142,6 +144,16 @@ const userChangedUrl: CaseReducer = (state, action) => { } }; +const updateEntriesExists: CaseReducer = (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; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts index 872489605f777..3c57da9843ca8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts @@ -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 +) => Immutable = (state) => state.entriesExist; + +export const checkingIfEntriesExist: ( + state: Immutable +) => boolean = createSelector(entriesExistState, (doEntriesExists) => { + return !isLoadedResourceState(doEntriesExists); +}); + +export const entriesExist: (state: Immutable) => boolean = createSelector( + entriesExistState, + (doEntriesExists) => { + return isLoadedResourceState(doEntriesExists) && doEntriesExists.data; + } +); + +export const trustedAppsListPageActive: (state: Immutable) => boolean = ( + state +) => state.active; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/empty_state.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/empty_state.tsx new file mode 100644 index 0000000000000..536995109ebb7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/empty_state.tsx @@ -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 ( + + + + } + body={ + + } + actions={ + + + + } + /> + ); +}); + +EmptyState.displayName = 'EmptyState'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index cb94e3bf56f91..3faa2251b1dbc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -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; 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 > => { 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 + > = jest.fn(async () => { + return { + data: [], + total: 0, + page: 1, + per_page: 1, + }; + }); + const releaseListResponse: jest.MockedFunction< + () => Promise + > = 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(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx index 2d0b9f759f158..2324c99e6270e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx @@ -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(); 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 ; @@ -51,7 +63,7 @@ export const TrustedAppsPage = memo(() => { @@ -62,6 +74,46 @@ export const TrustedAppsPage = memo(() => { ); + const content = ( + <> + + + {showCreateFlyout && ( + + )} + + {doEntriesExist ? ( + + + + + + + + + + {location.view_type === 'grid' && } + {location.view_type === 'list' && } + + + ) : ( + + )} + + ); + return ( { } headerBackComponent={backButton} subtitle={ABOUT_TRUSTED_APPS} - actions={addButton} + actions={doEntriesExist ? addButton : <>} > - - {location.show === 'create' && ( - } /> + ) : ( + content )} - - - - - - - - - - {location.view_type === 'grid' && } - {location.view_type === 'list' && } - - ); }); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts index 3a0f0b91bddb3..ec7e0d06baa02 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts @@ -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')); }); }); };