Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Endpoint] task/management-details #58308

Merged
merged 13 commits into from
Mar 4, 2020
2 changes: 1 addition & 1 deletion x-pack/plugins/endpoint/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ interface AlertMetadata {
export type AlertData = AlertEvent & AlertMetadata;

export interface EndpointMetadata {
'@timestamp': string;
event: {
created: Date;
};
Expand All @@ -264,7 +265,6 @@ export interface EndpointMetadata {
agent: {
version: string;
id: string;
name: string;
};
host: HostFields;
}
Expand Down
10 changes: 5 additions & 5 deletions x-pack/plugins/endpoint/public/applications/endpoint/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ interface RouterProps {
}

const AppRoot: React.FunctionComponent<RouterProps> = React.memo(
({ basename, store, coreStart: { http } }) => (
({ basename, store, coreStart: { http, notifications } }) => (
<Provider store={store}>
<KibanaContextProvider services={{ http }}>
<I18nProvider>
<I18nProvider>
<KibanaContextProvider services={{ http, notifications }}>
<BrowserRouter basename={basename}>
<RouteCapture>
<HeaderNavigation basename={basename} />
Expand Down Expand Up @@ -72,8 +72,8 @@ const AppRoot: React.FunctionComponent<RouterProps> = React.memo(
</Switch>
</RouteCapture>
</BrowserRouter>
</I18nProvider>
</KibanaContextProvider>
</KibanaContextProvider>
</I18nProvider>
</Provider>
)
);
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { ManagementListPagination } from '../../types';
import { EndpointResultList } from '../../../../../common/types';
import { ManagementListPagination, ServerApiError } from '../../types';
import { EndpointResultList, EndpointMetadata } from '../../../../../common/types';

interface ServerReturnedManagementList {
type: 'serverReturnedManagementList';
payload: EndpointResultList;
}

interface ServerReturnedManagementDetails {
type: 'serverReturnedManagementDetails';
payload: EndpointMetadata;
}

interface ServerFailedToReturnManagementDetails {
type: 'serverFailedToReturnManagementDetails';
payload: ServerApiError;
}

interface UserExitedManagementList {
type: 'userExitedManagementList';
}
Expand All @@ -23,5 +33,7 @@ interface UserPaginatedManagementList {

export type ManagementAction =
| ServerReturnedManagementList
| ServerReturnedManagementDetails
| ServerFailedToReturnManagementDetails
| UserExitedManagementList
| UserPaginatedManagementList;
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe('endpoint_list store concerns', () => {
};
const generateEndpoint = (): EndpointMetadata => {
return {
'@timestamp': new Date(1582231151055).toString(),
event: {
created: new Date(0),
},
Expand All @@ -30,7 +31,6 @@ describe('endpoint_list store concerns', () => {
agent: {
version: '',
id: '',
name: '',
},
host: {
id: '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { CoreStart, HttpSetup } from 'kibana/public';
import { applyMiddleware, createStore, Dispatch, Store } from 'redux';
import { coreMock } from '../../../../../../../../src/core/public/mocks';
import { History, createBrowserHistory } from 'history';
import { managementListReducer, managementMiddlewareFactory } from './index';
import { EndpointMetadata, EndpointResultList } from '../../../../../common/types';
import { ManagementListState } from '../../types';
Expand All @@ -18,9 +19,12 @@ describe('endpoint list saga', () => {
let store: Store<ManagementListState>;
let getState: typeof store['getState'];
let dispatch: Dispatch<AppAction>;
let history: History<never>;

// https://github.com/elastic/endpoint-app-team/issues/131
const generateEndpoint = (): EndpointMetadata => {
return {
'@timestamp': new Date(1582231151055).toString(),
event: {
created: new Date(0),
},
Expand All @@ -32,7 +36,6 @@ describe('endpoint list saga', () => {
agent: {
version: '',
id: '',
name: '',
},
host: {
id: '',
Expand Down Expand Up @@ -65,12 +68,20 @@ describe('endpoint list saga', () => {
);
getState = store.getState;
dispatch = store.dispatch;
history = createBrowserHistory();
});
test('it handles `userNavigatedToPage`', async () => {
test('it handles `userChangedUrl`', async () => {
const apiResponse = getEndpointListApiResponse();
fakeHttpServices.post.mockResolvedValue(apiResponse);
expect(fakeHttpServices.post).not.toHaveBeenCalled();
dispatch({ type: 'userNavigatedToPage', payload: 'managementPage' });

dispatch({
type: 'userChangedUrl',
payload: {
...history.location,
pathname: '/management',
},
});
await sleep();
expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/metadata', {
body: JSON.stringify({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,28 @@
*/

import { MiddlewareFactory } from '../../types';
import { pageIndex, pageSize } from './selectors';
import {
pageIndex,
pageSize,
isOnManagementPage,
hasSelectedHost,
uiQueryParams,
} from './selectors';
import { ManagementListState } from '../../types';
import { AppAction } from '../action';

export const managementMiddlewareFactory: MiddlewareFactory<ManagementListState> = coreStart => {
return ({ getState, dispatch }) => next => async (action: AppAction) => {
next(action);
const state = getState();
if (
(action.type === 'userNavigatedToPage' && action.payload === 'managementPage') ||
(action.type === 'userChangedUrl' &&
isOnManagementPage(state) &&
hasSelectedHost(state) !== true) ||
action.type === 'userPaginatedManagementList'
) {
const managementPageIndex = pageIndex(getState());
const managementPageSize = pageSize(getState());
const managementPageIndex = pageIndex(state);
const managementPageSize = pageSize(state);
const response = await coreStart.http.post('/api/endpoint/metadata', {
body: JSON.stringify({
paging_properties: [
Expand All @@ -32,5 +41,20 @@ export const managementMiddlewareFactory: MiddlewareFactory<ManagementListState>
payload: response,
});
}
if (action.type === 'userChangedUrl' && hasSelectedHost(state) !== false) {
const { selected_host: selectedHost } = uiQueryParams(state);
try {
const response = await coreStart.http.get(`/api/endpoint/metadata/${selectedHost}`);
Copy link
Contributor

@peluja1012 peluja1012 Feb 27, 2020

Choose a reason for hiding this comment

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

Why is the API endpoint called /endpoint/metadata? Doesn't it return a list of hosts? @kevinlog

Copy link
Contributor

Choose a reason for hiding this comment

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

@peluja1012 we talked about a couple different options here besides /endpoint/endpoints. One was /endpoint/hosts. We settled on /endpoint/metadata since the information will eventually be more than just host data and will include more data specific to endpoints

dispatch({
type: 'serverReturnedManagementDetails',
payload: response,
});
} catch (error) {
dispatch({
type: 'serverFailedToReturnManagementDetails',
payload: error,
});
}
}
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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 { EndpointResultList } from '../../../../../common/types';

export const mockHostResultList: (options?: {
total?: number;
request_page_size?: number;
request_page_index?: number;
}) => EndpointResultList = (options = {}) => {
const {
total = 1,
request_page_size: requestPageSize = 10,
request_page_index: requestPageIndex = 0,
} = options;

// Skip any that are before the page we're on
const numberToSkip = requestPageSize * requestPageIndex;

// total - numberToSkip is the count of non-skipped ones, but return no more than a pageSize, and no less than 0
const actualCountToReturn = Math.max(Math.min(total - numberToSkip, requestPageSize), 0);

const endpoints = [];
for (let index = 0; index < actualCountToReturn; index++) {
endpoints.push({
'@timestamp': new Date(1582231151055).toString(),
event: {
created: new Date('2020-02-20T20:39:11.055Z'),
},
endpoint: {
policy: {
id: '00000000-0000-0000-0000-000000000000',
},
},
agent: {
version: '6.9.2',
id: '9a87fdac-e6c0-4f27-a25c-e349e7093cb1',
},
host: {
id: '3ca26fe5-1c7d-42b8-8763-98256d161c9f',
hostname: 'bea-0.example.com',
ip: ['10.154.150.114', '10.43.37.62', '10.217.73.149'],
mac: ['ea-5a-a8-c0-5-95', '7e-d8-fe-7f-b6-4e', '23-31-5d-af-e6-2b'],
os: {
name: 'windows 6.2',
full: 'Windows Server 2012',
version: '6.2',
variant: 'Windows Server Release 2',
},
},
});
}
const mock: EndpointResultList = {
endpoints,
total,
request_page_size: requestPageSize,
request_page_index: requestPageIndex,
};
return mock;
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ const initialState = (): ManagementListState => {
pageIndex: 0,
total: 0,
loading: false,
detailsError: undefined,
details: undefined,
location: undefined,
};
};

Expand All @@ -37,18 +40,30 @@ export const managementListReducer: Reducer<ManagementListState, AppAction> = (
pageIndex,
loading: false,
};
}

if (action.type === 'userExitedManagementList') {
} else if (action.type === 'serverReturnedManagementDetails') {
return {
...state,
details: action.payload,
};
} else if (action.type === 'serverFailedToReturnManagementDetails') {
return {
...state,
detailsError: action.payload,
};
} else if (action.type === 'userExitedManagementList') {
return initialState();
}

if (action.type === 'userPaginatedManagementList') {
} else if (action.type === 'userPaginatedManagementList') {
return {
...state,
...action.payload,
loading: true,
};
} else if (action.type === 'userChangedUrl') {
Copy link
Contributor

Choose a reason for hiding this comment

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

@oatkiller @paul-tavares Are we ok with copying and pasting this in all of our reducers?

Copy link
Contributor

Choose a reason for hiding this comment

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

@peluja1012 I don't like it either.
Personally, I think we should capture the route info. once and have a common (lib) method to retrieve/access it from anywhere. I think that would keep the middleware/reducer still "pure" (not force it to be aware of global state).

Example:

  1. view/route_capture.tsx could store the current location information locally in that module
  2. view/route_capture.tsx could export a function (call it getCurrentLocation(): EndpointAppLocation) that we can use from anywhere (in or outside of react components) to gain access to the current location.

Just a example of how it could be done - probably not in the view/ but in some /lib/.... With this in place, we could then have a generic isOnPage(pageId | pageRoute) type of getter (or multiples - one per page isOnPolicyListPage(), isOnManagementPage(), etc...

Any other ideas/suggestions? Lets agree on an implementation design approach and then we can move forward on getting it through

return {
...state,
location: action.payload,
detailsError: undefined,
};
}

return state;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { ManagementListState } from '../../types';
import querystring from 'querystring';
import { createSelector } from 'reselect';
import { Immutable } from '../../../../../common/types';
import { ManagementListState, ManagingIndexUIQueryParams } from '../../types';

export const listData = (state: ManagementListState) => state.endpoints;

Expand All @@ -15,3 +17,44 @@ export const pageSize = (state: ManagementListState) => state.pageSize;
export const totalHits = (state: ManagementListState) => state.total;

export const isLoading = (state: ManagementListState) => state.loading;

export const detailsError = (state: ManagementListState) => state.detailsError;

export const detailsData = (state: ManagementListState) => {
return state.details;
};

export const isOnManagementPage = (state: ManagementListState) =>
state.location ? state.location.pathname === '/management' : false;

export const uiQueryParams: (
state: ManagementListState
) => Immutable<ManagingIndexUIQueryParams> = createSelector(
(state: ManagementListState) => state.location,
(location: ManagementListState['location']) => {
const data: ManagingIndexUIQueryParams = {};
if (location) {
// Removes the `?` from the beginning of query string if it exists
const query = querystring.parse(location.search.slice(1));

const keys: Array<keyof ManagingIndexUIQueryParams> = ['selected_host'];

for (const key of keys) {
const value = query[key];
if (typeof value === 'string') {
data[key] = value;
} else if (Array.isArray(value)) {
data[key] = value[value.length - 1];
}
}
}
return data;
}
);

export const hasSelectedHost: (state: ManagementListState) => boolean = createSelector(
uiQueryParams,
({ selected_host: selectedHost }) => {
return selectedHost !== undefined;
}
);
12 changes: 12 additions & 0 deletions x-pack/plugins/endpoint/public/applications/endpoint/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,24 @@ export interface ManagementListState {
pageSize: number;
pageIndex: number;
loading: boolean;
detailsError?: ServerApiError;
details?: Immutable<EndpointMetadata>;
location?: Immutable<EndpointAppLocation>;
}

export interface ManagementListPagination {
pageIndex: number;
pageSize: number;
}
export interface ManagingIndexUIQueryParams {
selected_host?: string;
}

export interface ServerApiError {
statusCode: number;
error: string;
message: string;
}

// REFACTOR to use Types from Ingest Manager - see: https://github.com/elastic/endpoint-app-team/issues/150
export interface PolicyData {
Expand Down
Loading