Skip to content

Commit f2c0e69

Browse files
authored
[Endpoint] Host Details Policy Response Panel (#63518) (#63759)
* Added link to Policy status that updates URL and show details panel * Custom Styled Flyout Panel sub-header component to display sub-headers * Move Middleware spy utils under `store/` for re-use * Changed `appStoreFactory()` to accept optional `additionalMiddleware` prop * `waitForAction` middleware test utility now return Action on Promise resolve * Updated PageView component to remove bottom margin
1 parent a1039fc commit f2c0e69

File tree

16 files changed

+516
-213
lines changed

16 files changed

+516
-213
lines changed

x-pack/plugins/endpoint/public/applications/endpoint/mocks/app_context_render.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { coreMock } from '../../../../../../../src/core/public/mocks';
1212
import { EndpointPluginStartDependencies } from '../../../plugin';
1313
import { depsStartMock } from './dependencies_start_mock';
1414
import { AppRootProvider } from '../view/app_root_provider';
15+
import { createSpyMiddleware, MiddlewareActionSpyHelper } from '../store/test_utils';
1516

1617
type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult;
1718

@@ -23,6 +24,7 @@ export interface AppContextTestRender {
2324
history: ReturnType<typeof createMemoryHistory>;
2425
coreStart: ReturnType<typeof coreMock.createStart>;
2526
depsStart: EndpointPluginStartDependencies;
27+
middlewareSpy: MiddlewareActionSpyHelper;
2628
/**
2729
* A wrapper around `AppRootContext` component. Uses the mocked modules as input to the
2830
* `AppRootContext`
@@ -45,7 +47,12 @@ export const createAppRootMockRenderer = (): AppContextTestRender => {
4547
const history = createMemoryHistory<never>();
4648
const coreStart = coreMock.createStart({ basePath: '/mock' });
4749
const depsStart = depsStartMock();
48-
const store = appStoreFactory({ coreStart, depsStart });
50+
const middlewareSpy = createSpyMiddleware();
51+
const store = appStoreFactory({
52+
coreStart,
53+
depsStart,
54+
additionalMiddleware: [middlewareSpy.actionSpyMiddleware],
55+
});
4956
const AppWrapper: React.FunctionComponent<{ children: React.ReactElement }> = ({ children }) => (
5057
<AppRootProvider store={store} history={history} coreStart={coreStart} depsStart={depsStart}>
5158
{children}
@@ -64,6 +71,7 @@ export const createAppRootMockRenderer = (): AppContextTestRender => {
6471
history,
6572
coreStart,
6673
depsStart,
74+
middlewareSpy,
6775
AppWrapper,
6876
render,
6977
};

x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const uiQueryParams: (
3737
// Removes the `?` from the beginning of query string if it exists
3838
const query = querystring.parse(location.search.slice(1));
3939

40-
const keys: Array<keyof HostIndexUIQueryParams> = ['selected_host'];
40+
const keys: Array<keyof HostIndexUIQueryParams> = ['selected_host', 'show'];
4141

4242
for (const key of keys) {
4343
const value = query[key];
@@ -58,3 +58,11 @@ export const hasSelectedHost: (state: Immutable<HostListState>) => boolean = cre
5858
return selectedHost !== undefined;
5959
}
6060
);
61+
62+
/** What policy details panel view to show */
63+
export const showView: (state: HostListState) => 'policy_response' | 'details' = createSelector(
64+
uiQueryParams,
65+
searchParams => {
66+
return searchParams.show === 'policy_response' ? 'policy_response' : 'details';
67+
}
68+
);

x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { alertMiddlewareFactory } from './alerts/middleware';
1919
import { hostMiddlewareFactory } from './hosts';
2020
import { policyListMiddlewareFactory } from './policy_list';
2121
import { policyDetailsMiddlewareFactory } from './policy_details';
22-
import { GlobalState } from '../types';
22+
import { GlobalState, MiddlewareFactory } from '../types';
2323
import { AppAction } from './action';
2424
import { EndpointPluginStartDependencies } from '../../../plugin';
2525

@@ -62,10 +62,15 @@ export const appStoreFactory: (middlewareDeps?: {
6262
* Give middleware access to plugin start dependencies.
6363
*/
6464
depsStart: EndpointPluginStartDependencies;
65+
/**
66+
* Any additional Redux Middlewares
67+
* (should only be used for testing - example: to inject the action spy middleware)
68+
*/
69+
additionalMiddleware?: Array<ReturnType<MiddlewareFactory>>;
6570
}) => Store = middlewareDeps => {
6671
let middleware;
6772
if (middlewareDeps) {
68-
const { coreStart, depsStart } = middlewareDeps;
73+
const { coreStart, depsStart, additionalMiddleware = [] } = middlewareDeps;
6974
middleware = composeWithReduxDevTools(
7075
applyMiddleware(
7176
substateMiddlewareFactory(
@@ -83,7 +88,9 @@ export const appStoreFactory: (middlewareDeps?: {
8388
substateMiddlewareFactory(
8489
globalState => globalState.alertList,
8590
alertMiddlewareFactory(coreStart, depsStart)
86-
)
91+
),
92+
// Additional Middleware should go last
93+
...additionalMiddleware
8794
)
8895
);
8996
} else {

x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/index.test.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,10 @@ import { policyListMiddlewareFactory } from './middleware';
1212
import { coreMock } from '../../../../../../../../src/core/public/mocks';
1313
import { isOnPolicyListPage, selectIsLoading, urlSearchParams } from './selectors';
1414
import { DepsStartMock, depsStartMock } from '../../mocks';
15-
import {
16-
createSpyMiddleware,
17-
MiddlewareActionSpyHelper,
18-
setPolicyListApiMockImplementation,
19-
} from './test_mock_utils';
15+
import { setPolicyListApiMockImplementation } from './test_mock_utils';
2016
import { INGEST_API_DATASOURCES } from './services/ingest';
2117
import { Immutable } from '../../../../../common/types';
18+
import { createSpyMiddleware, MiddlewareActionSpyHelper } from '../test_utils';
2219

2320
describe('policy list store concerns', () => {
2421
let fakeCoreStart: ReturnType<typeof coreMock.createStart>;

x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/test_mock_utils.ts

Lines changed: 1 addition & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@
55
*/
66

77
import { HttpStart } from 'kibana/public';
8-
import { Dispatch } from 'redux';
98
import { INGEST_API_DATASOURCES } from './services/ingest';
109
import { EndpointDocGenerator } from '../../../../../common/generate_data';
11-
import { AppAction, GetPolicyListResponse, GlobalState, MiddlewareFactory } from '../../types';
10+
import { GetPolicyListResponse } from '../../types';
1211

1312
const generator = new EndpointDocGenerator('policy-list');
1413

@@ -37,115 +36,3 @@ export const setPolicyListApiMockImplementation = (
3736
return Promise.reject(new Error(`MOCK: unknown policy list api: ${path}`));
3837
});
3938
};
40-
41-
/**
42-
* Utilities for testing Redux middleware
43-
*/
44-
export interface MiddlewareActionSpyHelper<S = GlobalState> {
45-
/**
46-
* Returns a promise that is fulfilled when the given action is dispatched or a timeout occurs.
47-
* The use of this method instead of a `sleep()` type of delay should avoid test case instability
48-
* especially when run in a CI environment.
49-
*
50-
* @param actionType
51-
*/
52-
waitForAction: (actionType: AppAction['type']) => Promise<void>;
53-
/**
54-
* A property holding the information around the calls that were processed by the internal
55-
* `actionSpyMiddlware`. This property holds the information typically found in Jets's mocked
56-
* function `mock` property - [see here for more information](https://jestjs.io/docs/en/mock-functions#mock-property)
57-
*
58-
* **Note**: this property will only be set **after* the `actionSpyMiddlware` has been
59-
* initialized (ex. via `createStore()`. Attempting to reference this property before that time
60-
* will throw an error.
61-
* Also - do not hold on to references to this property value if `jest.clearAllMocks()` or
62-
* `jest.resetAllMocks()` is called between usages of the value.
63-
*/
64-
dispatchSpy: jest.Mock<Dispatch<AppAction>>['mock'];
65-
/**
66-
* Redux middleware that enables spying on the action that are dispatched through the store
67-
*/
68-
actionSpyMiddleware: ReturnType<MiddlewareFactory<S>>;
69-
}
70-
71-
/**
72-
* Creates a new instance of middleware action helpers
73-
* Note: in most cases (testing concern specific middleware) this function should be given
74-
* the state type definition, else, the global state will be used.
75-
*
76-
* @example
77-
* // Use in Policy List middleware testing
78-
* const middlewareSpyUtils = createSpyMiddleware<PolicyListState>();
79-
* store = createStore(
80-
* policyListReducer,
81-
* applyMiddleware(
82-
* policyListMiddlewareFactory(fakeCoreStart, depsStart),
83-
* middlewareSpyUtils.actionSpyMiddleware
84-
* )
85-
* );
86-
* // Reference `dispatchSpy` ONLY after creating the store that includes `actionSpyMiddleware`
87-
* const { waitForAction, dispatchSpy } = middlewareSpyUtils;
88-
* //
89-
* // later in test
90-
* //
91-
* it('...', async () => {
92-
* //...
93-
* await waitForAction('serverReturnedPolicyListData');
94-
* // do assertions
95-
* // or check how action was called
96-
* expect(dispatchSpy.calls.length).toBe(2)
97-
* });
98-
*/
99-
export const createSpyMiddleware = <S = GlobalState>(): MiddlewareActionSpyHelper<S> => {
100-
type ActionWatcher = (action: AppAction) => void;
101-
102-
const watchers = new Set<ActionWatcher>();
103-
let spyDispatch: jest.Mock<Dispatch<AppAction>>;
104-
105-
return {
106-
waitForAction: async (actionType: string) => {
107-
// Error is defined here so that we get a better stack trace that points to the test from where it was used
108-
const err = new Error(`action '${actionType}' was not dispatched within the allocated time`);
109-
110-
await new Promise((resolve, reject) => {
111-
const watch: ActionWatcher = action => {
112-
if (action.type === actionType) {
113-
watchers.delete(watch);
114-
clearTimeout(timeout);
115-
resolve();
116-
}
117-
};
118-
119-
// We timeout before jest's default 5s, so that a better error stack is returned
120-
const timeout = setTimeout(() => {
121-
watchers.delete(watch);
122-
reject(err);
123-
}, 4500);
124-
watchers.add(watch);
125-
});
126-
},
127-
128-
get dispatchSpy() {
129-
if (!spyDispatch) {
130-
throw new Error(
131-
'Spy Middleware has not been initialized. Access this property only after using `actionSpyMiddleware` in a redux store'
132-
);
133-
}
134-
return spyDispatch.mock;
135-
},
136-
137-
actionSpyMiddleware: api => {
138-
return next => {
139-
spyDispatch = jest.fn(action => {
140-
next(action);
141-
// loop through the list of watcher (if any) and call them with this action
142-
for (const watch of watchers) {
143-
watch(action);
144-
}
145-
return action;
146-
});
147-
return spyDispatch;
148-
};
149-
},
150-
};
151-
};
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { Dispatch } from 'redux';
8+
import { AppAction, GlobalState, MiddlewareFactory } from '../types';
9+
10+
/**
11+
* Utilities for testing Redux middleware
12+
*/
13+
export interface MiddlewareActionSpyHelper<S = GlobalState, A extends AppAction = AppAction> {
14+
/**
15+
* Returns a promise that is fulfilled when the given action is dispatched or a timeout occurs.
16+
* The `action` will given to the promise `resolve` thus allowing for checks to be done.
17+
* The use of this method instead of a `sleep()` type of delay should avoid test case instability
18+
* especially when run in a CI environment.
19+
*
20+
* @param actionType
21+
*/
22+
waitForAction: <T extends A['type']>(actionType: T) => Promise<A extends { type: T } ? A : never>;
23+
/**
24+
* A property holding the information around the calls that were processed by the internal
25+
* `actionSpyMiddelware`. This property holds the information typically found in Jets's mocked
26+
* function `mock` property - [see here for more information](https://jestjs.io/docs/en/mock-functions#mock-property)
27+
*
28+
* **Note**: this property will only be set **after* the `actionSpyMiddlware` has been
29+
* initialized (ex. via `createStore()`. Attempting to reference this property before that time
30+
* will throw an error.
31+
* Also - do not hold on to references to this property value if `jest.clearAllMocks()` or
32+
* `jest.resetAllMocks()` is called between usages of the value.
33+
*/
34+
dispatchSpy: jest.Mock<Dispatch<A>>['mock'];
35+
/**
36+
* Redux middleware that enables spying on the action that are dispatched through the store
37+
*/
38+
actionSpyMiddleware: ReturnType<MiddlewareFactory<S>>;
39+
}
40+
41+
/**
42+
* Creates a new instance of middleware action helpers
43+
* Note: in most cases (testing concern specific middleware) this function should be given
44+
* the state type definition, else, the global state will be used.
45+
*
46+
* @example
47+
* // Use in Policy List middleware testing
48+
* const middlewareSpyUtils = createSpyMiddleware<PolicyListState>();
49+
* store = createStore(
50+
* policyListReducer,
51+
* applyMiddleware(
52+
* policyListMiddlewareFactory(fakeCoreStart, depsStart),
53+
* middlewareSpyUtils.actionSpyMiddleware
54+
* )
55+
* );
56+
* // Reference `dispatchSpy` ONLY after creating the store that includes `actionSpyMiddleware`
57+
* const { waitForAction, dispatchSpy } = middlewareSpyUtils;
58+
* //
59+
* // later in test
60+
* //
61+
* it('...', async () => {
62+
* //...
63+
* await waitForAction('serverReturnedPolicyListData');
64+
* // do assertions
65+
* // or check how action was called
66+
* expect(dispatchSpy.calls.length).toBe(2)
67+
* });
68+
*/
69+
export const createSpyMiddleware = <
70+
S = GlobalState,
71+
A extends AppAction = AppAction
72+
>(): MiddlewareActionSpyHelper<S, A> => {
73+
type ActionWatcher = (action: A) => void;
74+
75+
const watchers = new Set<ActionWatcher>();
76+
let spyDispatch: jest.Mock<Dispatch<A>>;
77+
78+
return {
79+
waitForAction: async actionType => {
80+
type ResolvedAction = A extends { type: typeof actionType } ? A : never;
81+
82+
// Error is defined here so that we get a better stack trace that points to the test from where it was used
83+
const err = new Error(`action '${actionType}' was not dispatched within the allocated time`);
84+
85+
return new Promise<ResolvedAction>((resolve, reject) => {
86+
const watch: ActionWatcher = action => {
87+
if (action.type === actionType) {
88+
watchers.delete(watch);
89+
clearTimeout(timeout);
90+
resolve(action as ResolvedAction);
91+
}
92+
};
93+
94+
// We timeout before jest's default 5s, so that a better error stack is returned
95+
const timeout = setTimeout(() => {
96+
watchers.delete(watch);
97+
reject(err);
98+
}, 4500);
99+
watchers.add(watch);
100+
});
101+
},
102+
103+
get dispatchSpy() {
104+
if (!spyDispatch) {
105+
throw new Error(
106+
'Spy Middleware has not been initialized. Access this property only after using `actionSpyMiddleware` in a redux store'
107+
);
108+
}
109+
return spyDispatch.mock;
110+
},
111+
112+
actionSpyMiddleware: api => {
113+
return next => {
114+
spyDispatch = jest.fn(action => {
115+
next(action);
116+
// loop through the list of watcher (if any) and call them with this action
117+
for (const watch of watchers) {
118+
watch(action);
119+
}
120+
return action;
121+
});
122+
return spyDispatch;
123+
};
124+
},
125+
};
126+
};

x-pack/plugins/endpoint/public/applications/endpoint/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export interface HostListPagination {
5252
}
5353
export interface HostIndexUIQueryParams {
5454
selected_host?: string;
55+
show?: string;
5556
}
5657

5758
export interface ServerApiError {

0 commit comments

Comments
 (0)