[SIEMDPOINT][WIP] Add Management section and move Policy related views (#67417)

* Add Management top-level nav tab item
* Move of Policy related views to `management`
* Enhance PageView component to support sub-tabs
This commit is contained in:
Paul Tavares 2020-05-29 10:12:51 -04:00 committed by GitHub
parent 0712741bb3
commit ae724f1035
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
67 changed files with 815 additions and 363 deletions

View file

@ -14,6 +14,7 @@ import {
} from '../../common/components/link_to';
import * as i18n from './translations';
import { SiemPageName, SiemNavTab } from '../types';
import { getManagementUrl } from '../../management';
export const navTabs: SiemNavTab = {
[SiemPageName.overview]: {
@ -58,4 +59,11 @@ export const navTabs: SiemNavTab = {
disabled: false,
urlKey: 'case',
},
[SiemPageName.management]: {
id: SiemPageName.management,
name: i18n.MANAGEMENT,
href: getManagementUrl({ name: 'default' }),
disabled: false,
urlKey: SiemPageName.management,
},
};

View file

@ -29,3 +29,7 @@ export const TIMELINES = i18n.translate('xpack.siem.navigation.timelines', {
export const CASE = i18n.translate('xpack.siem.navigation.case', {
defaultMessage: 'Cases',
});
export const MANAGEMENT = i18n.translate('xpack.siem.navigation.management', {
defaultMessage: 'Management',
});

View file

@ -5,7 +5,6 @@
*/
import { Reducer, AnyAction, Middleware, Dispatch } from 'redux';
import { NavTab } from '../common/components/navigation/types';
import { HostsState } from '../hosts/store';
import { NetworkState } from '../network/store';
@ -15,7 +14,7 @@ import { Immutable } from '../../common/endpoint/types';
import { AlertListState } from '../../common/endpoint_alerts/types';
import { AppAction } from '../common/store/actions';
import { HostState } from '../endpoint_hosts/types';
import { PolicyDetailsState, PolicyListState } from '../endpoint_policy/types';
import { ManagementState } from '../management/store/types';
export enum SiemPageName {
overview = 'overview',
@ -24,6 +23,7 @@ export enum SiemPageName {
detections = 'detections',
timelines = 'timelines',
case = 'case',
management = 'management',
}
export type SiemNavTabKey =
@ -32,14 +32,15 @@ export type SiemNavTabKey =
| SiemPageName.network
| SiemPageName.detections
| SiemPageName.timelines
| SiemPageName.case;
| SiemPageName.case
| SiemPageName.management;
export type SiemNavTab = Record<SiemNavTabKey, NavTab>;
export interface SecuritySubPluginStore<K extends SecuritySubPluginKeyStore, T> {
initialState: Record<K, T>;
reducer: Record<K, Reducer<T, AnyAction>>;
middleware?: Middleware<{}, State, Dispatch<AppAction | Immutable<AppAction>>>;
middleware?: Array<Middleware<{}, State, Dispatch<AppAction | Immutable<AppAction>>>>;
}
export interface SecuritySubPlugin {
@ -52,8 +53,7 @@ type SecuritySubPluginKeyStore =
| 'timeline'
| 'hostList'
| 'alertList'
| 'policyDetails'
| 'policyList';
| 'management';
export interface SecuritySubPluginWithStore<K extends SecuritySubPluginKeyStore, T>
extends SecuritySubPlugin {
store: SecuritySubPluginStore<K, T>;
@ -67,8 +67,7 @@ export interface SecuritySubPlugins extends SecuritySubPlugin {
timeline: TimelineState;
alertList: Immutable<AlertListState>;
hostList: Immutable<HostState>;
policyDetails: Immutable<PolicyDetailsState>;
policyList: Immutable<PolicyListState>;
management: ManagementState;
};
reducer: {
hosts: Reducer<HostsState, AnyAction>;
@ -76,8 +75,7 @@ export interface SecuritySubPlugins extends SecuritySubPlugin {
timeline: Reducer<TimelineState, AnyAction>;
alertList: ImmutableReducer<AlertListState, AppAction>;
hostList: ImmutableReducer<HostState, AppAction>;
policyDetails: ImmutableReducer<PolicyDetailsState, AppAction>;
policyList: ImmutableReducer<PolicyListState, AppAction>;
management: ImmutableReducer<ManagementState, AppAction>;
};
middlewares: Array<Middleware<{}, State, Dispatch<AppAction | Immutable<AppAction>>>>;
};

View file

@ -21,6 +21,10 @@ exports[`PageView component should display body header custom element 1`] = `
background: none;
}
.c0 .endpoint-navTabs {
margin-left: 24px;
}
<PageView
bodyHeader={
<p>
@ -112,6 +116,10 @@ exports[`PageView component should display body header wrapped in EuiTitle 1`] =
background: none;
}
.c0 .endpoint-navTabs {
margin-left: 24px;
}
<PageView
bodyHeader="body header"
viewType="list"
@ -206,6 +214,10 @@ exports[`PageView component should display header left and right 1`] = `
background: none;
}
.c0 .endpoint-navTabs {
margin-left: 24px;
}
<PageView
headerLeft="page title"
headerRight="right side actions"
@ -315,6 +327,10 @@ exports[`PageView component should display only body if not header props used 1`
background: none;
}
.c0 .endpoint-navTabs {
margin-left: 24px;
}
<PageView
viewType="list"
>
@ -383,6 +399,10 @@ exports[`PageView component should display only header left 1`] = `
background: none;
}
.c0 .endpoint-navTabs {
margin-left: 24px;
}
<PageView
headerLeft="page title"
viewType="list"
@ -481,6 +501,10 @@ exports[`PageView component should display only header right but include an empt
background: none;
}
.c0 .endpoint-navTabs {
margin-left: 24px;
}
<PageView
headerRight="right side actions"
viewType="list"
@ -576,6 +600,10 @@ exports[`PageView component should pass through EuiPage props 1`] = `
background: none;
}
.c0 .endpoint-navTabs {
margin-left: 24px;
}
<PageView
aria-label="test-aria-label-here"
className="test-class-name-here"
@ -661,6 +689,10 @@ exports[`PageView component should use custom element for header left and not wr
background: none;
}
.c0 .endpoint-navTabs {
margin-left: 24px;
}
<PageView
headerLeft={
<p>

View file

@ -14,10 +14,13 @@ import {
EuiPageHeader,
EuiPageHeaderSection,
EuiPageProps,
EuiTab,
EuiTabs,
EuiTitle,
} from '@elastic/eui';
import React, { memo, ReactNode } from 'react';
import React, { memo, MouseEventHandler, ReactNode, useMemo } from 'react';
import styled from 'styled-components';
import { EuiTabProps } from '@elastic/eui/src/components/tabs/tab';
const StyledEuiPage = styled(EuiPage)`
&.endpoint--isListView {
@ -39,6 +42,9 @@ const StyledEuiPage = styled(EuiPage)`
background: none;
}
}
.endpoint-navTabs {
margin-left: ${(props) => props.theme.eui.euiSizeL};
}
`;
const isStringOrNumber = /(string|number)/;
@ -74,69 +80,94 @@ export const PageViewBodyHeaderTitle = memo<{ children: ReactNode }>(
);
PageViewBodyHeaderTitle.displayName = 'PageViewBodyHeaderTitle';
export type PageViewProps = EuiPageProps & {
/**
* The type of view
*/
viewType: 'list' | 'details';
/**
* content to be placed on the left side of the header. If a `string` is used, then it will
* be wrapped with `<EuiTitle><h1></h1></EuiTitle>`, else it will just be used as is.
*/
headerLeft?: ReactNode;
/** Content for the right side of the header */
headerRight?: ReactNode;
/**
* body (sub-)header section. If a `string` is used, then it will be wrapped with
* `<EuiTitle><h2></h2></EuiTitle>`
*/
bodyHeader?: ReactNode;
/**
* The list of tab navigation items
*/
tabs?: Array<
EuiTabProps & {
name: ReactNode;
id: string;
href?: string;
onClick?: MouseEventHandler<HTMLAnchorElement | HTMLButtonElement>;
}
>;
children?: ReactNode;
};
/**
* Page View layout for use in Endpoint
*/
export const PageView = memo<
EuiPageProps & {
/**
* The type of view
*/
viewType: 'list' | 'details';
/**
* content to be placed on the left side of the header. If a `string` is used, then it will
* be wrapped with `<EuiTitle><h1></h1></EuiTitle>`, else it will just be used as is.
*/
headerLeft?: ReactNode;
/** Content for the right side of the header */
headerRight?: ReactNode;
/**
* body (sub-)header section. If a `string` is used, then it will be wrapped with
* `<EuiTitle><h2></h2></EuiTitle>`
*/
bodyHeader?: ReactNode;
children?: ReactNode;
}
>(({ viewType, children, headerLeft, headerRight, bodyHeader, ...otherProps }) => {
return (
<StyledEuiPage
className={(viewType === 'list' && 'endpoint--isListView') || 'endpoint--isDetailsView'}
{...otherProps}
>
<EuiPageBody>
{(headerLeft || headerRight) && (
<EuiPageHeader className="endpoint-header">
<EuiPageHeaderSection data-test-subj="pageViewHeaderLeft">
{isStringOrNumber.test(typeof headerLeft) ? (
<PageViewHeaderTitle>{headerLeft}</PageViewHeaderTitle>
) : (
headerLeft
)}
</EuiPageHeaderSection>
{headerRight && (
<EuiPageHeaderSection data-test-subj="pageViewHeaderRight">
{headerRight}
</EuiPageHeaderSection>
)}
</EuiPageHeader>
)}
<EuiPageContent className="endpoint-page-content">
{bodyHeader && (
<EuiPageContentHeader>
<EuiPageContentHeaderSection data-test-subj="pageViewBodyTitleArea">
{isStringOrNumber.test(typeof bodyHeader) ? (
<PageViewBodyHeaderTitle>{bodyHeader}</PageViewBodyHeaderTitle>
export const PageView = memo<PageViewProps>(
({ viewType, children, headerLeft, headerRight, bodyHeader, tabs, ...otherProps }) => {
const tabComponents = useMemo(() => {
if (!tabs) {
return [];
}
return tabs.map(({ name, id, ...otherEuiTabProps }) => (
<EuiTab {...otherEuiTabProps} key={id}>
{name}
</EuiTab>
));
}, [tabs]);
return (
<StyledEuiPage
className={(viewType === 'list' && 'endpoint--isListView') || 'endpoint--isDetailsView'}
{...otherProps}
>
<EuiPageBody>
{(headerLeft || headerRight) && (
<EuiPageHeader className="endpoint-header">
<EuiPageHeaderSection data-test-subj="pageViewHeaderLeft">
{isStringOrNumber.test(typeof headerLeft) ? (
<PageViewHeaderTitle>{headerLeft}</PageViewHeaderTitle>
) : (
bodyHeader
headerLeft
)}
</EuiPageContentHeaderSection>
</EuiPageContentHeader>
</EuiPageHeaderSection>
{headerRight && (
<EuiPageHeaderSection data-test-subj="pageViewHeaderRight">
{headerRight}
</EuiPageHeaderSection>
)}
</EuiPageHeader>
)}
<EuiPageContentBody data-test-subj="pageViewBodyContent">{children}</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</StyledEuiPage>
);
});
{tabs && <EuiTabs className="endpoint-navTabs">{tabComponents}</EuiTabs>}
<EuiPageContent className="endpoint-page-content">
{bodyHeader && (
<EuiPageContentHeader>
<EuiPageContentHeaderSection data-test-subj="pageViewBodyTitleArea">
{isStringOrNumber.test(typeof bodyHeader) ? (
<PageViewBodyHeaderTitle>{bodyHeader}</PageViewBodyHeaderTitle>
) : (
bodyHeader
)}
</EuiPageContentHeaderSection>
</EuiPageContentHeader>
)}
<EuiPageContentBody data-test-subj="pageViewBodyContent">{children}</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</StyledEuiPage>
);
}
);
PageView.displayName = 'PageView';

View file

@ -18,6 +18,7 @@ jest.mock('react-router-dom', () => ({
state: '',
}),
withRouter: () => jest.fn(),
generatePath: jest.fn(),
}));
// Test will fail because we will to need to mock some core services to make the test work

View file

@ -27,6 +27,7 @@ import {
} from './redirect_to_case';
import { DetectionEngineTab } from '../../../alerts/pages/detection_engine/types';
import { TimelineType } from '../../../../common/types/timeline';
import { RedirectToManagementPage } from './redirect_to_management';
interface LinkToPageProps {
match: RouteMatch<{}>;
@ -120,6 +121,10 @@ export const LinkToPage = React.memo<LinkToPageProps>(({ match }) => (
component={RedirectToTimelinesPage}
path={`${match.url}/:pageName(${SiemPageName.timelines})/:tabName(${TimelineType.default}|${TimelineType.template})`}
/>
<Route
component={RedirectToManagementPage}
path={`${match.url}/:pageName(${SiemPageName.management})`}
/>
<Redirect to="/" />
</Switch>
));

View file

@ -0,0 +1,15 @@
/*
* 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 { RedirectWrapper } from './redirect_wrapper';
import { SiemPageName } from '../../../app/types';
export const RedirectToManagementPage = memo(() => {
return <RedirectWrapper to={`/${SiemPageName.management}`} />;
});
RedirectToManagementPage.displayName = 'RedirectToManagementPage';

View file

@ -72,6 +72,13 @@ describe('SIEM Navigation', () => {
name: 'Cases',
urlKey: 'case',
},
management: {
disabled: false,
href: '#/management',
id: 'management',
name: 'Management',
urlKey: 'management',
},
detections: {
disabled: false,
href: '#/link-to/detections',
@ -111,6 +118,7 @@ describe('SIEM Navigation', () => {
pageName: 'hosts',
pathName: '/hosts',
search: '',
state: undefined,
tabName: 'authentications',
query: { query: '', language: 'kuery' },
filters: [],
@ -179,6 +187,13 @@ describe('SIEM Navigation', () => {
name: 'Hosts',
urlKey: 'host',
},
management: {
disabled: false,
href: '#/management',
id: 'management',
name: 'Management',
urlKey: 'management',
},
network: {
disabled: false,
href: '#/link-to/network',

View file

@ -12,6 +12,7 @@ export enum CONSTANTS {
filters = 'filters',
hostsDetails = 'hosts.details',
hostsPage = 'hosts.page',
management = 'management',
networkDetails = 'network.details',
networkPage = 'network.page',
overviewPage = 'overview.page',
@ -22,4 +23,11 @@ export enum CONSTANTS {
unknown = 'unknown',
}
export type UrlStateType = 'case' | 'detections' | 'host' | 'network' | 'overview' | 'timeline';
export type UrlStateType =
| 'case'
| 'detections'
| 'host'
| 'network'
| 'overview'
| 'timeline'
| 'management';

View file

@ -8,10 +8,10 @@ import ApolloClient from 'apollo-client';
import * as H from 'history';
import { ActionCreator } from 'typescript-fsa';
import {
IIndexPattern,
Query,
Filter,
FilterManager,
IIndexPattern,
Query,
SavedQueryService,
} from 'src/plugins/data/public';
@ -46,6 +46,7 @@ export const URL_STATE_KEYS: Record<UrlStateType, KeyUrlState[]> = {
CONSTANTS.timerange,
CONSTANTS.timeline,
],
management: [],
network: [
CONSTANTS.appQuery,
CONSTANTS.filters,

View file

@ -15,11 +15,10 @@ import { depsStartMock } from './dependencies_start_mock';
import { MiddlewareActionSpyHelper, createSpyMiddleware } from '../../store/test_utils';
import { apolloClientObservable } from '../test_providers';
import { createStore, State, substateMiddlewareFactory } from '../../store';
import { hostMiddlewareFactory } from '../../../endpoint_hosts/store/middleware';
import { policyListMiddlewareFactory } from '../../../endpoint_policy/store/policy_list/middleware';
import { policyDetailsMiddlewareFactory } from '../../../endpoint_policy/store/policy_details/middleware';
import { hostMiddlewareFactory } from '../../../endpoint_hosts/store';
import { alertMiddlewareFactory } from '../../../endpoint_alerts/store/middleware';
import { AppRootProvider } from './app_root_provider';
import { managementMiddlewareFactory } from '../../../management/store';
import { SUB_PLUGINS_REDUCER, mockGlobalState } from '..';
type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult;
@ -63,18 +62,11 @@ export const createAppRootMockRenderer = (): AppContextTestRender => {
(globalState) => globalState.hostList,
hostMiddlewareFactory(coreStart, depsStart)
),
substateMiddlewareFactory(
(globalState) => globalState.policyList,
policyListMiddlewareFactory(coreStart, depsStart)
),
substateMiddlewareFactory(
(globalState) => globalState.policyDetails,
policyDetailsMiddlewareFactory(coreStart, depsStart)
),
substateMiddlewareFactory(
(globalState) => globalState.alertList,
alertMiddlewareFactory(coreStart, depsStart)
),
...managementMiddlewareFactory(coreStart, depsStart),
middlewareSpy.actionSpyMiddleware,
]);

View file

@ -25,15 +25,13 @@ import {
} from '../../../common/constants';
import { networkModel } from '../../network/store';
import { TimelineType, TimelineStatus } from '../../../common/types/timeline';
import { initialPolicyListState } from '../../endpoint_policy/store/policy_list/reducer';
import { initialAlertListState } from '../../endpoint_alerts/store/reducer';
import { initialPolicyDetailsState } from '../../endpoint_policy/store/policy_details/reducer';
import { initialHostListState } from '../../endpoint_hosts/store/reducer';
import { getManagementInitialState } from '../../management/store';
const policyList = initialPolicyListState();
const alertList = initialAlertListState();
const policyDetails = initialPolicyDetailsState();
const hostList = initialHostListState();
const management = getManagementInitialState();
export const mockGlobalState: State = {
app: {
@ -237,6 +235,5 @@ export const mockGlobalState: State = {
},
alertList,
hostList,
policyList,
policyDetails,
management,
};

View file

@ -9,8 +9,7 @@ import { networkReducer } from '../../network/store';
import { timelineReducer } from '../../timelines/store/timeline/reducer';
import { hostListReducer } from '../../endpoint_hosts/store';
import { alertListReducer } from '../../endpoint_alerts/store';
import { policyListReducer } from '../../endpoint_policy/store/policy_list';
import { policyDetailsReducer } from '../../endpoint_policy/store/policy_details';
import { managementReducer } from '../../management/store';
interface Global extends NodeJS.Global {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -25,6 +24,5 @@ export const SUB_PLUGINS_REDUCER = {
timeline: timelineReducer,
hostList: hostListReducer,
alertList: alertListReducer,
policyList: policyListReducer,
policyDetails: policyDetailsReducer,
management: managementReducer,
};

View file

@ -6,8 +6,8 @@
import { HostAction } from '../../endpoint_hosts/store/action';
import { AlertAction } from '../../endpoint_alerts/store/action';
import { PolicyListAction } from '../../endpoint_policy/store/policy_list';
import { PolicyDetailsAction } from '../../endpoint_policy/store/policy_details';
import { PolicyListAction } from '../../management/pages/policy/store/policy_list';
import { PolicyDetailsAction } from '../../management/pages/policy/store/policy_details';
export { appActions } from './app';
export { dragAndDropActions } from './drag_and_drop';

View file

@ -18,14 +18,8 @@ import {
EndpointAlertsPluginReducer,
} from '../../endpoint_alerts/store';
import { EndpointHostsPluginState, EndpointHostsPluginReducer } from '../../endpoint_hosts/store';
import {
EndpointPolicyDetailsStatePluginState,
EndpointPolicyDetailsStatePluginReducer,
} from '../../endpoint_policy/store/policy_details';
import {
EndpointPolicyListStatePluginState,
EndpointPolicyListStatePluginReducer,
} from '../../endpoint_policy/store/policy_list';
import { ManagementPluginReducer, ManagementPluginState } from '../../management/store/types';
export interface State
extends HostsPluginState,
@ -33,8 +27,7 @@ export interface State
TimelinePluginState,
EndpointAlertsPluginState,
EndpointHostsPluginState,
EndpointPolicyDetailsStatePluginState,
EndpointPolicyListStatePluginState {
ManagementPluginState {
app: AppState;
dragAndDrop: DragAndDropState;
inputs: InputsState;
@ -51,15 +44,14 @@ type SubPluginsInitState = HostsPluginState &
TimelinePluginState &
EndpointAlertsPluginState &
EndpointHostsPluginState &
EndpointPolicyDetailsStatePluginState &
EndpointPolicyListStatePluginState;
ManagementPluginState;
export type SubPluginsInitReducer = HostsPluginReducer &
NetworkPluginReducer &
TimelinePluginReducer &
EndpointAlertsPluginReducer &
EndpointHostsPluginReducer &
EndpointPolicyDetailsStatePluginReducer &
EndpointPolicyListStatePluginReducer;
ManagementPluginReducer;
export const createInitialState = (pluginsInitState: SubPluginsInitState): State => ({
...initialState,

View file

@ -61,6 +61,17 @@ export type ImmutableMiddlewareFactory<S = State> = (
depsStart: Pick<StartPlugins, 'data' | 'ingestManager'>
) => ImmutableMiddleware<S, AppAction>;
/**
* Takes application-standard middleware dependencies
* and returns an array of redux middleware.
* Middleware will be of the `ImmutableMiddleware` variety. Not able to directly
* change actions or state.
*/
export type ImmutableMultipleMiddlewareFactory<S = State> = (
coreStart: CoreStart,
depsStart: Pick<StartPlugins, 'data' | 'ingestManager'>
) => Array<ImmutableMiddleware<S, AppAction>>;
/**
* Simple type for a redux selector.
*/

View file

@ -22,10 +22,12 @@ export class EndpointAlerts {
plugins: StartPlugins
): SecuritySubPluginWithStore<'alertList', Immutable<AlertListState>> {
const { data, ingestManager } = plugins;
const middleware = substateMiddlewareFactory(
(globalState) => globalState.alertList,
alertMiddlewareFactory(core, { data, ingestManager })
);
const middleware = [
substateMiddlewareFactory(
(globalState) => globalState.alertList,
alertMiddlewareFactory(core, { data, ingestManager })
),
];
return {
routes: getEndpointAlertsRoutes(),

View file

@ -22,10 +22,12 @@ export class EndpointHosts {
plugins: StartPlugins
): SecuritySubPluginWithStore<'hostList', Immutable<HostState>> {
const { data, ingestManager } = plugins;
const middleware = substateMiddlewareFactory(
(globalState) => globalState.hostList,
hostMiddlewareFactory(core, { data, ingestManager })
);
const middleware = [
substateMiddlewareFactory(
(globalState) => globalState.hostList,
hostMiddlewareFactory(core, { data, ingestManager })
),
];
return {
routes: getEndpointHostsRoutes(),
store: {

View file

@ -1,41 +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 { SecuritySubPluginWithStore } from '../app/types';
import { getPolicyDetailsRoutes } from './routes';
import { PolicyDetailsState } from './types';
import { Immutable } from '../../common/endpoint/types';
import { initialPolicyDetailsState, policyDetailsReducer } from './store/policy_details/reducer';
import { policyDetailsMiddlewareFactory } from './store/policy_details/middleware';
import { CoreStart } from '../../../../../src/core/public';
import { StartPlugins } from '../types';
import { substateMiddlewareFactory } from '../common/store';
export class EndpointPolicyDetails {
public setup() {}
public start(
core: CoreStart,
plugins: StartPlugins
): SecuritySubPluginWithStore<'policyDetails', Immutable<PolicyDetailsState>> {
const { data, ingestManager } = plugins;
const middleware = substateMiddlewareFactory(
(globalState) => globalState.policyDetails,
policyDetailsMiddlewareFactory(core, { data, ingestManager })
);
return {
routes: getPolicyDetailsRoutes(),
store: {
initialState: {
policyDetails: initialPolicyDetailsState(),
},
reducer: { policyDetails: policyDetailsReducer },
middleware,
},
};
}
}

View file

@ -1,41 +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 { SecuritySubPluginWithStore } from '../app/types';
import { getPolicyListRoutes } from './routes';
import { PolicyListState } from './types';
import { Immutable } from '../../common/endpoint/types';
import { initialPolicyListState, policyListReducer } from './store/policy_list/reducer';
import { policyListMiddlewareFactory } from './store/policy_list/middleware';
import { CoreStart } from '../../../../../src/core/public';
import { StartPlugins } from '../types';
import { substateMiddlewareFactory } from '../common/store';
export class EndpointPolicyList {
public setup() {}
public start(
core: CoreStart,
plugins: StartPlugins
): SecuritySubPluginWithStore<'policyList', Immutable<PolicyListState>> {
const { data, ingestManager } = plugins;
const middleware = substateMiddlewareFactory(
(globalState) => globalState.policyList,
policyListMiddlewareFactory(core, { data, ingestManager })
);
return {
routes: getPolicyListRoutes(),
store: {
initialState: {
policyList: initialPolicyListState(),
},
reducer: { policyList: policyListReducer },
middleware,
},
};
}
}

View file

@ -1,18 +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 React from 'react';
import { Route } from 'react-router-dom';
import { PolicyList, PolicyDetails } from './view';
export const getPolicyListRoutes = () => [
<Route path="/:pageName(policy)" exact component={PolicyList} />,
];
export const getPolicyDetailsRoutes = () => [
<Route path="/:pageName(policy)/:id" exact component={PolicyDetails} />,
];

View file

@ -1,19 +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 { useSelector } from 'react-redux';
import { PolicyListState, PolicyDetailsState } from '../types';
import { State } from '../../common/store';
export function usePolicyListSelector<TSelected>(selector: (state: PolicyListState) => TSelected) {
return useSelector((state: State) => selector(state.policyList as PolicyListState));
}
export function usePolicyDetailsSelector<TSelected>(
selector: (state: PolicyDetailsState) => TSelected
) {
return useSelector((state: State) => selector(state.policyDetails as PolicyDetailsState));
}

View file

@ -0,0 +1,21 @@
/*
* 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 { SiemPageName } from '../../app/types';
import { ManagementStoreGlobalNamespace, ManagementSubTab } from '../types';
// --[ ROUTING ]---------------------------------------------------------------------------
export const MANAGEMENT_ROUTING_ROOT_PATH = `/:pageName(${SiemPageName.management})`;
export const MANAGEMENT_ROUTING_ENDPOINTS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.endpoints})`;
export const MANAGEMENT_ROUTING_POLICIES_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.policies})`;
export const MANAGEMENT_ROUTING_POLICY_DETAILS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.policies})/:policyId`;
// --[ STORE ]---------------------------------------------------------------------------
/** The SIEM global store namespace where the management state will be mounted */
export const MANAGEMENT_STORE_GLOBAL_NAMESPACE: ManagementStoreGlobalNamespace = 'management';
/** Namespace within the Management state where policy list state is maintained */
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';

View file

@ -0,0 +1,70 @@
/*
* 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 { generatePath } from 'react-router-dom';
import {
MANAGEMENT_ROUTING_ENDPOINTS_PATH,
MANAGEMENT_ROUTING_POLICIES_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_PATH,
MANAGEMENT_ROUTING_ROOT_PATH,
} from './constants';
import { ManagementSubTab } from '../types';
import { SiemPageName } from '../../app/types';
export type GetManagementUrlProps = {
/**
* Exclude the URL prefix (everything to the left of where the router was mounted.
* This may be needed when interacting with react-router (ex. to do `history.push()` or
* validations against matched path)
*/
excludePrefix?: boolean;
} & (
| { name: 'default' }
| { name: 'endpointList' }
| { name: 'policyList' }
| { name: 'policyDetails'; policyId: string }
);
// Prefix is (almost) everything to the left of where the Router was mounted. In SIEM, since
// we're using Hash router, thats the `#`.
const URL_PREFIX = '#';
/**
* Returns a URL string for a given Management page view
* @param props
*/
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':
url += generatePath(MANAGEMENT_ROUTING_ENDPOINTS_PATH, {
pageName: SiemPageName.management,
tabName: ManagementSubTab.endpoints,
});
break;
case 'policyList':
url += generatePath(MANAGEMENT_ROUTING_POLICIES_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;
}
return url;
};

View file

@ -0,0 +1,37 @@
/*
* 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, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { useParams } from 'react-router-dom';
import { PageView, PageViewProps } from '../../common/components/endpoint/page_view';
import { ManagementSubTab } from '../types';
import { getManagementUrl } from '..';
export const ManagementPageView = memo<Omit<PageViewProps, 'tabs'>>((options) => {
const { tabName } = useParams<{ tabName: ManagementSubTab }>();
const tabs = useMemo((): PageViewProps['tabs'] => {
return [
{
name: i18n.translate('xpack.siem.managementTabs.endpoints', {
defaultMessage: 'Endpoints',
}),
id: ManagementSubTab.endpoints,
isSelected: tabName === ManagementSubTab.endpoints,
href: getManagementUrl({ name: 'endpointList' }),
},
{
name: i18n.translate('xpack.siem.managementTabs.policies', { defaultMessage: 'Policies' }),
id: ManagementSubTab.policies,
isSelected: tabName === ManagementSubTab.policies,
href: getManagementUrl({ name: 'policyList' }),
},
];
}, [tabName]);
return <PageView {...options} tabs={tabs} />;
});
ManagementPageView.displayName = 'ManagementPageView';

View file

@ -0,0 +1,39 @@
/*
* 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 { CoreStart } from 'kibana/public';
import { managementReducer, getManagementInitialState, managementMiddlewareFactory } from './store';
import { getManagementRoutes } from './routes';
import { StartPlugins } from '../types';
import { MANAGEMENT_STORE_GLOBAL_NAMESPACE } from './common/constants';
import { SecuritySubPluginWithStore } from '../app/types';
import { Immutable } from '../../common/endpoint/types';
import { ManagementStoreGlobalNamespace } from './types';
import { ManagementState } from './store/types';
export { getManagementUrl } from './common/routing';
export class Management {
public setup() {}
public start(
core: CoreStart,
plugins: StartPlugins
): SecuritySubPluginWithStore<ManagementStoreGlobalNamespace, Immutable<ManagementState>> {
return {
routes: getManagementRoutes(),
store: {
initialState: {
[MANAGEMENT_STORE_GLOBAL_NAMESPACE]: getManagementInitialState(),
},
reducer: {
[MANAGEMENT_STORE_GLOBAL_NAMESPACE]: managementReducer,
},
middleware: managementMiddlewareFactory(core, plugins),
},
};
}
}

View file

@ -0,0 +1,43 @@
/*
* 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 { 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>
);
};
export const ManagementContainer = memo(() => {
return (
<Switch>
<Route path={MANAGEMENT_ROUTING_ENDPOINTS_PATH} exact component={TmpEndpoints} />
<Route path={MANAGEMENT_ROUTING_POLICIES_PATH} component={PolicyContainer} />
<Route
path={MANAGEMENT_ROUTING_ROOT_PATH}
exact
render={() => <Redirect to="/management/endpoints" />}
/>
<Route path="*" component={NotFoundPage} />
</Switch>
);
});
ManagementContainer.displayName = 'ManagementContainer';

View file

@ -0,0 +1,26 @@
/*
* 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 { Route, Switch } from 'react-router-dom';
import { PolicyDetails, PolicyList } from './view';
import {
MANAGEMENT_ROUTING_POLICIES_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_PATH,
} from '../../common/constants';
import { NotFoundPage } from '../../../app/404';
export const PolicyContainer = memo(() => {
return (
<Switch>
<Route path={MANAGEMENT_ROUTING_POLICIES_PATH} exact component={PolicyList} />
<Route path={MANAGEMENT_ROUTING_POLICY_DETAILS_PATH} exact component={PolicyDetails} />
<Route path="*" component={NotFoundPage} />
</Switch>
);
});
PolicyContainer.displayName = 'PolicyContainer';

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { UIPolicyConfig } from '../../../common/endpoint/types';
import { UIPolicyConfig } from '../../../../../common/endpoint/types';
/**
* A typed Object.entries() function where the keys and values are typed based on the given object

View file

@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { GetAgentStatusResponse } from '../../../../../ingest_manager/common/types/rest_spec';
import { PolicyData, UIPolicyConfig } from '../../../../common/endpoint/types';
import { ServerApiError } from '../../../common/types';
import { GetAgentStatusResponse } from '../../../../../../../ingest_manager/common/types/rest_spec';
import { PolicyData, UIPolicyConfig } from '../../../../../../common/endpoint/types';
import { ServerApiError } from '../../../../../common/types';
import { PolicyDetailsState } from '../../types';
interface ServerReturnedPolicyDetailsData {

View file

@ -9,7 +9,7 @@ import { createStore, Dispatch, Store } from 'redux';
import { policyDetailsReducer, PolicyDetailsAction } from './index';
import { policyConfig } from './selectors';
import { clone } from '../../models/policy_details_config';
import { factory as policyConfigFactory } from '../../../../common/endpoint/models/policy_config';
import { factory as policyConfigFactory } from '../../../../../../common/endpoint/models/policy_config';
describe('policy details: ', () => {
let store: Store<PolicyDetailsState>;

View file

@ -5,9 +5,9 @@
*/
import { PolicyDetailsState } from '../../types';
import { ImmutableReducer } from '../../../common/store';
import { AppAction } from '../../../common/store/actions';
import { Immutable } from '../../../../common/endpoint/types';
import { ImmutableReducer } from '../../../../../common/store';
import { AppAction } from '../../../../../common/store/actions';
import { Immutable } from '../../../../../../common/endpoint/types';
export { policyDetailsMiddlewareFactory } from './middleware';
export { PolicyDetailsAction } from './action';

View file

@ -16,9 +16,9 @@ import {
sendGetFleetAgentStatusForConfig,
sendPutDatasource,
} from '../policy_list/services/ingest';
import { NewPolicyData, PolicyData, Immutable } from '../../../../common/endpoint/types';
import { factory as policyConfigFactory } from '../../../../common/endpoint/models/policy_config';
import { ImmutableMiddlewareFactory } from '../../../common/store';
import { NewPolicyData, PolicyData, Immutable } from '../../../../../../common/endpoint/types';
import { factory as policyConfigFactory } from '../../../../../../common/endpoint/models/policy_config';
import { ImmutableMiddlewareFactory } from '../../../../../common/store';
export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory<Immutable<
PolicyDetailsState

View file

@ -5,9 +5,9 @@
*/
import { fullPolicy, isOnPolicyDetailsPage } from './selectors';
import { PolicyDetailsState } from '../../types';
import { Immutable, PolicyConfig, UIPolicyConfig } from '../../../../common/endpoint/types';
import { ImmutableReducer } from '../../../common/store';
import { AppAction } from '../../../common/store/actions';
import { Immutable, PolicyConfig, UIPolicyConfig } from '../../../../../../common/endpoint/types';
import { ImmutableReducer } from '../../../../../common/store';
import { AppAction } from '../../../../../common/store/actions';
export const initialPolicyDetailsState = (): PolicyDetailsState => {
return {

View file

@ -5,14 +5,17 @@
*/
import { createSelector } from 'reselect';
import { matchPath } from 'react-router-dom';
import { PolicyDetailsState } from '../../types';
import {
Immutable,
NewPolicyData,
PolicyConfig,
UIPolicyConfig,
} from '../../../../common/endpoint/types';
import { factory as policyConfigFactory } from '../../../../common/endpoint/models/policy_config';
} from '../../../../../../common/endpoint/types';
import { factory as policyConfigFactory } from '../../../../../../common/endpoint/models/policy_config';
import { MANAGEMENT_ROUTING_POLICY_DETAILS_PATH } from '../../../../common/constants';
import { ManagementRoutePolicyDetailsParams } from '../../../../types';
/** Returns the policy details */
export const policyDetails = (state: Immutable<PolicyDetailsState>) => state.policyItem;
@ -31,22 +34,24 @@ export const policyDetailsForUpdate: (
/** Returns a boolean of whether the user is on the policy details page or not */
export const isOnPolicyDetailsPage = (state: Immutable<PolicyDetailsState>) => {
if (state.location) {
const pathnameParts = state.location.pathname.split('/');
return pathnameParts[1] === 'policy' && pathnameParts[2];
} else {
return false;
}
return (
matchPath(state.location?.pathname ?? '', {
path: MANAGEMENT_ROUTING_POLICY_DETAILS_PATH,
exact: true,
}) !== null
);
};
/** Returns the policyId from the url */
export const policyIdFromParams: (state: Immutable<PolicyDetailsState>) => string = createSelector(
(state) => state.location,
(location: PolicyDetailsState['location']) => {
if (location) {
return location.pathname.split('/')[2];
}
return '';
return (
matchPath<ManagementRoutePolicyDetailsParams>(location?.pathname ?? '', {
path: MANAGEMENT_ROUTING_POLICY_DETAILS_PATH,
exact: true,
})?.params?.policyId ?? ''
);
}
);

View file

@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { PolicyData } from '../../../../common/endpoint/types';
import { ServerApiError } from '../../../common/types';
import { PolicyData } from '../../../../../../common/endpoint/types';
import { ServerApiError } from '../../../../../common/types';
interface ServerReturnedPolicyListData {
type: 'serverReturnedPolicyListData';

View file

@ -7,19 +7,24 @@
import { PolicyListState } from '../../types';
import { Store, applyMiddleware, createStore } from 'redux';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../../ingest_manager/common';
import { coreMock } from '../../../../../../../../../src/core/public/mocks';
import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../../../../ingest_manager/common';
import { policyListReducer, initialPolicyListState } from './reducer';
import { policyListMiddlewareFactory } from './middleware';
import { isOnPolicyListPage, selectIsLoading, urlSearchParams } from './selectors';
import { DepsStartMock, depsStartMock } from '../../../common/mock/endpoint';
import { DepsStartMock, depsStartMock } from '../../../../../common/mock/endpoint';
import { setPolicyListApiMockImplementation } from './test_mock_utils';
import { INGEST_API_DATASOURCES } from './services/ingest';
import { createSpyMiddleware, MiddlewareActionSpyHelper } from '../../../common/store/test_utils';
import {
createSpyMiddleware,
MiddlewareActionSpyHelper,
} from '../../../../../common/store/test_utils';
import { getManagementUrl } from '../../../../common/routing';
describe('policy list store concerns', () => {
const policyListPathUrl = getManagementUrl({ name: 'policyList', excludePrefix: true });
let fakeCoreStart: ReturnType<typeof coreMock.createStart>;
let depsStart: DepsStartMock;
let store: Store;
@ -57,7 +62,7 @@ describe('policy list store concerns', () => {
store.dispatch({
type: 'userChangedUrl',
payload: {
pathname: '/policy',
pathname: policyListPathUrl,
search: '',
hash: '',
},
@ -70,7 +75,7 @@ describe('policy list store concerns', () => {
store.dispatch({
type: 'userChangedUrl',
payload: {
pathname: '/policy',
pathname: policyListPathUrl,
search: '',
hash: '',
},
@ -84,7 +89,7 @@ describe('policy list store concerns', () => {
store.dispatch({
type: 'userChangedUrl',
payload: {
pathname: '/policy',
pathname: policyListPathUrl,
search: '',
hash: '',
},
@ -112,7 +117,7 @@ describe('policy list store concerns', () => {
store.dispatch({
type: 'userChangedUrl',
payload: {
pathname: '/policy',
pathname: policyListPathUrl,
search: '',
hash: '',
},
@ -132,7 +137,7 @@ describe('policy list store concerns', () => {
store.dispatch({
type: 'userChangedUrl',
payload: {
pathname: '/policy',
pathname: policyListPathUrl,
search: searchParams,
hash: '',
},

View file

@ -5,9 +5,9 @@
*/
import { PolicyListState } from '../../types';
import { ImmutableReducer } from '../../../common/store';
import { AppAction } from '../../../common/store/actions';
import { Immutable } from '../../../../common/endpoint/types';
import { ImmutableReducer } from '../../../../../common/store';
import { AppAction } from '../../../../../common/store/actions';
import { Immutable } from '../../../../../../common/endpoint/types';
export { policyListReducer } from './reducer';
export { PolicyListAction } from './action';
export { policyListMiddlewareFactory } from './middleware';

View file

@ -7,8 +7,8 @@
import { GetPolicyListResponse, PolicyListState } from '../../types';
import { sendGetEndpointSpecificDatasources } from './services/ingest';
import { isOnPolicyListPage, urlSearchParams } from './selectors';
import { ImmutableMiddlewareFactory } from '../../../common/store';
import { Immutable } from '../../../../common/endpoint/types';
import { ImmutableMiddlewareFactory } from '../../../../../common/store';
import { Immutable } from '../../../../../../common/endpoint/types';
export const policyListMiddlewareFactory: ImmutableMiddlewareFactory<Immutable<PolicyListState>> = (
coreStart

View file

@ -6,9 +6,9 @@
import { PolicyListState } from '../../types';
import { isOnPolicyListPage } from './selectors';
import { ImmutableReducer } from '../../../common/store';
import { AppAction } from '../../../common/store/actions';
import { Immutable } from '../../../../common/endpoint/types';
import { ImmutableReducer } from '../../../../../common/store';
import { AppAction } from '../../../../../common/store/actions';
import { Immutable } from '../../../../../../common/endpoint/types';
export const initialPolicyListState = (): PolicyListState => {
return {

View file

@ -6,8 +6,10 @@
import { createSelector } from 'reselect';
import { parse } from 'query-string';
import { matchPath } from 'react-router-dom';
import { PolicyListState, PolicyListUrlSearchParams } from '../../types';
import { Immutable } from '../../../../common/endpoint/types';
import { Immutable } from '../../../../../../common/endpoint/types';
import { MANAGEMENT_ROUTING_POLICIES_PATH } from '../../../../common/constants';
const PAGE_SIZES = Object.freeze([10, 20, 50]);
@ -24,7 +26,12 @@ export const selectIsLoading = (state: Immutable<PolicyListState>) => state.isLo
export const selectApiError = (state: Immutable<PolicyListState>) => state.apiError;
export const isOnPolicyListPage = (state: Immutable<PolicyListState>) => {
return state.location?.pathname === '/policy';
return (
matchPath(state.location?.pathname ?? '', {
path: MANAGEMENT_ROUTING_POLICIES_PATH,
exact: true,
}) !== null
);
};
const routeLocation = (state: Immutable<PolicyListState>) => state.location;

View file

@ -5,8 +5,8 @@
*/
import { sendGetDatasource, sendGetEndpointSpecificDatasources } from './ingest';
import { httpServiceMock } from '../../../../../../../../src/core/public/mocks';
import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../../../ingest_manager/common';
import { httpServiceMock } from '../../../../../../../../../../src/core/public/mocks';
import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../../../../../ingest_manager/common';
describe('ingest service', () => {
let http: ReturnType<typeof httpServiceMock.createStartContract>;

View file

@ -9,9 +9,9 @@ import {
GetDatasourcesRequest,
GetAgentStatusResponse,
DATASOURCE_SAVED_OBJECT_TYPE,
} from '../../../../../../ingest_manager/common';
} from '../../../../../../../../ingest_manager/common';
import { GetPolicyListResponse, GetPolicyResponse, UpdatePolicyResponse } from '../../../types';
import { NewPolicyData } from '../../../../../common/endpoint/types';
import { NewPolicyData } from '../../../../../../../common/endpoint/types';
const INGEST_API_ROOT = `/api/ingest_manager`;
export const INGEST_API_DATASOURCES = `${INGEST_API_ROOT}/datasources`;

View file

@ -6,7 +6,7 @@
import { HttpStart } from 'kibana/public';
import { INGEST_API_DATASOURCES } from './services/ingest';
import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data';
import { EndpointDocGenerator } from '../../../../../../common/endpoint/generate_data';
import { GetPolicyListResponse } from '../../types';
const generator = new EndpointDocGenerator('policy-list');

View file

@ -10,14 +10,14 @@ import {
MalwareFields,
UIPolicyConfig,
AppLocation,
} from '../../common/endpoint/types';
import { ServerApiError } from '../common/types';
} from '../../../../common/endpoint/types';
import { ServerApiError } from '../../../common/types';
import {
GetAgentStatusResponse,
GetDatasourcesResponse,
GetOneDatasourceResponse,
UpdateDatasourceResponse,
} from '../../../ingest_manager/common';
} from '../../../../../ingest_manager/common';
/**
* Policy list store state

View file

@ -8,12 +8,20 @@ import React from 'react';
import { mount } from 'enzyme';
import { PolicyDetails } from './policy_details';
import { EndpointDocGenerator } from '../../../common/endpoint/generate_data';
import { createAppRootMockRenderer } from '../../common/mock/endpoint';
import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data';
import { createAppRootMockRenderer } from '../../../../common/mock/endpoint';
import { getManagementUrl } from '../../../common/routing';
describe('Policy Details', () => {
type FindReactWrapperResponse = ReturnType<ReturnType<typeof render>['find']>;
const policyDetailsPathUrl = getManagementUrl({
name: 'policyDetails',
policyId: '1',
excludePrefix: true,
});
const policyListPathUrl = getManagementUrl({ name: 'policyList', excludePrefix: true });
const policyListPathUrlWithPrefix = getManagementUrl({ name: 'policyList' });
const sleep = (ms = 100) => new Promise((wakeup) => setTimeout(wakeup, ms));
const generator = new EndpointDocGenerator();
const { history, AppWrapper, coreStart } = createAppRootMockRenderer();
@ -33,7 +41,7 @@ describe('Policy Details', () => {
describe('when displayed with invalid id', () => {
beforeEach(() => {
http.get.mockReturnValue(Promise.reject(new Error('policy not found')));
history.push('/policy/1');
history.push(policyDetailsPathUrl);
policyView = render(<PolicyDetails />);
});
@ -77,7 +85,7 @@ describe('Policy Details', () => {
return Promise.reject(new Error('unknown API call!'));
});
history.push('/policy/1');
history.push(policyDetailsPathUrl);
policyView = render(<PolicyDetails />);
});
@ -89,7 +97,7 @@ describe('Policy Details', () => {
const backToListButton = pageHeaderLeft.find('EuiButtonEmpty');
expect(backToListButton.prop('iconType')).toBe('arrowLeft');
expect(backToListButton.prop('href')).toBe('/mock/app/endpoint/policy');
expect(backToListButton.prop('href')).toBe(policyListPathUrlWithPrefix);
expect(backToListButton.text()).toBe('Back to policy list');
const pageTitle = pageHeaderLeft.find('[data-test-subj="pageViewHeaderLeftTitle"]');
@ -101,9 +109,9 @@ describe('Policy Details', () => {
const backToListButton = policyView.find(
'EuiPageHeaderSection[data-test-subj="pageViewHeaderLeft"] EuiButtonEmpty'
);
expect(history.location.pathname).toEqual('/policy/1');
expect(history.location.pathname).toEqual(policyDetailsPathUrl);
backToListButton.simulate('click', { button: 0 });
expect(history.location.pathname).toEqual('/policy');
expect(history.location.pathname).toEqual(policyListPathUrl);
});
it('should display agent stats', async () => {
await asyncActions;
@ -130,9 +138,9 @@ describe('Policy Details', () => {
const cancelbutton = policyView.find(
'EuiButtonEmpty[data-test-subj="policyDetailsCancelButton"]'
);
expect(history.location.pathname).toEqual('/policy/1');
expect(history.location.pathname).toEqual(policyDetailsPathUrl);
cancelbutton.simulate('click', { button: 0 });
expect(history.location.pathname).toEqual('/policy');
expect(history.location.pathname).toEqual(policyListPathUrl);
});
it('should display save button', async () => {
await asyncActions;

View file

@ -28,18 +28,21 @@ import {
isLoading,
apiError,
} from '../store/policy_details/selectors';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { AgentsSummary } from './agents_summary';
import { VerticalDivider } from './vertical_divider';
import { WindowsEvents, MacEvents, LinuxEvents } from './policy_forms/events';
import { MalwareProtections } from './policy_forms/protections/malware';
import { AppAction } from '../../common/store/actions';
import { useNavigateByRouterEventHandler } from '../../common/hooks/endpoint/use_navigate_by_router_event_handler';
import { PageView, PageViewHeaderTitle } from '../../common/components/endpoint/page_view';
import { AppAction } from '../../../../common/store/actions';
import { useNavigateByRouterEventHandler } from '../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
import { PageViewHeaderTitle } from '../../../../common/components/endpoint/page_view';
import { ManagementPageView } from '../../../components/management_page_view';
import { SpyRoute } from '../../../../common/utils/route/spy_routes';
import { getManagementUrl } from '../../../common/routing';
export const PolicyDetails = React.memo(() => {
const dispatch = useDispatch<(action: AppAction) => void>();
const { notifications, services } = useKibana();
const { notifications } = useKibana();
// Store values
const policyItem = usePolicyDetailsSelector(policyDetails);
@ -81,7 +84,9 @@ export const PolicyDetails = React.memo(() => {
}
}, [notifications.toasts, policyName, policyUpdateStatus]);
const handleBackToListOnClick = useNavigateByRouterEventHandler('/policy');
const handleBackToListOnClick = useNavigateByRouterEventHandler(
getManagementUrl({ name: 'policyList', excludePrefix: true })
);
const handleSaveOnClick = useCallback(() => {
setShowConfirm(true);
@ -103,7 +108,7 @@ export const PolicyDetails = React.memo(() => {
// Else, if we have an error, then show error on the page.
if (!policyItem) {
return (
<PageView viewType="details">
<ManagementPageView viewType="details">
{isPolicyLoading ? (
<EuiLoadingSpinner size="xl" />
) : policyApiError ? (
@ -111,7 +116,8 @@ export const PolicyDetails = React.memo(() => {
{policyApiError?.message}
</EuiCallOut>
) : null}
</PageView>
<SpyRoute />
</ManagementPageView>
);
}
@ -122,7 +128,7 @@ export const PolicyDetails = React.memo(() => {
iconType="arrowLeft"
contentProps={{ style: { paddingLeft: '0' } }}
onClick={handleBackToListOnClick}
href={`${services.http.basePath.get()}/app/endpoint/policy`}
href={getManagementUrl({ name: 'policyList' })}
>
<FormattedMessage
id="xpack.siem.endpoint.policy.details.backToListTitle"
@ -180,7 +186,7 @@ export const PolicyDetails = React.memo(() => {
onConfirm={handleSaveConfirmation}
/>
)}
<PageView
<ManagementPageView
viewType="details"
data-test-subj="policyDetailsPage"
headerLeft={headerLeftContent}
@ -211,7 +217,8 @@ export const PolicyDetails = React.memo(() => {
<MacEvents />
<EuiSpacer size="l" />
<LinuxEvents />
</PageView>
</ManagementPageView>
<SpyRoute />
</>
);
});

View file

@ -11,7 +11,7 @@ import { useDispatch } from 'react-redux';
import { usePolicyDetailsSelector } from '../../policy_hooks';
import { policyConfig } from '../../../store/policy_details/selectors';
import { PolicyDetailsAction } from '../../../store/policy_details';
import { UIPolicyConfig } from '../../../../../common/endpoint/types';
import { UIPolicyConfig } from '../../../../../../../common/endpoint/types';
export const EventsCheckbox = React.memo(function ({
name,

View file

@ -14,7 +14,7 @@ import { usePolicyDetailsSelector } from '../../policy_hooks';
import { selectedLinuxEvents, totalLinuxEvents } from '../../../store/policy_details/selectors';
import { ConfigForm } from '../config_form';
import { getIn, setIn } from '../../../models/policy_details_config';
import { UIPolicyConfig } from '../../../../../common/endpoint/types';
import { UIPolicyConfig } from '../../../../../../../common/endpoint/types';
export const LinuxEvents = React.memo(() => {
const selected = usePolicyDetailsSelector(selectedLinuxEvents);

View file

@ -14,7 +14,7 @@ import { usePolicyDetailsSelector } from '../../policy_hooks';
import { selectedMacEvents, totalMacEvents } from '../../../store/policy_details/selectors';
import { ConfigForm } from '../config_form';
import { getIn, setIn } from '../../../models/policy_details_config';
import { UIPolicyConfig } from '../../../../../common/endpoint/types';
import { UIPolicyConfig } from '../../../../../../../common/endpoint/types';
export const MacEvents = React.memo(() => {
const selected = usePolicyDetailsSelector(selectedMacEvents);

View file

@ -14,7 +14,7 @@ import { usePolicyDetailsSelector } from '../../policy_hooks';
import { selectedWindowsEvents, totalWindowsEvents } from '../../../store/policy_details/selectors';
import { ConfigForm } from '../config_form';
import { setIn, getIn } from '../../../models/policy_details_config';
import { UIPolicyConfig, Immutable } from '../../../../../common/endpoint/types';
import { UIPolicyConfig, Immutable } from '../../../../../../../common/endpoint/types';
export const WindowsEvents = React.memo(() => {
const selected = usePolicyDetailsSelector(selectedWindowsEvents);

View file

@ -11,7 +11,7 @@ import { EuiRadio, EuiSwitch, EuiTitle, EuiSpacer, htmlIdGenerator } from '@elas
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { Immutable, ProtectionModes } from '../../../../../common/endpoint/types';
import { Immutable, ProtectionModes } from '../../../../../../../common/endpoint/types';
import { OS, MalwareProtectionOSes } from '../../../types';
import { ConfigForm } from '../config_form';
import { policyConfig } from '../../../store/policy_details/selectors';

View file

@ -0,0 +1,44 @@
/*
* 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 { useSelector } from 'react-redux';
import { PolicyListState, PolicyDetailsState } from '../types';
import { State } from '../../../../common/store';
import {
MANAGEMENT_STORE_GLOBAL_NAMESPACE,
MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE,
MANAGEMENT_STORE_POLICY_LIST_NAMESPACE,
} from '../../../common/constants';
/**
* Narrows global state down to the PolicyListState before calling the provided Policy List Selector
* @param selector
*/
export function usePolicyListSelector<TSelected>(selector: (state: PolicyListState) => TSelected) {
return useSelector((state: State) => {
return selector(
state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][
MANAGEMENT_STORE_POLICY_LIST_NAMESPACE
] as PolicyListState
);
});
}
/**
* Narrows global state down to the PolicyDetailsState before calling the provided Policy Details Selector
* @param selector
*/
export function usePolicyDetailsSelector<TSelected>(
selector: (state: PolicyDetailsState) => TSelected
) {
return useSelector((state: State) =>
selector(
state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][
MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE
] as PolicyDetailsState
)
);
}

View file

@ -20,11 +20,13 @@ import {
} from '../store/policy_list/selectors';
import { usePolicyListSelector } from './policy_hooks';
import { PolicyListAction } from '../store/policy_list';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
import { Immutable, PolicyData } from '../../../common/endpoint/types';
import { useNavigateByRouterEventHandler } from '../../common/hooks/endpoint/use_navigate_by_router_event_handler';
import { PageView } from '../../common/components/endpoint/page_view';
import { LinkToApp } from '../../common/components/endpoint/link_to_app';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { Immutable, PolicyData } from '../../../../../common/endpoint/types';
import { useNavigateByRouterEventHandler } from '../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
import { LinkToApp } from '../../../../common/components/endpoint/link_to_app';
import { ManagementPageView } from '../../../components/management_page_view';
import { SpyRoute } from '../../../../common/utils/route/spy_routes';
import { getManagementUrl } from '../../../common/routing';
interface TableChangeCallbackArguments {
page: { index: number; size: number };
@ -93,14 +95,13 @@ export const PolicyList = React.memo(() => {
}),
// eslint-disable-next-line react/display-name
render: (value: string, item: Immutable<PolicyData>) => {
const routeUri = `/policy/${item.id}`;
return (
<PolicyLink
name={value}
route={routeUri}
href={services.application.getUrlForApp('endpoint') + routeUri}
/>
);
const routePath = getManagementUrl({
name: 'policyDetails',
policyId: item.id,
excludePrefix: true,
});
const routeUrl = getManagementUrl({ name: 'policyDetails', policyId: item.id });
return <PolicyLink name={value} route={routePath} href={routeUrl} />;
},
truncateText: true,
},
@ -150,7 +151,7 @@ export const PolicyList = React.memo(() => {
);
return (
<PageView
<ManagementPageView
viewType="list"
data-test-subj="policyListPage"
headerLeft={i18n.translate('xpack.siem.endpoint.policyList.viewTitle', {
@ -174,7 +175,8 @@ export const PolicyList = React.memo(() => {
onChange={handleTableChange}
data-test-subj="policyTable"
/>
</PageView>
<SpyRoute />
</ManagementPageView>
);
});

View file

@ -5,7 +5,7 @@
*/
import styled from 'styled-components';
import { EuiTheme } from '../../../../../legacy/common/eui_styled_components';
import { EuiTheme } from '../../../../../../../legacy/common/eui_styled_components';
type SpacingOptions = keyof EuiTheme['eui']['spacerSizes'];

View file

@ -0,0 +1,18 @@
/*
* 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 from 'react';
import { Route } from 'react-router-dom';
import { ManagementContainer } from './pages';
import { MANAGEMENT_ROUTING_ROOT_PATH } from './common/constants';
/**
* Returns the React Router Routes for the management area
*/
export const getManagementRoutes = () => [
// Mounts the Management interface on `/management`
<Route path={MANAGEMENT_ROUTING_ROOT_PATH} component={ManagementContainer} />,
];

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export { managementReducer, getManagementInitialState } from './reducer';
export { managementMiddlewareFactory } from './middleware';

View file

@ -0,0 +1,33 @@
/*
* 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 { ImmutableMultipleMiddlewareFactory, substateMiddlewareFactory } from '../../common/store';
import { policyListMiddlewareFactory } from '../pages/policy/store/policy_list';
import { policyDetailsMiddlewareFactory } from '../pages/policy/store/policy_details';
import {
MANAGEMENT_STORE_GLOBAL_NAMESPACE,
MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE,
MANAGEMENT_STORE_POLICY_LIST_NAMESPACE,
} from '../common/constants';
// @ts-ignore
export const managementMiddlewareFactory: ImmutableMultipleMiddlewareFactory = (
coreStart,
depsStart
) => {
return [
substateMiddlewareFactory(
(globalState) =>
globalState[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_POLICY_LIST_NAMESPACE],
policyListMiddlewareFactory(coreStart, depsStart)
),
substateMiddlewareFactory(
(globalState) =>
globalState[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE],
policyDetailsMiddlewareFactory(coreStart, depsStart)
),
];
};

View file

@ -0,0 +1,45 @@
/*
* 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 { combineReducers as reduxCombineReducers } from 'redux';
import {
initialPolicyDetailsState,
policyDetailsReducer,
} from '../pages/policy/store/policy_details/reducer';
import {
initialPolicyListState,
policyListReducer,
} from '../pages/policy/store/policy_list/reducer';
import {
MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE,
MANAGEMENT_STORE_POLICY_LIST_NAMESPACE,
} from '../common/constants';
import { ImmutableCombineReducers } from '../../common/store';
import { AppAction } from '../../common/store/actions';
import { ManagementState } from './types';
// Change the type of `combinerReducers` locally
const combineReducers: ImmutableCombineReducers = reduxCombineReducers;
/**
* Returns the initial state of the store for the SIEM Management section
*/
export const getManagementInitialState = (): ManagementState => {
return {
[MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: initialPolicyListState(),
[MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: initialPolicyDetailsState(),
};
};
/**
* Redux store reducer for the SIEM Management section
*/
export const managementReducer = combineReducers<ManagementState, AppAction>({
// @ts-ignore
[MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: policyListReducer,
// @ts-ignore
[MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: policyDetailsReducer,
});

View file

@ -0,0 +1,26 @@
/*
* 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 { Immutable } from '../../../common/endpoint/types';
import { PolicyDetailsState, PolicyListState } from '../pages/policy/types';
import { ImmutableReducer } from '../../common/store';
import { AppAction } from '../../common/store/actions';
/**
* Redux store state for the Management section
*/
export interface ManagementState {
policyDetails: Immutable<PolicyDetailsState>;
policyList: Immutable<PolicyListState>;
}
export interface ManagementPluginState {
management: ManagementState;
}
export interface ManagementPluginReducer {
management: ImmutableReducer<ManagementState, AppAction>;
}

View file

@ -0,0 +1,36 @@
/*
* 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 { SiemPageName } from '../app/types';
/**
* The type for the management store global namespace. Used mostly internally to reference
* the type while defining more complex interfaces/types
*/
export type ManagementStoreGlobalNamespace = 'management';
/**
* The management list of sub-tabs. Changes to these will impact the Router routes.
*/
export enum ManagementSubTab {
endpoints = 'endpoints',
policies = 'policy',
}
/**
* The URL route params for the Management Policy List section
*/
export interface ManagementRoutePolicyListParams {
pageName: SiemPageName.management;
tabName: ManagementSubTab.policies;
}
/**
* The URL route params for the Management Policy Details section
*/
export interface ManagementRoutePolicyDetailsParams extends ManagementRoutePolicyListParams {
policyId: string;
}

View file

@ -64,13 +64,8 @@ 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 endpoitHostsSubPlugin = new (await import('./endpoint_hosts')).EndpointHosts();
const endpointPolicyListSubPlugin = new (
await import('./endpoint_policy/list')
).EndpointPolicyList();
const endpointPolicyDetailsSubPlugin = new (
await import('./endpoint_policy/details')
).EndpointPolicyDetails();
const endpointHostsSubPlugin = new (await import('./endpoint_hosts')).EndpointHosts();
const managementSubPlugin = new (await import('./management')).Management();
const alertsStart = alertsSubPlugin.start();
const casesStart = casesSubPlugin.start();
@ -79,12 +74,8 @@ 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 = endpoitHostsSubPlugin.start(coreStart, startPlugins);
const endpointPolicyListStart = endpointPolicyListSubPlugin.start(coreStart, startPlugins);
const endpointPolicyDetailsStart = endpointPolicyDetailsSubPlugin.start(
coreStart,
startPlugins
);
const endpointHostsStart = endpointHostsSubPlugin.start(coreStart, startPlugins);
const managementSubPluginStart = managementSubPlugin.start(coreStart, startPlugins);
return renderApp(services, params, {
routes: [
@ -96,8 +87,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
...timelinesStart.routes,
...endpointAlertsStart.routes,
...endpointHostsStart.routes,
...endpointPolicyListStart.routes,
...endpointPolicyDetailsStart.routes,
...managementSubPluginStart.routes,
],
store: {
initialState: {
@ -106,8 +96,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
...timelinesStart.store.initialState,
...endpointAlertsStart.store.initialState,
...endpointHostsStart.store.initialState,
...endpointPolicyListStart.store.initialState,
...endpointPolicyDetailsStart.store.initialState,
...managementSubPluginStart.store.initialState,
},
reducer: {
...hostsStart.store.reducer,
@ -115,22 +104,12 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
...timelinesStart.store.reducer,
...endpointAlertsStart.store.reducer,
...endpointHostsStart.store.reducer,
...endpointPolicyListStart.store.reducer,
...endpointPolicyDetailsStart.store.reducer,
...managementSubPluginStart.store.reducer,
},
middlewares: [
...(endpointAlertsStart.store.middleware != null
? [endpointAlertsStart.store.middleware]
: []),
...(endpointHostsStart.store.middleware != null
? [endpointHostsStart.store.middleware]
: []),
...(endpointPolicyListStart.store.middleware != null
? [endpointPolicyListStart.store.middleware]
: []),
...(endpointPolicyDetailsStart.store.middleware != null
? [endpointPolicyDetailsStart.store.middleware]
: []),
...(endpointAlertsStart.store.middleware ?? []),
...(endpointHostsStart.store.middleware ?? []),
...(managementSubPluginStart.store.middleware ?? []),
],
},
});