body header
@@ -82,28 +91,37 @@ exports[`PageView component should display body header custom element 1`] = `
`;
exports[`PageView component should display body header wrapped in EuiTitle 1`] = `
-.c0 {
+.c0.endpoint--isListView {
padding: 0;
}
-.c0 .endpoint-header {
+.c0.endpoint--isListView .endpoint-header {
padding: 24px;
}
-.c0 .endpoint-page-content {
+.c0.endpoint--isListView .endpoint-page-content {
border-left: none;
border-right: none;
}
+.c0.endpoint--isDetailsView .endpoint-page-content {
+ padding: 0;
+ border: none;
+ background: none;
+}
+
-
+
-
-
- body header
-
-
+
+
+
+ body header
+
+
+
@@ -163,29 +184,38 @@ exports[`PageView component should display body header wrapped in EuiTitle 1`] =
`;
exports[`PageView component should display header left and right 1`] = `
-.c0 {
+.c0.endpoint--isListView {
padding: 0;
}
-.c0 .endpoint-header {
+.c0.endpoint--isListView .endpoint-header {
padding: 24px;
}
-.c0 .endpoint-page-content {
+.c0.endpoint--isListView .endpoint-page-content {
border-left: none;
border-right: none;
}
+.c0.endpoint--isDetailsView .endpoint-page-content {
+ padding: 0;
+ border: none;
+ background: none;
+}
+
-
+
-
-
+
- page title
-
-
+
+ page title
+
+
+
-
+.c0.endpoint--isDetailsView .endpoint-page-content {
+ padding: 0;
+ border: none;
+ background: none;
+}
+
+
+
-
+
-
-
+
- page title
-
-
+
+ page title
+
+
+
@@ -401,28 +456,37 @@ exports[`PageView component should display only header left 1`] = `
`;
exports[`PageView component should display only header right but include an empty left side 1`] = `
-.c0 {
+.c0.endpoint--isListView {
padding: 0;
}
-.c0 .endpoint-header {
+.c0.endpoint--isListView .endpoint-header {
padding: 24px;
}
-.c0 .endpoint-page-content {
+.c0.endpoint--isListView .endpoint-page-content {
border-left: none;
border-right: none;
}
+.c0.endpoint--isDetailsView .endpoint-page-content {
+ padding: 0;
+ border: none;
+ background: none;
+}
+
-
+
title here
}
+ viewType="list"
>
-
+
{
mount(ui, { wrappingComponent: EuiThemeProvider });
it('should display only body if not header props used', () => {
- expect(render(body content)).toMatchSnapshot();
+ expect(render(body content)).toMatchSnapshot();
});
it('should display header left and right', () => {
expect(
render(
-
+
body content
)
).toMatchSnapshot();
});
it('should display only header left', () => {
- expect(render(body content)).toMatchSnapshot();
+ expect(
+ render(
+
+ body content
+
+ )
+ ).toMatchSnapshot();
});
it('should display only header right but include an empty left side', () => {
expect(
- render(body content)
+ render(
+
+ body content
+
+ )
).toMatchSnapshot();
});
it(`should use custom element for header left and not wrap in EuiTitle`, () => {
expect(
- render(title here}>body content)
+ render(
+ title here}>
+ body content
+
+ )
).toMatchSnapshot();
});
it('should display body header wrapped in EuiTitle', () => {
- expect(render(body content)).toMatchSnapshot();
+ expect(
+ render(
+
+ body content
+
+ )
+ ).toMatchSnapshot();
});
it('should display body header custom element', () => {
expect(
- render(body header}>body content)
+ render(
+ body header}>
+ body content
+
+ )
).toMatchSnapshot();
});
it('should pass through EuiPage props', () => {
expect(
render(
props.theme.eui.euiSizeL};
+ .endpoint-header {
+ padding: ${props => props.theme.eui.euiSizeL};
+ }
+ .endpoint-page-content {
+ border-left: none;
+ border-right: none;
+ }
}
- .endpoint-page-content {
- border-left: none;
- border-right: none;
+ &.endpoint--isDetailsView {
+ .endpoint-page-content {
+ padding: 0;
+ border: none;
+ background: none;
+ }
}
`;
const isStringOrNumber = /(string|number)/;
+/**
+ * The `PageView` component used to render `headerLeft` when it is set as a `string`
+ * Can be used when wanting to customize the `headerLeft` value but still use the standard
+ * title component
+ */
+export const PageViewHeaderTitle = memo<{ children: ReactNode }>(({ children }) => {
+ return (
+
+ {children}
+
+ );
+});
+
+/**
+ * The `PageView` component used to render `bodyHeader` when it is set as a `string`
+ * Can be used when wanting to customize the `bodyHeader` value but still use the standard
+ * title component
+ */
+export const PageViewBodyHeaderTitle = memo<{ children: ReactNode }>(
+ ({ children, ...otherProps }) => {
+ return (
+
+ {children}
+
+ );
+ }
+);
+
/**
* 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 ``, else it will just be used as is.
@@ -52,17 +93,18 @@ export const PageView = memo<
bodyHeader?: ReactNode;
children?: ReactNode;
}
->(({ children, headerLeft, headerRight, bodyHeader, ...otherProps }) => {
+>(({ viewType, children, headerLeft, headerRight, bodyHeader, ...otherProps }) => {
return (
-
+
{(headerLeft || headerRight) && (
{isStringOrNumber.test(typeof headerLeft) ? (
-
- {headerLeft}
-
+ {headerLeft}
) : (
headerLeft
)}
@@ -77,11 +119,9 @@ export const PageView = memo<
{bodyHeader && (
-
+
{isStringOrNumber.test(typeof bodyHeader) ? (
-
- {bodyHeader}
-
+ {bodyHeader}
) : (
bodyHeader
)}
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/models/policy.ts b/x-pack/plugins/endpoint/public/applications/endpoint/models/policy.ts
new file mode 100644
index 0000000000000..e1ac9defc858e
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/models/policy.ts
@@ -0,0 +1,90 @@
+/*
+ * 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 { PolicyConfig } from '../types';
+
+/**
+ * Generate a new Policy model.
+ * NOTE: in the near future, this will likely be removed and an API call to EPM will be used to retrieve
+ * the latest from the Endpoint package
+ */
+export const generatePolicy = (): PolicyConfig => {
+ return {
+ windows: {
+ events: {
+ process: true,
+ network: true,
+ },
+ malware: {
+ mode: 'prevent',
+ },
+ logging: {
+ stdout: 'debug',
+ file: 'info',
+ },
+ advanced: {
+ elasticsearch: {
+ indices: {
+ control: 'control-index',
+ event: 'event-index',
+ logging: 'logging-index',
+ },
+ kernel: {
+ connect: true,
+ process: true,
+ },
+ },
+ },
+ },
+ mac: {
+ events: {
+ process: true,
+ },
+ malware: {
+ mode: 'detect',
+ },
+ logging: {
+ stdout: 'debug',
+ file: 'info',
+ },
+ advanced: {
+ elasticsearch: {
+ indices: {
+ control: 'control-index',
+ event: 'event-index',
+ logging: 'logging-index',
+ },
+ kernel: {
+ connect: true,
+ process: true,
+ },
+ },
+ },
+ },
+ linux: {
+ events: {
+ process: true,
+ },
+ logging: {
+ stdout: 'debug',
+ file: 'info',
+ },
+ advanced: {
+ elasticsearch: {
+ indices: {
+ control: 'control-index',
+ event: 'event-index',
+ logging: 'logging-index',
+ },
+ kernel: {
+ connect: true,
+ process: true,
+ },
+ },
+ },
+ },
+ };
+};
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/models/policy_details_config.ts b/x-pack/plugins/endpoint/public/applications/endpoint/models/policy_details_config.ts
index 1900516cb539b..1145d1d19242a 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/models/policy_details_config.ts
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/models/policy_details_config.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { PolicyConfig } from '../types';
+import { UIPolicyConfig } from '../types';
/**
* A typed Object.entries() function where the keys and values are typed based on the given object
@@ -14,10 +14,10 @@ const entries = (o: T): Array<[keyof T, T[keyof T]]> =>
type DeepPartial = { [K in keyof T]?: DeepPartial };
/**
- * Returns a deep copy of PolicyDetailsConfig
+ * Returns a deep copy of `UIPolicyConfig` object
*/
-export function clone(policyDetailsConfig: PolicyConfig): PolicyConfig {
- const clonedConfig: DeepPartial = {};
+export function clone(policyDetailsConfig: UIPolicyConfig): UIPolicyConfig {
+ const clonedConfig: DeepPartial = {};
for (const [key, val] of entries(policyDetailsConfig)) {
if (typeof val === 'object') {
const valClone: Partial = {};
@@ -41,5 +41,5 @@ export function clone(policyDetailsConfig: PolicyConfig): PolicyConfig {
/**
* clonedConfig is typed as DeepPartial so we can construct the copy from an empty object
*/
- return clonedConfig as PolicyConfig;
+ return clonedConfig as UIPolicyConfig;
}
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/services/ingest.ts b/x-pack/plugins/endpoint/public/applications/endpoint/services/ingest.ts
index fbb92f8bbe915..583ebc55d896b 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/services/ingest.ts
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/services/ingest.ts
@@ -5,11 +5,17 @@
*/
import { HttpFetchOptions, HttpStart } from 'kibana/public';
-import { GetDatasourcesRequest } from '../../../../../ingest_manager/common/types/rest_spec';
-import { PolicyData } from '../types';
+import {
+ CreateDatasourceResponse,
+ GetAgentStatusResponse,
+ GetDatasourcesRequest,
+} from '../../../../../ingest_manager/common/types/rest_spec';
+import { NewPolicyData, PolicyData } from '../types';
const INGEST_API_ROOT = `/api/ingest_manager`;
const INGEST_API_DATASOURCES = `${INGEST_API_ROOT}/datasources`;
+const INGEST_API_FLEET = `${INGEST_API_ROOT}/fleet`;
+const INGEST_API_FLEET_AGENT_STATUS = `${INGEST_API_FLEET}/agent-status`;
// FIXME: Import from ingest after - https://github.com/elastic/kibana/issues/60677
export interface GetDatasourcesResponse {
@@ -26,6 +32,11 @@ export interface GetDatasourceResponse {
success: boolean;
}
+// FIXME: Import from Ingest after - https://github.com/elastic/kibana/issues/60677
+export type UpdateDatasourceResponse = CreateDatasourceResponse & {
+ item: PolicyData;
+};
+
/**
* Retrieves a list of endpoint specific datasources (those created with a `package.name` of
* `endpoint`) from Ingest
@@ -60,3 +71,44 @@ export const sendGetDatasource = (
) => {
return http.get(`${INGEST_API_DATASOURCES}/${datasourceId}`, options);
};
+
+/**
+ * Updates a datasources
+ *
+ * @param http
+ * @param datasourceId
+ * @param datasource
+ * @param options
+ */
+export const sendPutDatasource = (
+ http: HttpStart,
+ datasourceId: string,
+ datasource: NewPolicyData,
+ options: Exclude = {}
+): Promise => {
+ return http.put(`${INGEST_API_DATASOURCES}/${datasourceId}`, {
+ ...options,
+ body: JSON.stringify(datasource),
+ });
+};
+
+/**
+ * Get a status summary for all Agents that are currently assigned to a given agent configuration
+ *
+ * @param http
+ * @param configId
+ * @param options
+ */
+export const sendGetFleetAgentStatusForConfig = (
+ http: HttpStart,
+ /** the Agent (fleet) configuration id */
+ configId: string,
+ options: Exclude = {}
+): Promise => {
+ return http.get(INGEST_API_FLEET_AGENT_STATUS, {
+ ...options,
+ query: {
+ configId,
+ },
+ });
+};
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/action.ts
index e7e523a9287b8..9905145048a8a 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/action.ts
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/action.ts
@@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { PolicyData, PolicyConfig } from '../../types';
+import { PolicyData, PolicyDetailsState, ServerApiError, UIPolicyConfig } from '../../types';
+import { GetAgentStatusResponse } from '../../../../../../ingest_manager/common/types/rest_spec';
interface ServerReturnedPolicyDetailsData {
type: 'serverReturnedPolicyDetailsData';
@@ -13,14 +14,50 @@ interface ServerReturnedPolicyDetailsData {
};
}
+interface ServerFailedToReturnPolicyDetailsData {
+ type: 'serverFailedToReturnPolicyDetailsData';
+ payload: ServerApiError;
+}
+
/**
* When users change a policy via forms, this action is dispatched with a payload that modifies the configuration of a cloned policy config.
*/
interface UserChangedPolicyConfig {
type: 'userChangedPolicyConfig';
payload: {
- policyConfig: PolicyConfig;
+ policyConfig: UIPolicyConfig;
+ };
+}
+
+interface ServerReturnedPolicyDetailsAgentSummaryData {
+ type: 'serverReturnedPolicyDetailsAgentSummaryData';
+ payload: {
+ agentStatusSummary: GetAgentStatusResponse['results'];
+ };
+}
+
+interface ServerReturnedPolicyDetailsUpdateFailure {
+ type: 'serverReturnedPolicyDetailsUpdateFailure';
+ payload: PolicyDetailsState['updateStatus'];
+}
+
+interface ServerReturnedUpdatedPolicyDetailsData {
+ type: 'serverReturnedUpdatedPolicyDetailsData';
+ payload: {
+ policyItem: PolicyData;
+ updateStatus: PolicyDetailsState['updateStatus'];
};
}
-export type PolicyDetailsAction = ServerReturnedPolicyDetailsData | UserChangedPolicyConfig;
+interface UserClickedPolicyDetailsSaveButton {
+ type: 'userClickedPolicyDetailsSaveButton';
+}
+
+export type PolicyDetailsAction =
+ | ServerReturnedPolicyDetailsData
+ | UserClickedPolicyDetailsSaveButton
+ | ServerReturnedPolicyDetailsAgentSummaryData
+ | ServerReturnedPolicyDetailsUpdateFailure
+ | ServerReturnedUpdatedPolicyDetailsData
+ | ServerFailedToReturnPolicyDetailsData
+ | UserChangedPolicyConfig;
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/index.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/index.test.ts
index b20df84fdf575..cf14092953227 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/index.test.ts
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/index.test.ts
@@ -9,6 +9,7 @@ import { createStore, Dispatch, Store } from 'redux';
import { policyDetailsReducer, PolicyDetailsAction } from './index';
import { policyConfig, windowsEventing } from './selectors';
import { clone } from '../../models/policy_details_config';
+import { generatePolicy } from '../../models/policy';
describe('policy details: ', () => {
let store: Store;
@@ -30,7 +31,18 @@ describe('policy details: ', () => {
config_id: '',
enabled: true,
output_id: '',
- inputs: [],
+ inputs: [
+ {
+ type: 'endpoint',
+ enabled: true,
+ streams: [],
+ config: {
+ policy: {
+ value: generatePolicy(),
+ },
+ },
+ },
+ ],
namespace: '',
package: {
name: '',
@@ -39,32 +51,6 @@ describe('policy details: ', () => {
},
revision: 1,
},
- policyConfig: {
- windows: {
- malware: {
- mode: 'detect',
- },
- eventing: {
- process: false,
- network: false,
- },
- },
- mac: {
- malware: {
- mode: '',
- },
- eventing: {
- process: false,
- network: false,
- },
- },
- linux: {
- eventing: {
- process: false,
- network: false,
- },
- },
- },
},
});
});
@@ -77,7 +63,7 @@ describe('policy details: ', () => {
}
const newPayload1 = clone(config);
- newPayload1.windows.eventing.process = true;
+ newPayload1.windows.events.process = true;
dispatch({
type: 'userChangedPolicyConfig',
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/middleware.ts
index 1942538aa9df9..18248e272aada 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/middleware.ts
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/middleware.ts
@@ -4,9 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { MiddlewareFactory, PolicyDetailsState } from '../../types';
-import { policyIdFromParams, isOnPolicyDetailsPage } from './selectors';
-import { sendGetDatasource } from '../../services/ingest';
+import { MiddlewareFactory, PolicyData, PolicyDetailsState } from '../../types';
+import { policyIdFromParams, isOnPolicyDetailsPage, policyDetails } from './selectors';
+import {
+ sendGetDatasource,
+ sendGetFleetAgentStatusForConfig,
+ sendPutDatasource,
+ UpdateDatasourceResponse,
+} from '../../services/ingest';
+import { generatePolicy } from '../../models/policy';
export const policyDetailsMiddlewareFactory: MiddlewareFactory = coreStart => {
const http = coreStart.http;
@@ -17,25 +23,78 @@ export const policyDetailsMiddlewareFactory: MiddlewareFactory {
return {
policyItem: undefined,
- policyConfig: undefined,
isLoading: false,
+ agentStatusSummary: {
+ error: 0,
+ events: 0,
+ offline: 0,
+ online: 0,
+ total: 0,
+ },
};
};
@@ -20,7 +27,10 @@ export const policyDetailsReducer: Reducer = (
state = initialPolicyDetailsState(),
action
) => {
- if (action.type === 'serverReturnedPolicyDetailsData') {
+ if (
+ action.type === 'serverReturnedPolicyDetailsData' ||
+ action.type === 'serverReturnedUpdatedPolicyDetailsData'
+ ) {
return {
...state,
...action.payload,
@@ -28,19 +38,67 @@ export const policyDetailsReducer: Reducer = (
};
}
- if (action.type === 'userChangedUrl') {
+ if (action.type === 'serverFailedToReturnPolicyDetailsData') {
return {
...state,
- location: action.payload,
+ isLoading: false,
+ apiError: action.payload,
};
}
- if (action.type === 'userChangedPolicyConfig') {
+ if (action.type === 'serverReturnedPolicyDetailsAgentSummaryData') {
+ return {
+ ...state,
+ ...action.payload,
+ };
+ }
+
+ if (action.type === 'serverReturnedPolicyDetailsUpdateFailure') {
+ return {
+ ...state,
+ isLoading: false,
+ updateStatus: action.payload,
+ };
+ }
+
+ if (action.type === 'userClickedPolicyDetailsSaveButton') {
return {
...state,
- policyConfig: action.payload.policyConfig,
+ isLoading: true,
+ updateApiError: undefined,
};
}
+ if (action.type === 'userChangedUrl') {
+ const newState = {
+ ...state,
+ location: action.payload,
+ };
+
+ if (isOnPolicyDetailsPage(newState)) {
+ return newState;
+ }
+ return {
+ ...initialPolicyDetailsState(),
+ location: action.payload,
+ };
+ }
+
+ if (action.type === 'userChangedPolicyConfig') {
+ const newState = { ...state, policyItem: { ...(state.policyItem as PolicyData) } };
+ const newPolicy = (newState.policyItem.inputs[0].config.policy.value = {
+ ...fullPolicy(state),
+ });
+
+ Object.entries(action.payload.policyConfig).forEach(([section, newSettings]) => {
+ newPolicy[section as keyof UIPolicyConfig] = {
+ ...newPolicy[section as keyof UIPolicyConfig],
+ ...newSettings,
+ };
+ });
+
+ return newState;
+ }
+
return state;
};
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/selectors.ts
index 6a5d4077b3c32..0d505931c9ec5 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/selectors.ts
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/selectors.ts
@@ -5,8 +5,8 @@
*/
import { createSelector } from 'reselect';
-import { PolicyDetailsState } from '../../types';
-import { Immutable } from '../../../../../common/types';
+import { PolicyConfig, PolicyDetailsState, UIPolicyConfig } from '../../types';
+import { generatePolicy } from '../../models/policy';
/** Returns the policy details */
export const policyDetails = (state: PolicyDetailsState) => state.policyItem;
@@ -32,20 +32,64 @@ export const policyIdFromParams: (state: PolicyDetailsState) => string = createS
}
);
+/**
+ * Returns the full Endpoint Policy, which will include private settings not shown on the UI.
+ * Note: this will return a default full policy if the `policyItem` is `undefined`
+ */
+export const fullPolicy: (s: PolicyDetailsState) => PolicyConfig = createSelector(
+ policyDetails,
+ policyData => {
+ return policyData?.inputs[0]?.config?.policy?.value ?? generatePolicy();
+ }
+);
+
+const fullWindowsPolicySettings: (
+ s: PolicyDetailsState
+) => PolicyConfig['windows'] = createSelector(fullPolicy, policy => policy?.windows);
+
+const fullMacPolicySettings: (s: PolicyDetailsState) => PolicyConfig['mac'] = createSelector(
+ fullPolicy,
+ policy => policy?.mac
+);
+
+const fullLinuxPolicySettings: (s: PolicyDetailsState) => PolicyConfig['linux'] = createSelector(
+ fullPolicy,
+ policy => policy?.linux
+);
+
/** Returns the policy configuration */
-export const policyConfig = (state: Immutable) => state.policyConfig;
+export const policyConfig: (s: PolicyDetailsState) => UIPolicyConfig = createSelector(
+ fullWindowsPolicySettings,
+ fullMacPolicySettings,
+ fullLinuxPolicySettings,
+ (windows, mac, linux) => {
+ return {
+ windows: {
+ events: windows.events,
+ malware: windows.malware,
+ },
+ mac: {
+ events: mac.events,
+ malware: mac.malware,
+ },
+ linux: {
+ events: linux.events,
+ },
+ };
+ }
+);
/** Returns an object of all the windows eventing configuration */
export const windowsEventing = (state: PolicyDetailsState) => {
const config = policyConfig(state);
- return config && config.windows.eventing;
+ return config && config.windows.events;
};
/** Returns the total number of possible windows eventing configurations */
export const totalWindowsEventing = (state: PolicyDetailsState): number => {
const config = policyConfig(state);
if (config) {
- return Object.keys(config.windows.eventing).length;
+ return Object.keys(config.windows.events).length;
}
return 0;
};
@@ -54,9 +98,21 @@ export const totalWindowsEventing = (state: PolicyDetailsState): number => {
export const selectedWindowsEventing = (state: PolicyDetailsState): number => {
const config = policyConfig(state);
if (config) {
- return Object.values(config.windows.eventing).reduce((count, event) => {
+ return Object.values(config.windows.events).reduce((count, event) => {
return event === true ? count + 1 : count;
}, 0);
}
return 0;
};
+
+/** is there an api call in flight */
+export const isLoading = (state: PolicyDetailsState) => state.isLoading;
+
+/** API error when fetching Policy data */
+export const apiError = (state: PolicyDetailsState) => state.apiError;
+
+/** Policy Agent Summary Stats */
+export const agentStatusSummary = (state: PolicyDetailsState) => state.agentStatusSummary;
+
+/** Status for an update to the policy */
+export const updateStatus = (state: PolicyDetailsState) => state.updateStatus;
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts
index 7947a35068234..4215edb4d6810 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts
@@ -17,7 +17,8 @@ import {
import { EndpointPluginStartDependencies } from '../../plugin';
import { AppAction } from './store/action';
import { CoreStart } from '../../../../../../src/core/public';
-import { Datasource } from '../../../../ingest_manager/common/types/models';
+import { Datasource, NewDatasource } from '../../../../ingest_manager/common/types/models';
+import { GetAgentStatusResponse } from '../../../../ingest_manager/common/types/rest_spec';
export { AppAction };
export type MiddlewareFactory = (
@@ -53,9 +54,27 @@ export interface ServerApiError {
}
/**
- * An Endpoint Policy.
+ * New policy data. Used when updating the policy record via ingest APIs
*/
-export type PolicyData = Datasource;
+export type NewPolicyData = NewDatasource & {
+ inputs: [
+ {
+ type: 'endpoint';
+ enabled: boolean;
+ streams: [];
+ config: {
+ policy: {
+ value: PolicyConfig;
+ };
+ };
+ }
+ ];
+};
+
+/**
+ * Endpoint Policy data, which extends Ingest's `Datasource` type
+ */
+export type PolicyData = Datasource & NewPolicyData;
/**
* Policy list store state
@@ -81,57 +100,100 @@ export interface PolicyListState {
export interface PolicyDetailsState {
/** A single policy item */
policyItem?: PolicyData;
- /** data is being retrieved from server */
- policyConfig?: PolicyConfig;
+ /** API error if loading data failed */
+ apiError?: ServerApiError;
isLoading: boolean;
/** current location of the application */
location?: Immutable;
+ /** A summary of stats for the agents associated with a given Fleet Agent Configuration */
+ agentStatusSummary: GetAgentStatusResponse['results'];
+ /** Status of an update to the policy */
+ updateStatus?: {
+ success: boolean;
+ error?: ServerApiError;
+ };
}
/**
- * Policy Details configuration
+ * Endpoint Policy configuration
*/
export interface PolicyConfig {
- windows: WindowsPolicyConfig;
- mac: MacPolicyConfig;
- linux: LinuxPolicyConfig;
+ windows: {
+ events: {
+ process: boolean;
+ network: boolean;
+ };
+ /** malware mode can be detect, prevent or prevent and notify user */
+ malware: {
+ mode: string;
+ };
+ logging: {
+ stdout: string;
+ file: string;
+ };
+ advanced: PolicyConfigAdvancedOptions;
+ };
+ mac: {
+ events: {
+ process: boolean;
+ };
+ malware: {
+ mode: string;
+ };
+ logging: {
+ stdout: string;
+ file: string;
+ };
+ advanced: PolicyConfigAdvancedOptions;
+ };
+ linux: {
+ events: {
+ process: boolean;
+ };
+ logging: {
+ stdout: string;
+ file: string;
+ };
+ advanced: PolicyConfigAdvancedOptions;
+ };
}
-/**
- * Windows-specific policy configuration
- */
-interface WindowsPolicyConfig {
- /** malware mode can be detect, prevent or prevent and notify user */
- malware: {
- mode: string;
- };
- eventing: {
- process: boolean;
- network: boolean;
+interface PolicyConfigAdvancedOptions {
+ elasticsearch: {
+ indices: {
+ control: string;
+ event: string;
+ logging: string;
+ };
+ kernel: {
+ connect: boolean;
+ process: boolean;
+ };
};
}
/**
- * Mac-specific policy configuration
+ * Windows-specific policy configuration that is supported via the UI
*/
-interface MacPolicyConfig {
- /** malware mode can be detect, prevent or prevent and notify user */
- malware: {
- mode: string;
- };
- eventing: {
- process: boolean;
- network: boolean;
- };
-}
+type WindowsPolicyConfig = Pick;
+
/**
- * Linux-specific policy configuration
+ * Mac-specific policy configuration that is supported via the UI
*/
-interface LinuxPolicyConfig {
- eventing: {
- process: boolean;
- network: boolean;
- };
+type MacPolicyConfig = Pick;
+
+/**
+ * Linux-specific policy configuration that is supported via the UI
+ */
+type LinuxPolicyConfig = Pick;
+
+/**
+ * The set of Policy configuration settings that are show/edited via the UI
+ */
+export interface UIPolicyConfig {
+ windows: WindowsPolicyConfig;
+ mac: MacPolicyConfig;
+ linux: LinuxPolicyConfig;
}
/** OS used in Policy */
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/agents_summary.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/agents_summary.tsx
new file mode 100644
index 0000000000000..d0751cf9fb886
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/agents_summary.tsx
@@ -0,0 +1,88 @@
+/*
+ * 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 {
+ EuiDescriptionList,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiHealth,
+ EuiI18nNumber,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+export interface AgentsSummaryProps {
+ total: number;
+ online: number;
+ offline: number;
+ error: number;
+}
+
+/**
+ * Display a summary of stats (counts) associated with a group of agents (ex. those associated with a Policy)
+ */
+export const AgentsSummary = memo(props => {
+ const stats = useMemo<
+ Array<{ key: keyof AgentsSummaryProps; title: string; health: string }>
+ >(() => {
+ return [
+ {
+ key: 'total',
+ title: i18n.translate('xpack.endpoint.policyDetails.agentsSummary.totalTitle', {
+ defaultMessage: 'Hosts',
+ }),
+ health: '',
+ },
+ {
+ key: 'online',
+ title: i18n.translate('xpack.endpoint.policyDetails.agentsSummary.onlineTitle', {
+ defaultMessage: 'Online',
+ }),
+ health: 'success',
+ },
+ {
+ key: 'offline',
+ title: i18n.translate('xpack.endpoint.policyDetails.agentsSummary.offlineTitle', {
+ defaultMessage: 'Offline',
+ }),
+ health: 'warning',
+ },
+ {
+ key: 'error',
+ title: i18n.translate('xpack.endpoint.policyDetails.agentsSummary.errorTitle', {
+ defaultMessage: 'Error',
+ }),
+ health: 'danger',
+ },
+ ];
+ }, []);
+
+ return (
+
+ {stats.map(({ key, title, health }) => {
+ return (
+
+
+ {health && }
+
+ >
+ ),
+ },
+ ]}
+ />
+
+ );
+ })}
+
+ );
+});
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx
index a64b3293ec6cd..f2c79155f3c23 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
@@ -12,32 +12,154 @@ import {
EuiButtonEmpty,
EuiText,
EuiSpacer,
+ EuiOverlayMask,
+ EuiConfirmModal,
+ EuiCallOut,
+ EuiLoadingSpinner,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
+import { useDispatch } from 'react-redux';
+import { useHistory } from 'react-router-dom';
import { usePolicyDetailsSelector } from './policy_hooks';
-import { policyDetails } from '../../store/policy_details/selectors';
+import {
+ policyDetails,
+ agentStatusSummary,
+ updateStatus,
+ isLoading,
+ apiError,
+} from '../../store/policy_details/selectors';
import { WindowsEventing } from './policy_forms/eventing/windows';
-import { PageView } from '../../components/page_view';
+import { PageView, PageViewHeaderTitle } from '../../components/page_view';
+import { AppAction } from '../../types';
+import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
+import { AgentsSummary } from './agents_summary';
+import { VerticalDivider } from './vertical_divider';
export const PolicyDetails = React.memo(() => {
+ const dispatch = useDispatch<(action: AppAction) => void>();
+ const { notifications, services } = useKibana();
+ const history = useHistory();
+
+ // Store values
const policyItem = usePolicyDetailsSelector(policyDetails);
+ const policyAgentStatusSummary = usePolicyDetailsSelector(agentStatusSummary);
+ const policyUpdateStatus = usePolicyDetailsSelector(updateStatus);
+ const isPolicyLoading = usePolicyDetailsSelector(isLoading);
+ const policyApiError = usePolicyDetailsSelector(apiError);
+
+ // Local state
+ const [showConfirm, setShowConfirm] = useState(false);
+ const policyName = policyItem?.name ?? '';
- const headerLeftContent =
- policyItem?.name ??
- i18n.translate('xpack.endpoint.policyDetails.notFound', {
- defaultMessage: 'Policy Not Found',
+ // Handle showing udpate statuses
+ useEffect(() => {
+ if (policyUpdateStatus) {
+ if (policyUpdateStatus.success) {
+ notifications.toasts.success({
+ toastLifeTimeMs: 10000,
+ title: i18n.translate('xpack.endpoint.policy.details.updateSuccessTitle', {
+ defaultMessage: 'Success!',
+ }),
+ body: (
+
+ ),
+ });
+ } else {
+ notifications.toasts.danger({
+ toastLifeTimeMs: 10000,
+ title: i18n.translate('xpack.endpoint.policy.details.updateErrorTitle', {
+ defaultMessage: 'Failed!',
+ }),
+ body: <>{policyUpdateStatus.error!.message}>,
+ });
+ }
+ }
+ }, [notifications.toasts, policyItem, policyName, policyUpdateStatus]);
+
+ const handleBackToListOnClick = useCallback(
+ ev => {
+ ev.preventDefault();
+ history.push(`/policy`);
+ },
+ [history]
+ );
+
+ const handleSaveOnClick = useCallback(() => {
+ setShowConfirm(true);
+ }, []);
+
+ const handleSaveConfirmation = useCallback(() => {
+ dispatch({
+ type: 'userClickedPolicyDetailsSaveButton',
});
+ setShowConfirm(false);
+ }, [dispatch]);
+
+ const handleSaveCancel = useCallback(() => {
+ setShowConfirm(false);
+ }, []);
+
+ // Before proceeding - check if we have a policy data.
+ // If not, and we are still loading, show spinner.
+ // Else, if we have an error, then show error on the page.
+ if (!policyItem) {
+ return (
+
+ {isPolicyLoading ? (
+
+ ) : policyApiError ? (
+
+ {policyApiError?.message}
+
+ ) : null}
+
+ );
+ }
+
+ const headerLeftContent = (
+
+ {/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
+
+
+
+
{policyItem.name}
+
+ );
const headerRightContent = (
-
+
+
+
+
+
+
+
-
+
@@ -45,18 +167,85 @@ export const PolicyDetails = React.memo(() => {
);
return (
-
-
-
-
-
-
-
-
-
+ <>
+ {showConfirm && (
+
+ )}
+
+
+
+
+
+
+
+
+
+ >
+ );
+});
+
+const ConfirmUpdate = React.memo<{
+ hostCount: number;
+ onConfirm: () => void;
+ onCancel: () => void;
+}>(({ hostCount, onCancel, onConfirm }) => {
+ return (
+
+
+ {hostCount > 0 && (
+ <>
+
+
+
+
+ >
+ )}
+
+
+
+
+
);
});
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/checkbox.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/checkbox.tsx
index add137ea57a5e..8b7fb89ed1646 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/checkbox.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/checkbox.tsx
@@ -27,7 +27,11 @@ export const EventingCheckbox: React.FC<{
(event: React.ChangeEvent) => {
if (policyDetailsConfig) {
const newPayload = clone(policyDetailsConfig);
- newPayload[os].eventing[protectionField] = event.target.checked;
+ if (os === OS.linux || os === OS.mac) {
+ newPayload[os].events.process = event.target.checked;
+ } else {
+ newPayload[os].events[protectionField] = event.target.checked;
+ }
dispatch({
type: 'userChangedPolicyConfig',
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx
index 7af302de8576e..5ee1539ce9788 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx
@@ -151,6 +151,7 @@ export const PolicyList = React.memo(() => {
return (
`
+ width: 0;
+ height: 100%;
+ border-left: ${props => {
+ return props.theme.eui.euiBorderThin;
+ }};
+ margin-left: ${props => props.theme.eui.paddingSizes[props?.spacing ?? 'none'] || 0};
+ margin-right: ${props => props.theme.eui.paddingSizes[props?.spacing ?? 'none'] || 0};
+`;