[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:
parent
b203eaf370
commit
14df31b6a0
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -40,6 +40,7 @@ export const initialCreationDialogState = (): TrustedAppsListPageState['creation
|
|||
});
|
||||
|
||||
export const initialTrustedAppsPageState = (): TrustedAppsListPageState => ({
|
||||
entriesExist: { type: 'UninitialisedResourceState' },
|
||||
listView: {
|
||||
listResourceState: { type: 'UninitialisedResourceState' },
|
||||
freshDataTimestamp: Date.now(),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue