[Endpoint] Move the Endpoint Host page over to the Management Endpoints sub-tab (#68002)

* move code dir. to management/pages
* Make hosts appear on endpoints tab
* Add support for `className` to `<FormattedDate>` component
* add FormattedDate to Host list to display last seen date
This commit is contained in:
Paul Tavares 2020-06-04 21:09:00 -04:00 committed by GitHub
parent c3269fa117
commit cdb6b4dd33
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 327 additions and 233 deletions

View file

@ -149,7 +149,9 @@ export const PageView = memo<PageViewProps>(
)}
</EuiPageHeader>
)}
{tabs && <EuiTabs className="endpoint-navTabs">{tabComponents}</EuiTabs>}
{tabComponents.length > 0 && (
<EuiTabs className="endpoint-navTabs">{tabComponents}</EuiTabs>
)}
<EuiPageContent className="endpoint-page-content">
{bodyHeader && (
<EuiPageContentHeader>

View file

@ -75,14 +75,15 @@ PreferenceFormattedP1DTDate.displayName = 'PreferenceFormattedP1DTDate';
export const FormattedDate = React.memo<{
fieldName: string;
value?: string | number | null;
className?: string;
}>(
({ value, fieldName }): JSX.Element => {
({ value, fieldName, className = '' }): JSX.Element => {
if (value == null) {
return getOrEmptyTagFromValue(value);
}
const maybeDate = getMaybeDate(value);
return maybeDate.isValid() ? (
<LocalizedDateTooltip date={maybeDate.toDate()} fieldName={fieldName}>
<LocalizedDateTooltip date={maybeDate.toDate()} fieldName={fieldName} className={className}>
<PreferenceFormattedDate value={maybeDate.toDate()} />
</LocalizedDateTooltip>
) : (

View file

@ -13,9 +13,11 @@ export const LocalizedDateTooltip = React.memo<{
children: React.ReactNode;
date: Date;
fieldName?: string;
}>(({ children, date, fieldName }) => (
className?: string;
}>(({ children, date, fieldName, className = '' }) => (
<EuiToolTip
data-test-subj="localized-date-tool-tip"
anchorClassName={className}
content={
<EuiFlexGroup data-test-subj="dates-container" direction="column" gutterSize="none">
{fieldName != null ? (

View file

@ -18,7 +18,7 @@ import { createStore, State, substateMiddlewareFactory } from '../../store';
import { alertMiddlewareFactory } from '../../../endpoint_alerts/store/middleware';
import { AppRootProvider } from './app_root_provider';
import { managementMiddlewareFactory } from '../../../management/store/middleware';
import { hostMiddlewareFactory } from '../../../endpoint_hosts/store/middleware';
import { createKibanaContextProviderMock } from '../kibana_react';
import { SUB_PLUGINS_REDUCER, mockGlobalState } from '..';
type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult;
@ -57,10 +57,6 @@ export const createAppRootMockRenderer = (): AppContextTestRender => {
const depsStart = depsStartMock();
const middlewareSpy = createSpyMiddleware();
const store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, apolloClientObservable, [
substateMiddlewareFactory(
(globalState) => globalState.hostList,
hostMiddlewareFactory(coreStart, depsStart)
),
substateMiddlewareFactory(
(globalState) => globalState.alertList,
alertMiddlewareFactory(coreStart, depsStart)
@ -68,11 +64,14 @@ export const createAppRootMockRenderer = (): AppContextTestRender => {
...managementMiddlewareFactory(coreStart, depsStart),
middlewareSpy.actionSpyMiddleware,
]);
const MockKibanaContextProvider = createKibanaContextProviderMock();
const AppWrapper: React.FC<{ children: React.ReactElement }> = ({ children }) => (
<AppRootProvider store={store} history={history} coreStart={coreStart} depsStart={depsStart}>
{children}
</AppRootProvider>
<MockKibanaContextProvider>
<AppRootProvider store={store} history={history} coreStart={coreStart} depsStart={depsStart}>
{children}
</AppRootProvider>
</MockKibanaContextProvider>
);
const render: UiRender = (ui, options) => {
return reactRender(ui, {

View file

@ -26,10 +26,8 @@ import {
import { networkModel } from '../../network/store';
import { TimelineType, TimelineStatus } from '../../../common/types/timeline';
import { initialAlertListState } from '../../endpoint_alerts/store/reducer';
import { initialHostListState } from '../../endpoint_hosts/store/reducer';
import { mockManagementState } from '../../management/store/reducer';
import { AlertListState } from '../../../common/endpoint_alerts/types';
import { HostState } from '../../endpoint_hosts/types';
import { ManagementState } from '../../management/types';
export const mockGlobalState: State = {
@ -237,6 +235,5 @@ export const mockGlobalState: State = {
* they are cast to mutable versions here.
*/
alertList: initialAlertListState as AlertListState,
hostList: initialHostListState as HostState,
management: mockManagementState as ManagementState,
};

View file

@ -11,9 +11,7 @@ import { managementReducer } from '../../management/store/reducer';
import { ManagementPluginReducer } from '../../management';
import { SubPluginsInitReducer } from '../store';
import { EndpointAlertsPluginReducer } from '../../endpoint_alerts';
import { EndpointHostsPluginReducer } from '../../endpoint_hosts';
import { alertListReducer } from '../../endpoint_alerts/store/reducer';
import { hostListReducer } from '../../endpoint_hosts/store/reducer';
interface Global extends NodeJS.Global {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -30,7 +28,6 @@ export const SUB_PLUGINS_REDUCER: SubPluginsInitReducer = {
* These state's are wrapped in `Immutable`, but for compatibility with the overall app architecture,
* they are cast to mutable versions here.
*/
hostList: hostListReducer as EndpointHostsPluginReducer['hostList'],
alertList: alertListReducer as EndpointAlertsPluginReducer['alertList'],
management: managementReducer as ManagementPluginReducer['management'],
};

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { HostAction } from '../../endpoint_hosts/store/action';
import { HostAction } from '../../management/pages/endpoint_hosts/store/action';
import { AlertAction } from '../../endpoint_alerts/store/action';
import { PolicyListAction } from '../../management/pages/policy/store/policy_list';
import { PolicyDetailsAction } from '../../management/pages/policy/store/policy_details';

View file

@ -17,7 +17,6 @@ import { TimelinePluginReducer } from '../../timelines/store/timeline';
import { SecuritySubPlugins } from '../../app/types';
import { ManagementPluginReducer } from '../../management';
import { EndpointAlertsPluginReducer } from '../../endpoint_alerts';
import { EndpointHostsPluginReducer } from '../../endpoint_hosts';
import { State } from './types';
import { AppAction } from './actions';
@ -25,7 +24,6 @@ export type SubPluginsInitReducer = HostsPluginReducer &
NetworkPluginReducer &
TimelinePluginReducer &
EndpointAlertsPluginReducer &
EndpointHostsPluginReducer &
ManagementPluginReducer;
/**

View file

@ -17,7 +17,6 @@ import { DragAndDropState } from './drag_and_drop/reducer';
import { TimelinePluginState } from '../../timelines/store/timeline';
import { NetworkPluginState } from '../../network/store';
import { EndpointAlertsPluginState } from '../../endpoint_alerts';
import { EndpointHostsPluginState } from '../../endpoint_hosts';
import { ManagementPluginState } from '../../management';
/**
@ -31,7 +30,6 @@ export type State = CombinedState<
NetworkPluginState &
TimelinePluginState &
EndpointAlertsPluginState &
EndpointHostsPluginState &
ManagementPluginState & {
app: AppState;
dragAndDrop: DragAndDropState;

View file

@ -1,61 +0,0 @@
/*
* 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 { Reducer } from 'redux';
import { SecuritySubPluginWithStore } from '../app/types';
import { endpointHostsRoutes } from './routes';
import { hostListReducer } from './store/reducer';
import { HostState } from './types';
import { hostMiddlewareFactory } from './store/middleware';
import { CoreStart } from '../../../../../src/core/public';
import { StartPlugins } from '../types';
import { substateMiddlewareFactory } from '../common/store';
import { AppAction } from '../common/store/actions';
/**
* Internally, our state is sometimes immutable, ignore that in our external
* interface.
*/
export interface EndpointHostsPluginState {
hostList: HostState;
}
/**
* Internally, we use `ImmutableReducer`, but we present a regular reducer
* externally for compatibility w/ regular redux.
*/
export interface EndpointHostsPluginReducer {
hostList: Reducer<HostState, AppAction>;
}
export class EndpointHosts {
public setup() {}
public start(
core: CoreStart,
plugins: StartPlugins
): SecuritySubPluginWithStore<'hostList', HostState> {
const { data, ingestManager } = plugins;
const middleware = [
substateMiddlewareFactory(
(globalState) => globalState.hostList,
hostMiddlewareFactory(core, { data, ingestManager })
),
];
return {
routes: endpointHostsRoutes(),
store: {
initialState: { hostList: undefined },
/**
* Cast the ImmutableReducer to a regular reducer for compatibility with
* the subplugin architecture (which expects plain redux reducers.)
*/
reducer: { hostList: hostListReducer } as EndpointHostsPluginReducer,
middleware,
},
};
}
}

View file

@ -19,3 +19,5 @@ export const MANAGEMENT_STORE_GLOBAL_NAMESPACE: ManagementStoreGlobalNamespace =
export const MANAGEMENT_STORE_POLICY_LIST_NAMESPACE = 'policyList';
/** Namespace within the Management state where policy details state is maintained */
export const MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE = 'policyDetails';
/** Namespace within the Management state where endpoints state is maintained */
export const MANAGEMENT_STORE_ENDPOINTS_NAMESPACE = 'endpoints';

View file

@ -5,6 +5,8 @@
*/
import { generatePath } from 'react-router-dom';
// eslint-disable-next-line import/no-nodejs-modules
import querystring from 'querystring';
import {
MANAGEMENT_ROUTING_ENDPOINTS_PATH,
MANAGEMENT_ROUTING_POLICIES_PATH,
@ -13,7 +15,28 @@ import {
} from './constants';
import { ManagementSubTab } from '../types';
import { SiemPageName } from '../../app/types';
import { HostIndexUIQueryParams } from '../pages/endpoint_hosts/types';
// Taken from: https://github.com/microsoft/TypeScript/issues/12936#issuecomment-559034150
type ExactKeys<T1, T2> = Exclude<keyof T1, keyof T2> extends never ? T1 : never;
type Exact<T, Shape> = T extends Shape ? ExactKeys<T, Shape> : never;
/**
* Returns a string to be used in the URL as search query params.
* Ensures that when creating a URL query param string, that the given input strictly
* matches the expected interface (guards against possibly leaking internal state)
*/
const querystringStringify: <ExpectedType extends object, ArgType>(
params: Exact<ExpectedType, ArgType>
) => string = querystring.stringify;
/** Make `selected_host` required */
type EndpointDetailsUrlProps = Omit<HostIndexUIQueryParams, 'selected_host'> &
Required<Pick<HostIndexUIQueryParams, 'selected_host'>>;
/**
* Input props for the `getManagementUrl()` method
*/
export type GetManagementUrlProps = {
/**
* Exclude the URL prefix (everything to the left of where the router was mounted.
@ -22,8 +45,8 @@ export type GetManagementUrlProps = {
*/
excludePrefix?: boolean;
} & (
| { name: 'default' }
| { name: 'endpointList' }
| ({ name: 'default' | 'endpointList' } & HostIndexUIQueryParams)
| ({ name: 'endpointDetails' | 'endpointPolicyResponse' } & EndpointDetailsUrlProps)
| { name: 'policyList' }
| { name: 'policyDetails'; policyId: string }
);
@ -39,31 +62,47 @@ const URL_PREFIX = '#';
export const getManagementUrl = (props: GetManagementUrlProps): string => {
let url = props.excludePrefix ? '' : URL_PREFIX;
switch (props.name) {
case 'default':
url += generatePath(MANAGEMENT_ROUTING_ROOT_PATH, {
pageName: SiemPageName.management,
});
break;
case 'endpointList':
if (props.name === 'default' || props.name === 'endpointList') {
const { name, excludePrefix, ...queryParams } = props;
const urlQueryParams = querystringStringify<HostIndexUIQueryParams, typeof queryParams>(
queryParams
);
if (name === 'endpointList') {
url += generatePath(MANAGEMENT_ROUTING_ENDPOINTS_PATH, {
pageName: SiemPageName.management,
tabName: ManagementSubTab.endpoints,
});
break;
case 'policyList':
url += generatePath(MANAGEMENT_ROUTING_POLICIES_PATH, {
} else {
url += generatePath(MANAGEMENT_ROUTING_ROOT_PATH, {
pageName: SiemPageName.management,
tabName: ManagementSubTab.policies,
});
break;
case 'policyDetails':
url += generatePath(MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, {
pageName: SiemPageName.management,
tabName: ManagementSubTab.policies,
policyId: props.policyId,
});
break;
}
if (urlQueryParams) {
url += `?${urlQueryParams}`;
}
} else if (props.name === 'endpointDetails' || props.name === 'endpointPolicyResponse') {
const { name, excludePrefix, ...queryParams } = props;
queryParams.show = (props.name === 'endpointPolicyResponse'
? 'policy_response'
: '') as HostIndexUIQueryParams['show'];
url += `${generatePath(MANAGEMENT_ROUTING_ENDPOINTS_PATH, {
pageName: SiemPageName.management,
tabName: ManagementSubTab.endpoints,
})}?${querystringStringify<EndpointDetailsUrlProps, typeof queryParams>(queryParams)}`;
} else if (props.name === 'policyList') {
url += generatePath(MANAGEMENT_ROUTING_POLICIES_PATH, {
pageName: SiemPageName.management,
tabName: ManagementSubTab.policies,
});
} else if (props.name === 'policyDetails') {
url += generatePath(MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, {
pageName: SiemPageName.management,
tabName: ManagementSubTab.policies,
policyId: props.policyId,
});
}
return url;

View file

@ -13,7 +13,10 @@ import { getManagementUrl } from '..';
export const ManagementPageView = memo<Omit<PageViewProps, 'tabs'>>((options) => {
const { tabName } = useParams<{ tabName: ManagementSubTab }>();
const tabs = useMemo((): PageViewProps['tabs'] => {
const tabs = useMemo((): PageViewProps['tabs'] | undefined => {
if (options.viewType === 'details') {
return undefined;
}
return [
{
name: i18n.translate('xpack.securitySolution.managementTabs.endpoints', {

View file

@ -0,0 +1,25 @@
/*
* 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 { Switch, Route } from 'react-router-dom';
import React, { memo } from 'react';
import { HostList } from './view';
import { MANAGEMENT_ROUTING_ENDPOINTS_PATH } from '../../common/constants';
import { NotFoundPage } from '../../../app/404';
/**
* Provides the routing container for the endpoints related views
*/
export const EndpointsContainer = memo(() => {
return (
<Switch>
<Route path={MANAGEMENT_ROUTING_ENDPOINTS_PATH} exact component={HostList} />
<Route path="*" component={NotFoundPage} />
</Switch>
);
});
EndpointsContainer.displayName = 'EndpointsContainer';

View file

@ -4,8 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { HostResultList, HostInfo, GetHostPolicyResponse } from '../../../common/endpoint/types';
import { ServerApiError } from '../../common/types';
import {
HostResultList,
HostInfo,
GetHostPolicyResponse,
} from '../../../../../common/endpoint/types';
import { ServerApiError } from '../../../../common/types';
interface ServerReturnedHostList {
type: 'serverReturnedHostList';

View file

@ -8,10 +8,10 @@ import { CoreStart, HttpSetup } from 'kibana/public';
import { History, createBrowserHistory } from 'history';
import { applyMiddleware, Store, createStore } from 'redux';
import { coreMock } from '../../../../../../src/core/public/mocks';
import { coreMock } from '../../../../../../../../src/core/public/mocks';
import { HostResultList, AppLocation } from '../../../common/endpoint/types';
import { DepsStartMock, depsStartMock } from '../../common/mock/endpoint';
import { HostResultList, AppLocation } from '../../../../../common/endpoint/types';
import { DepsStartMock, depsStartMock } from '../../../../common/mock/endpoint';
import { hostMiddlewareFactory } from './middleware';
@ -20,8 +20,11 @@ import { hostListReducer } from './reducer';
import { uiQueryParams } from './selectors';
import { mockHostResultList } from './mock_host_result_list';
import { HostState, HostIndexUIQueryParams } from '../types';
import { MiddlewareActionSpyHelper, createSpyMiddleware } from '../../common/store/test_utils';
import { urlFromQueryParams } from '../view/url_from_query_params';
import {
MiddlewareActionSpyHelper,
createSpyMiddleware,
} from '../../../../common/store/test_utils';
import { getManagementUrl } from '../../..';
describe('host list pagination: ', () => {
let fakeCoreStart: jest.Mocked<CoreStart>;
@ -53,7 +56,9 @@ describe('host list pagination: ', () => {
queryParams = () => uiQueryParams(store.getState());
historyPush = (nextQueryParams: HostIndexUIQueryParams): void => {
return history.push(urlFromQueryParams(nextQueryParams));
return history.push(
getManagementUrl({ name: 'endpointList', excludePrefix: true, ...nextQueryParams })
);
};
});
@ -67,7 +72,7 @@ describe('host list pagination: ', () => {
type: 'userChangedUrl',
payload: {
...history.location,
pathname: '/endpoint-hosts',
pathname: getManagementUrl({ name: 'endpointList', excludePrefix: true }),
},
});
await waitForAction('serverReturnedHostList');

View file

@ -5,19 +5,23 @@
*/
import { CoreStart, HttpSetup } from 'kibana/public';
import { applyMiddleware, createStore, Store } from 'redux';
import { coreMock } from '../../../../../../src/core/public/mocks';
import { coreMock } from '../../../../../../../../src/core/public/mocks';
import { History, createBrowserHistory } from 'history';
import { DepsStartMock, depsStartMock } from '../../common/mock/endpoint';
import { DepsStartMock, depsStartMock } from '../../../../common/mock/endpoint';
import { createSpyMiddleware, MiddlewareActionSpyHelper } from '../../common/store/test_utils';
import { Immutable, HostResultList } from '../../../common/endpoint/types';
import { AppAction } from '../../common/store/actions';
import {
createSpyMiddleware,
MiddlewareActionSpyHelper,
} from '../../../../common/store/test_utils';
import { Immutable, HostResultList } from '../../../../../common/endpoint/types';
import { AppAction } from '../../../../common/store/actions';
import { mockHostResultList } from './mock_host_result_list';
import { listData } from './selectors';
import { HostState } from '../types';
import { hostListReducer } from './reducer';
import { hostMiddlewareFactory } from './middleware';
import { getManagementUrl } from '../../..';
describe('host list middleware', () => {
let fakeCoreStart: jest.Mocked<CoreStart>;
@ -56,7 +60,7 @@ describe('host list middleware', () => {
type: 'userChangedUrl',
payload: {
...history.location,
pathname: '/endpoint-hosts',
pathname: getManagementUrl({ name: 'endpointList', excludePrefix: true }),
},
});
await waitForAction('serverReturnedHostList');

View file

@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { HostResultList } from '../../../common/endpoint/types';
import { ImmutableMiddlewareFactory } from '../../common/store';
import { HostResultList } from '../../../../../common/endpoint/types';
import { ImmutableMiddlewareFactory } from '../../../../common/store';
import { isOnHostPage, hasSelectedHost, uiQueryParams, listData } from './selectors';
import { HostState } from '../types';
@ -37,7 +37,7 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory<HostState> = (cor
});
}
}
if (action.type === 'userChangedUrl' && hasSelectedHost(state) !== false) {
if (action.type === 'userChangedUrl' && hasSelectedHost(state) === true) {
// If user navigated directly to a host details page, load the host list
if (listData(state).length === 0) {
const { page_index: pageIndex, page_size: pageSize } = uiQueryParams(state);

View file

@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { HostInfo, HostResultList, HostStatus } from '../../../common/endpoint/types';
import { EndpointDocGenerator } from '../../../common/endpoint/generate_data';
import { HostInfo, HostResultList, HostStatus } from '../../../../../common/endpoint/types';
import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data';
export const mockHostResultList: (options?: {
total?: number;

View file

@ -6,9 +6,9 @@
import { isOnHostPage, hasSelectedHost } from './selectors';
import { HostState } from '../types';
import { AppAction } from '../../common/store/actions';
import { ImmutableReducer } from '../../common/store';
import { Immutable } from '../../../common/endpoint/types';
import { AppAction } from '../../../../common/store/actions';
import { ImmutableReducer } from '../../../../common/store';
import { Immutable } from '../../../../../common/endpoint/types';
export const initialHostListState: Immutable<HostState> = {
hosts: [],

View file

@ -7,13 +7,15 @@
// eslint-disable-next-line import/no-nodejs-modules
import querystring from 'querystring';
import { createSelector } from 'reselect';
import { matchPath } from 'react-router-dom';
import {
Immutable,
HostPolicyResponseAppliedAction,
HostPolicyResponseConfiguration,
HostPolicyResponseActionStatus,
} from '../../../common/endpoint/types';
} from '../../../../../common/endpoint/types';
import { HostState, HostIndexUIQueryParams } from '../types';
import { MANAGEMENT_ROUTING_ENDPOINTS_PATH } from '../../../common/constants';
const PAGE_SIZES = Object.freeze([10, 20, 50]);
@ -96,8 +98,14 @@ export const policyResponseLoading = (state: Immutable<HostState>): boolean =>
export const policyResponseError = (state: Immutable<HostState>) => state.policyResponseError;
export const isOnHostPage = (state: Immutable<HostState>) =>
state.location ? state.location.pathname === '/endpoint-hosts' : false;
export const isOnHostPage = (state: Immutable<HostState>) => {
return (
matchPath(state.location?.pathname ?? '', {
path: MANAGEMENT_ROUTING_ENDPOINTS_PATH,
exact: true,
}) !== null
);
};
export const uiQueryParams: (
state: Immutable<HostState>
@ -117,11 +125,21 @@ export const uiQueryParams: (
];
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];
const value: string | undefined =
typeof query[key] === 'string'
? (query[key] as string)
: Array.isArray(query[key])
? (query[key][query[key].length - 1] as string)
: undefined;
if (value !== undefined) {
if (key === 'show') {
if (value === 'policy_response' || value === 'details') {
data[key] = value;
}
} else {
data[key] = value;
}
}
}

View file

@ -10,8 +10,8 @@ import {
HostMetadata,
HostPolicyResponse,
AppLocation,
} from '../../common/endpoint/types';
import { ServerApiError } from '../common/types';
} from '../../../../common/endpoint/types';
import { ServerApiError } from '../../../common/types';
export interface HostState {
/** list of host **/
@ -53,5 +53,5 @@ export interface HostIndexUIQueryParams {
/** Which page to show */
page_index?: string;
/** show the policy response or host details */
show?: string;
show?: 'policy_response' | 'details';
}

View file

@ -16,14 +16,14 @@ import {
import React, { memo, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { HostMetadata } from '../../../../common/endpoint/types';
import { HostMetadata } from '../../../../../../common/endpoint/types';
import { useHostSelector, useHostLogsUrl } from '../hooks';
import { urlFromQueryParams } from '../url_from_query_params';
import { policyResponseStatus, uiQueryParams } from '../../store/selectors';
import { POLICY_STATUS_TO_HEALTH_COLOR } from '../host_constants';
import { FormattedDateAndTime } from '../../../common/components/endpoint/formatted_date_time';
import { useNavigateByRouterEventHandler } from '../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
import { LinkToApp } from '../../../common/components/endpoint/link_to_app';
import { FormattedDateAndTime } from '../../../../../common/components/endpoint/formatted_date_time';
import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
import { LinkToApp } from '../../../../../common/components/endpoint/link_to_app';
import { getManagementUrl } from '../../../..';
const HostIds = styled(EuiListGroupItem)`
margin-top: 0;
@ -61,14 +61,24 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => {
];
}, [details]);
const policyResponseUri = useMemo(() => {
return urlFromQueryParams({
...queryParams,
selected_host: details.host.id,
show: 'policy_response',
});
const [policyResponseUri, policyResponseRoutePath] = useMemo(() => {
const { selected_host, show, ...currentUrlParams } = queryParams;
return [
getManagementUrl({
name: 'endpointPolicyResponse',
...currentUrlParams,
selected_host: details.host.id,
}),
getManagementUrl({
name: 'endpointPolicyResponse',
excludePrefix: true,
...currentUrlParams,
selected_host: details.host.id,
}),
];
}, [details.host.id, queryParams]);
const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseUri);
const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseRoutePath);
const detailsResultsLower = useMemo(() => {
return [
@ -90,7 +100,7 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => {
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
<EuiLink
data-test-subj="policyStatusValue"
href={`?${policyResponseUri.search}`}
href={policyResponseUri}
onClick={policyStatusClickHandler}
>
<FormattedMessage

View file

@ -18,7 +18,7 @@ import {
import { useHistory } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
import { useHostSelector } from '../hooks';
import { urlFromQueryParams } from '../url_from_query_params';
import {
@ -35,9 +35,10 @@ import {
} from '../../store/selectors';
import { HostDetails } from './host_details';
import { PolicyResponse } from './policy_response';
import { HostMetadata } from '../../../../common/endpoint/types';
import { HostMetadata } from '../../../../../../common/endpoint/types';
import { FlyoutSubHeader, FlyoutSubHeaderProps } from './components/flyout_sub_header';
import { useNavigateByRouterEventHandler } from '../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
import { getManagementUrl } from '../../../..';
export const HostDetailsFlyout = memo(() => {
const history = useHistory();
@ -115,24 +116,32 @@ const PolicyResponseFlyoutPanel = memo<{
const responseAttentionCount = useHostSelector(policyResponseFailedOrWarningActionCount);
const loading = useHostSelector(policyResponseLoading);
const error = useHostSelector(policyResponseError);
const detailsUri = useMemo(
() =>
urlFromQueryParams({
const [detailsUri, detailsRoutePath] = useMemo(
() => [
getManagementUrl({
name: 'endpointList',
...queryParams,
selected_host: hostMeta.host.id,
}),
getManagementUrl({
name: 'endpointList',
excludePrefix: true,
...queryParams,
selected_host: hostMeta.host.id,
}),
],
[hostMeta.host.id, queryParams]
);
const backToDetailsClickHandler = useNavigateByRouterEventHandler(detailsUri);
const backToDetailsClickHandler = useNavigateByRouterEventHandler(detailsRoutePath);
const backButtonProp = useMemo((): FlyoutSubHeaderProps['backButton'] => {
return {
title: i18n.translate('xpack.securitySolution.endpoint.host.policyResponse.backLinkTitle', {
defaultMessage: 'Endpoint Details',
}),
href: `?${detailsUri.search}`,
href: detailsUri,
onClick: backToDetailsClickHandler,
};
}, [backToDetailsClickHandler, detailsUri.search]);
}, [backToDetailsClickHandler, detailsUri]);
return (
<>

View file

@ -19,7 +19,7 @@ import {
Immutable,
HostPolicyResponseAppliedAction,
HostPolicyResponseConfiguration,
} from '../../../../common/endpoint/types';
} from '../../../../../../common/endpoint/types';
/**
* Nested accordion in the policy response detailing any concerned

View file

@ -7,12 +7,18 @@
import { useSelector } from 'react-redux';
import { useMemo } from 'react';
import { HostState } from '../types';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
import { State } from '../../common/store/types';
import {
MANAGEMENT_STORE_ENDPOINTS_NAMESPACE,
MANAGEMENT_STORE_GLOBAL_NAMESPACE,
} from '../../../common/constants';
import { useKibana } from '../../../../common/lib/kibana';
import { State } from '../../../../common/store';
export function useHostSelector<TSelected>(selector: (state: HostState) => TSelected) {
return useSelector(function (state: State) {
return selector(state.hostList as HostState);
return selector(
state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_ENDPOINTS_NAMESPACE] as HostState
);
});
}

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { HostStatus, HostPolicyResponseActionStatus } from '../../../common/endpoint/types';
import { HostStatus, HostPolicyResponseActionStatus } from '../../../../../common/endpoint/types';
export const HOST_STATUS_TO_HEALTH_COLOR = Object.freeze<
{

View file

@ -9,14 +9,14 @@ import * as reactTestingLibrary from '@testing-library/react';
import { HostList } from './index';
import { mockHostDetailsApiResult, mockHostResultList } from '../store/mock_host_result_list';
import { AppContextTestRender, createAppRootMockRenderer } from '../../common/mock/endpoint';
import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint';
import {
HostInfo,
HostStatus,
HostPolicyResponseActionStatus,
} from '../../../common/endpoint/types';
import { EndpointDocGenerator } from '../../../common/endpoint/generate_data';
import { AppAction } from '../../common/store/actions';
} from '../../../../../common/endpoint/types';
import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data';
import { AppAction } from '../../../../common/store/actions';
describe('when on the hosts page', () => {
const docGenerator = new EndpointDocGenerator();
@ -202,7 +202,7 @@ describe('when on the hosts page', () => {
const policyStatusLink = await renderResult.findByTestId('policyStatusValue');
expect(policyStatusLink).not.toBeNull();
expect(policyStatusLink.getAttribute('href')).toEqual(
'?page_index=0&page_size=10&selected_host=1&show=policy_response'
'#/management/endpoints?page_index=0&page_size=10&selected_host=1&show=policy_response'
);
});
it('should update the URL when policy status link is clicked', async () => {
@ -381,7 +381,7 @@ describe('when on the hosts page', () => {
const subHeaderBackLink = await renderResult.findByTestId('flyoutSubHeaderBackButton');
expect(subHeaderBackLink.textContent).toBe('Endpoint Details');
expect(subHeaderBackLink.getAttribute('href')).toBe(
'?page_index=0&page_size=10&selected_host=1'
'#/management/endpoints?page_index=0&page_size=10&selected_host=1'
);
});
it('should update URL when back to details link is clicked', async () => {

View file

@ -22,17 +22,19 @@ import { createStructuredSelector } from 'reselect';
import { HostDetailsFlyout } from './details';
import * as selectors from '../store/selectors';
import { useHostSelector } from './hooks';
import { urlFromQueryParams } from './url_from_query_params';
import { HOST_STATUS_TO_HEALTH_COLOR } from './host_constants';
import { useNavigateByRouterEventHandler } from '../../common/hooks/endpoint/use_navigate_by_router_event_handler';
import { CreateStructuredSelector } from '../../common/store';
import { Immutable, HostInfo } from '../../../common/endpoint/types';
import { PageView } from '../../common/components/endpoint/page_view';
import { useNavigateByRouterEventHandler } from '../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
import { CreateStructuredSelector } from '../../../../common/store';
import { Immutable, HostInfo } from '../../../../../common/endpoint/types';
import { SpyRoute } from '../../../../common/utils/route/spy_routes';
import { ManagementPageView } from '../../../components/management_page_view';
import { getManagementUrl } from '../../..';
import { FormattedDate } from '../../../../common/components/formatted_date';
const HostLink = memo<{
name: string;
href: string;
route: ReturnType<typeof urlFromQueryParams>;
route: string;
}>(({ name, href, route }) => {
const clickHandler = useNavigateByRouterEventHandler(route);
@ -77,8 +79,11 @@ export const HostList = () => {
const onTableChange = useCallback(
({ page }: { page: { index: number; size: number } }) => {
const { index, size } = page;
// FIXME: PT: if host details is open, table is not displaying correct number of rows
history.push(
urlFromQueryParams({
getManagementUrl({
name: 'endpointList',
excludePrefix: true,
...queryParams,
page_index: JSON.stringify(index),
page_size: JSON.stringify(size),
@ -89,22 +94,34 @@ export const HostList = () => {
);
const columns: Array<EuiBasicTableColumn<Immutable<HostInfo>>> = useMemo(() => {
const lastActiveColumnName = i18n.translate('xpack.securitySolution.endpointList.lastActive', {
defaultMessage: 'Last Active',
});
return [
{
field: 'metadata.host',
name: i18n.translate('xpack.securitySolution.endpoint.host.list.hostname', {
name: i18n.translate('xpack.securitySolution.endpointList.hostname', {
defaultMessage: 'Hostname',
}),
render: ({ hostname, id }: HostInfo['metadata']['host']) => {
const newQueryParams = urlFromQueryParams({ ...queryParams, selected_host: id });
return (
<HostLink name={hostname} href={`?${newQueryParams.search}`} route={newQueryParams} />
);
const toRoutePath = getManagementUrl({
...queryParams,
name: 'endpointDetails',
selected_host: id,
excludePrefix: true,
});
const toRouteUrl = getManagementUrl({
...queryParams,
name: 'endpointDetails',
selected_host: id,
});
return <HostLink name={hostname} href={toRouteUrl} route={toRoutePath} />;
},
},
{
field: 'host_status',
name: i18n.translate('xpack.securitySolution.endpoint.host.list.hostStatus', {
name: i18n.translate('xpack.securitySolution.endpointList.hostStatus', {
defaultMessage: 'Host Status',
}),
// eslint-disable-next-line react/display-name
@ -116,7 +133,7 @@ export const HostList = () => {
className="eui-textTruncate"
>
<FormattedMessage
id="xpack.securitySolution.endpoint.host.list.hostStatusValue"
id="xpack.securitySolution.endpointList.hostStatusValue"
defaultMessage="{hostStatus, select, online {Online} error {Error} other {Offline}}"
values={{ hostStatus }}
/>
@ -126,7 +143,7 @@ export const HostList = () => {
},
{
field: '',
name: i18n.translate('xpack.securitySolution.endpoint.host.list.policy', {
name: i18n.translate('xpack.securitySolution.endpointList.policy', {
defaultMessage: 'Policy',
}),
truncateText: true,
@ -137,7 +154,7 @@ export const HostList = () => {
},
{
field: '',
name: i18n.translate('xpack.securitySolution.endpoint.host.list.policyStatus', {
name: i18n.translate('xpack.securitySolution.endpointList.policyStatus', {
defaultMessage: 'Policy Status',
}),
// eslint-disable-next-line react/display-name
@ -145,7 +162,7 @@ export const HostList = () => {
return (
<EuiHealth color="success" className="eui-textTruncate">
<FormattedMessage
id="xpack.securitySolution.endpoint.host.list.policyStatus"
id="xpack.securitySolution.endpointList.policyStatus"
defaultMessage="Policy Status"
/>
</EuiHealth>
@ -154,7 +171,7 @@ export const HostList = () => {
},
{
field: '',
name: i18n.translate('xpack.securitySolution.endpoint.host.list.alerts', {
name: i18n.translate('xpack.securitySolution.endpointList.alerts', {
defaultMessage: 'Alerts',
}),
dataType: 'number',
@ -164,14 +181,14 @@ export const HostList = () => {
},
{
field: 'metadata.host.os.name',
name: i18n.translate('xpack.securitySolution.endpoint.host.list.os', {
name: i18n.translate('xpack.securitySolution.endpointList.os', {
defaultMessage: 'Operating System',
}),
truncateText: true,
},
{
field: 'metadata.host.ip',
name: i18n.translate('xpack.securitySolution.endpoint.host.list.ip', {
name: i18n.translate('xpack.securitySolution.endpointList.ip', {
defaultMessage: 'IP Address',
}),
// eslint-disable-next-line react/display-name
@ -189,35 +206,38 @@ export const HostList = () => {
},
{
field: 'metadata.agent.version',
name: i18n.translate('xpack.securitySolution.endpoint.host.list.endpointVersion', {
name: i18n.translate('xpack.securitySolution.endpointList.endpointVersion', {
defaultMessage: 'Version',
}),
},
{
field: '',
name: i18n.translate('xpack.securitySolution.endpoint.host.list.lastActive', {
defaultMessage: 'Last Active',
}),
dataType: 'date',
render: () => {
return 'xxxx';
field: 'metadata.@timestamp',
name: lastActiveColumnName,
render(dateValue: HostInfo['metadata']['@timestamp']) {
return (
<FormattedDate
fieldName={lastActiveColumnName}
value={dateValue}
className="eui-textTruncate"
/>
);
},
},
];
}, [queryParams]);
return (
<PageView
<ManagementPageView
viewType="list"
data-test-subj="hostPage"
headerLeft={i18n.translate('xpack.securitySolution.endpoint.host.hosts', {
defaultMessage: 'Hosts',
headerLeft={i18n.translate('xpack.securitySolution.endpointLis.pageTitle', {
defaultMessage: 'Endpoints',
})}
>
{hasSelectedHost && <HostDetailsFlyout />}
<EuiText color="subdued" size="xs" data-test-subj="hostListTableTotal">
<FormattedMessage
id="xpack.securitySolution.endpoint.host.list.totalCount"
id="xpack.securitySolution.endpointList.totalCount"
defaultMessage="{totalItemCount, plural, one {# Host} other {# Hosts}}"
values={{ totalItemCount }}
/>
@ -232,6 +252,7 @@ export const HostList = () => {
pagination={paginationSetup}
onChange={onTableChange}
/>
</PageView>
<SpyRoute />
</ManagementPageView>
);
};

View file

@ -8,7 +8,7 @@
import querystring from 'querystring';
import { HostIndexUIQueryParams } from '../types';
import { AppLocation } from '../../../common/endpoint/types';
import { AppLocation } from '../../../../../common/endpoint/types';
export function urlFromQueryParams(queryParams: HostIndexUIQueryParams): Partial<AppLocation> {
const search = querystring.stringify(queryParams);

View file

@ -6,34 +6,27 @@
import React, { memo } from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import { SpyRoute } from '../../common/utils/route/spy_routes';
import { PolicyContainer } from './policy';
import {
MANAGEMENT_ROUTING_ENDPOINTS_PATH,
MANAGEMENT_ROUTING_POLICIES_PATH,
MANAGEMENT_ROUTING_ROOT_PATH,
} from '../common/constants';
import { ManagementPageView } from '../components/management_page_view';
import { NotFoundPage } from '../../app/404';
const TmpEndpoints = () => {
return (
<ManagementPageView viewType="list" headerLeft="Test">
<h1>{'Endpoints will go here'}</h1>
<SpyRoute />
</ManagementPageView>
);
};
import { EndpointsContainer } from './endpoint_hosts';
import { getManagementUrl } from '..';
export const ManagementContainer = memo(() => {
return (
<Switch>
<Route path={MANAGEMENT_ROUTING_ENDPOINTS_PATH} exact component={TmpEndpoints} />
<Route path={MANAGEMENT_ROUTING_ENDPOINTS_PATH} component={EndpointsContainer} />
<Route path={MANAGEMENT_ROUTING_POLICIES_PATH} component={PolicyContainer} />
<Route
path={MANAGEMENT_ROUTING_ROOT_PATH}
exact
render={() => <Redirect to="/management/endpoints" />}
render={() => (
<Redirect to={getManagementUrl({ name: 'endpointList', excludePrefix: true })} />
)}
/>
<Route path="*" component={NotFoundPage} />
</Switch>

View file

@ -11,16 +11,34 @@ import {
} from '../../common/store';
import { policyListMiddlewareFactory } from '../pages/policy/store/policy_list';
import { policyDetailsMiddlewareFactory } from '../pages/policy/store/policy_details';
import {
MANAGEMENT_STORE_ENDPOINTS_NAMESPACE,
MANAGEMENT_STORE_GLOBAL_NAMESPACE,
MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE,
MANAGEMENT_STORE_POLICY_LIST_NAMESPACE,
} from '../common/constants';
import { hostMiddlewareFactory } from '../pages/endpoint_hosts/store/middleware';
const policyListSelector = (state: State) =>
state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_POLICY_LIST_NAMESPACE];
const policyDetailsSelector = (state: State) =>
state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE];
const endpointsSelector = (state: State) =>
state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_ENDPOINTS_NAMESPACE];
export const managementMiddlewareFactory: SecuritySubPluginMiddlewareFactory = (
coreStart,
depsStart
) => {
const listSelector = (state: State) => state.management.policyList;
const detailSelector = (state: State) => state.management.policyDetails;
return [
substateMiddlewareFactory(listSelector, policyListMiddlewareFactory(coreStart, depsStart)),
substateMiddlewareFactory(detailSelector, policyDetailsMiddlewareFactory(coreStart, depsStart)),
substateMiddlewareFactory(
policyListSelector,
policyListMiddlewareFactory(coreStart, depsStart)
),
substateMiddlewareFactory(
policyDetailsSelector,
policyDetailsMiddlewareFactory(coreStart, depsStart)
),
substateMiddlewareFactory(endpointsSelector, hostMiddlewareFactory(coreStart, depsStart)),
];
};

View file

@ -14,18 +14,24 @@ import {
initialPolicyListState,
} from '../pages/policy/store/policy_list/reducer';
import {
MANAGEMENT_STORE_ENDPOINTS_NAMESPACE,
MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE,
MANAGEMENT_STORE_POLICY_LIST_NAMESPACE,
} from '../common/constants';
import { ImmutableCombineReducers } from '../../common/store';
import { Immutable } from '../../../common/endpoint/types';
import { ManagementState } from '../types';
import { hostListReducer, initialHostListState } from '../pages/endpoint_hosts/store/reducer';
const immutableCombineReducers: ImmutableCombineReducers = combineReducers;
/**
* Returns the initial state of the store for the SIEM Management section
*/
export const mockManagementState: Immutable<ManagementState> = {
policyList: initialPolicyListState(),
policyDetails: initialPolicyDetailsState(),
[MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: initialPolicyListState(),
[MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: initialPolicyDetailsState(),
[MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: initialHostListState,
};
/**
@ -34,4 +40,6 @@ export const mockManagementState: Immutable<ManagementState> = {
export const managementReducer = immutableCombineReducers({
[MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: policyListReducer,
[MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: policyDetailsReducer,
// @ts-ignore
[MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: hostListReducer,
});

View file

@ -7,6 +7,7 @@
import { CombinedState } from 'redux';
import { SiemPageName } from '../app/types';
import { PolicyListState, PolicyDetailsState } from './pages/policy/types';
import { HostState } from './pages/endpoint_hosts/types';
/**
* The type for the management store global namespace. Used mostly internally to reference
@ -17,6 +18,7 @@ export type ManagementStoreGlobalNamespace = 'management';
export type ManagementState = CombinedState<{
policyList: PolicyListState;
policyDetails: PolicyDetailsState;
endpoints: HostState;
}>;
/**

View file

@ -65,7 +65,6 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
const overviewSubPlugin = new (await import('./overview')).Overview();
const timelinesSubPlugin = new (await import('./timelines')).Timelines();
const endpointAlertsSubPlugin = new (await import('./endpoint_alerts')).EndpointAlerts();
const endpointHostsSubPlugin = new (await import('./endpoint_hosts')).EndpointHosts();
const managementSubPlugin = new (await import('./management')).Management();
const alertsStart = alertsSubPlugin.start();
@ -75,7 +74,6 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
const overviewStart = overviewSubPlugin.start();
const timelinesStart = timelinesSubPlugin.start();
const endpointAlertsStart = endpointAlertsSubPlugin.start(coreStart, startPlugins);
const endpointHostsStart = endpointHostsSubPlugin.start(coreStart, startPlugins);
const managementSubPluginStart = managementSubPlugin.start(coreStart, startPlugins);
return renderApp(services, params, {
@ -87,7 +85,6 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
...overviewStart.routes,
...timelinesStart.routes,
...endpointAlertsStart.routes,
...endpointHostsStart.routes,
...managementSubPluginStart.routes,
],
store: {
@ -96,7 +93,6 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
...networkStart.store.initialState,
...timelinesStart.store.initialState,
...endpointAlertsStart.store.initialState,
...endpointHostsStart.store.initialState,
...managementSubPluginStart.store.initialState,
},
reducer: {
@ -104,12 +100,10 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
...networkStart.store.reducer,
...timelinesStart.store.reducer,
...endpointAlertsStart.store.reducer,
...endpointHostsStart.store.reducer,
...managementSubPluginStart.store.reducer,
},
middlewares: [
...(endpointAlertsStart.store.middleware ?? []),
...(endpointHostsStart.store.middleware ?? []),
...(managementSubPluginStart.store.middleware ?? []),
],
},