Endpoint: Redux types (#63413)

## Summary

Changes some types around so modifying redux state or actions will cause type errors

* the state and action types aren't using `Immutable` directly. This means that you can instantiate them and mutate them
* the `state` and `action` params received by all reducers will be automatically cast to `Immutable`
* reducers can return either `Immutable` or regular versions of `state`
* some code may be mutating redux state directly, this is being ignored (at least for now)
This commit is contained in:
Robert Austin 2020-04-15 11:19:46 -04:00 committed by GitHub
parent bb0d03fe11
commit 08aa214190
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 444 additions and 305 deletions

View file

@ -6,11 +6,8 @@
import uuid from 'uuid';
import seedrandom from 'seedrandom';
import { AlertEvent, EndpointEvent, HostMetadata, OSFields, HostFields } from './types';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { PolicyData } from '../public/applications/endpoint/types';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { generatePolicy } from '../public/applications/endpoint/models/policy';
import { AlertEvent, EndpointEvent, HostMetadata, OSFields, HostFields, PolicyData } from './types';
import { factory as policyFactory } from './models/policy_config';
export type Event = AlertEvent | EndpointEvent;
@ -474,7 +471,7 @@ export class EndpointDocGenerator {
streams: [],
config: {
policy: {
value: generatePolicy(),
value: policyFactory(),
},
},
},

View file

@ -7,11 +7,9 @@
import { PolicyConfig, ProtectionModes } 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
* Return a new default `PolicyConfig`.
*/
export const generatePolicy = (): PolicyConfig => {
export const factory = (): PolicyConfig => {
return {
windows: {
events: {

View file

@ -7,12 +7,15 @@
import { SearchResponse } from 'elasticsearch';
import { TypeOf } from '@kbn/config-schema';
import { alertingIndexGetQuerySchema } from './schema/alert_index';
import { Datasource, NewDatasource } from '../../ingest_manager/common';
/**
* A deep readonly type that will make all children of a given object readonly recursively
*/
export type Immutable<T> = T extends undefined | null | boolean | string | number
? T
: unknown extends T
? unknown
: T extends Array<infer U>
? ImmutableArray<U>
: T extends Map<infer K, infer V>
@ -442,3 +445,125 @@ export type AlertingIndexGetQueryInput = KbnConfigSchemaInputTypeOf<
* Result of the validated query params when handling alert index requests.
*/
export type AlertingIndexGetQueryResult = TypeOf<typeof alertingIndexGetQuerySchema>;
/**
* Endpoint Policy configuration
*/
export interface PolicyConfig {
windows: {
events: {
dll_and_driver_load: boolean;
dns: boolean;
file: boolean;
network: boolean;
process: boolean;
registry: boolean;
security: boolean;
};
malware: MalwareFields;
logging: {
stdout: string;
file: string;
};
advanced: PolicyConfigAdvancedOptions;
};
mac: {
events: {
file: boolean;
process: boolean;
network: boolean;
};
malware: MalwareFields;
logging: {
stdout: string;
file: string;
};
advanced: PolicyConfigAdvancedOptions;
};
linux: {
events: {
file: boolean;
process: boolean;
network: boolean;
};
logging: {
stdout: string;
file: string;
};
advanced: PolicyConfigAdvancedOptions;
};
}
/**
* Windows-specific policy configuration that is supported via the UI
*/
type WindowsPolicyConfig = Pick<PolicyConfig['windows'], 'events' | 'malware'>;
/**
* Mac-specific policy configuration that is supported via the UI
*/
type MacPolicyConfig = Pick<PolicyConfig['mac'], 'malware' | 'events'>;
/**
* Linux-specific policy configuration that is supported via the UI
*/
type LinuxPolicyConfig = Pick<PolicyConfig['linux'], 'events'>;
/**
* The set of Policy configuration settings that are show/edited via the UI
*/
export interface UIPolicyConfig {
windows: WindowsPolicyConfig;
mac: MacPolicyConfig;
linux: LinuxPolicyConfig;
}
interface PolicyConfigAdvancedOptions {
elasticsearch: {
indices: {
control: string;
event: string;
logging: string;
};
kernel: {
connect: boolean;
process: boolean;
};
};
}
/** Policy: Malware protection fields */
export interface MalwareFields {
mode: ProtectionModes;
}
/** Policy protection mode options */
export enum ProtectionModes {
detect = 'detect',
prevent = 'prevent',
preventNotify = 'preventNotify',
off = 'off',
}
/**
* Endpoint Policy data, which extends Ingest's `Datasource` type
*/
export type PolicyData = Datasource & NewPolicyData;
/**
* New policy data. Used when updating the policy record via ingest APIs
*/
export type NewPolicyData = NewDatasource & {
inputs: [
{
type: 'endpoint';
enabled: boolean;
streams: [];
config: {
policy: {
value: PolicyConfig;
};
};
}
];
};

View file

@ -0,0 +1,16 @@
/*
* 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 { all } from 'deepmerge';
import { Immutable } from '../../../../common/types';
import { IIndexPattern } from '../../../../../../../src/plugins/data/common';
/**
* Model for the `IIndexPattern` interface exported by the `data` plugin.
*/
export function clone(value: IIndexPattern | Immutable<IIndexPattern>): IIndexPattern {
return all([value]) as IIndexPattern;
}

View file

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

View file

@ -14,16 +14,17 @@ import { coreMock } from 'src/core/public/mocks';
import { DepsStartMock, depsStartMock } from '../../mocks';
import { createBrowserHistory } from 'history';
import { mockAlertResultList } from './mock_alert_result_list';
import { Immutable } from '../../../../../common/types';
describe('alert details tests', () => {
let store: Store<AlertListState, AppAction>;
let store: Store<Immutable<AlertListState>, Immutable<AppAction>>;
let coreStart: ReturnType<typeof coreMock.createStart>;
let depsStart: DepsStartMock;
let history: History<never>;
/**
* A function that waits until a selector returns true.
*/
let selectorIsTrue: (selector: (state: AlertListState) => boolean) => Promise<void>;
let selectorIsTrue: (selector: (state: Immutable<AlertListState>) => boolean) => Promise<void>;
beforeEach(() => {
coreStart = coreMock.createStart();
depsStart = depsStartMock();

View file

@ -12,20 +12,20 @@ import { alertMiddlewareFactory } from './middleware';
import { AppAction } from '../action';
import { coreMock } from 'src/core/public/mocks';
import { DepsStartMock, depsStartMock } from '../../mocks';
import { AlertResultList } from '../../../../../common/types';
import { AlertResultList, Immutable } from '../../../../../common/types';
import { isOnAlertPage } from './selectors';
import { createBrowserHistory } from 'history';
import { mockAlertResultList } from './mock_alert_result_list';
describe('alert list tests', () => {
let store: Store<AlertListState, AppAction>;
let store: Store<Immutable<AlertListState>, Immutable<AppAction>>;
let coreStart: ReturnType<typeof coreMock.createStart>;
let depsStart: DepsStartMock;
let history: History<never>;
/**
* A function that waits until a selector returns true.
*/
let selectorIsTrue: (selector: (state: AlertListState) => boolean) => Promise<void>;
let selectorIsTrue: (selector: (state: Immutable<AlertListState>) => boolean) => Promise<void>;
beforeEach(() => {
coreStart = coreMock.createStart();
depsStart = depsStartMock();

View file

@ -15,9 +15,10 @@ import { DepsStartMock, depsStartMock } from '../../mocks';
import { createBrowserHistory } from 'history';
import { uiQueryParams } from './selectors';
import { urlFromQueryParams } from '../../view/alerts/url_from_query_params';
import { Immutable } from '../../../../../common/types';
describe('alert list pagination', () => {
let store: Store<AlertListState, AppAction>;
let store: Store<Immutable<AlertListState>, Immutable<AppAction>>;
let coreStart: ReturnType<typeof coreMock.createStart>;
let depsStart: DepsStartMock;
let history: History<never>;

View file

@ -4,11 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Reducer } from 'redux';
import { AlertListState } from '../../types';
import { AlertListState, ImmutableReducer } from '../../types';
import { AppAction } from '../action';
import { Immutable } from '../../../../../common/types';
const initialState = (): AlertListState => {
const initialState = (): Immutable<AlertListState> => {
return {
alerts: [],
alertDetails: undefined,
@ -22,7 +22,7 @@ const initialState = (): AlertListState => {
};
};
export const alertListReducer: Reducer<AlertListState, AppAction> = (
export const alertListReducer: ImmutableReducer<AlertListState, AppAction> = (
state = initialState(),
action
) => {

View file

@ -19,23 +19,23 @@ const createStructuredSelector: CreateStructuredSelector = createStructuredSelec
/**
* Returns the Alert Data array from state
*/
export const alertListData = (state: AlertListState) => state.alerts;
export const alertListData = (state: Immutable<AlertListState>) => state.alerts;
export const selectedAlertDetailsData = (state: AlertListState) => state.alertDetails;
export const selectedAlertDetailsData = (state: Immutable<AlertListState>) => state.alertDetails;
/**
* Returns the alert list pagination data from state
*/
export const alertListPagination = createStructuredSelector({
pageIndex: (state: AlertListState) => state.pageIndex,
pageSize: (state: AlertListState) => state.pageSize,
total: (state: AlertListState) => state.total,
pageIndex: (state: Immutable<AlertListState>) => state.pageIndex,
pageSize: (state: Immutable<AlertListState>) => state.pageSize,
total: (state: Immutable<AlertListState>) => state.total,
});
/**
* Returns a boolean based on whether or not the user is on the alerts page
*/
export const isOnAlertPage = (state: AlertListState): boolean => {
export const isOnAlertPage = (state: Immutable<AlertListState>): boolean => {
return state.location ? state.location.pathname === '/alerts' : false;
};
@ -44,10 +44,10 @@ export const isOnAlertPage = (state: AlertListState): boolean => {
* Used to calculate urls for links and such.
*/
export const uiQueryParams: (
state: AlertListState
state: Immutable<AlertListState>
) => Immutable<AlertingIndexUIQueryParams> = createSelector(
(state: AlertListState) => state.location,
(location: AlertListState['location']) => {
state => state.location,
(location: Immutable<AlertListState>['location']) => {
const data: AlertingIndexUIQueryParams = {};
if (location) {
// Removes the `?` from the beginning of query string if it exists
@ -82,7 +82,7 @@ export const uiQueryParams: (
* Parses the ui query params and returns a object that represents the query used by the SearchBar component.
* If the query url param is undefined, a default is returned.
*/
export const searchBarQuery: (state: AlertListState) => Query = createSelector(
export const searchBarQuery: (state: Immutable<AlertListState>) => Query = createSelector(
uiQueryParams,
({ query }) => {
if (query !== undefined) {
@ -97,21 +97,20 @@ export const searchBarQuery: (state: AlertListState) => Query = createSelector(
* Parses the ui query params and returns a rison encoded string that represents the search bar's date range.
* A default is provided if 'date_range' is not present in the url params.
*/
export const encodedSearchBarDateRange: (state: AlertListState) => string = createSelector(
uiQueryParams,
({ date_range: dateRange }) => {
if (dateRange === undefined) {
return encode({ from: 'now-24h', to: 'now' });
} else {
return dateRange;
}
export const encodedSearchBarDateRange: (
state: Immutable<AlertListState>
) => string = createSelector(uiQueryParams, ({ date_range: dateRange }) => {
if (dateRange === undefined) {
return encode({ from: 'now-24h', to: 'now' });
} else {
return dateRange;
}
);
});
/**
* Parses the ui query params and returns a object that represents the dateRange used by the SearchBar component.
*/
export const searchBarDateRange: (state: AlertListState) => TimeRange = createSelector(
export const searchBarDateRange: (state: Immutable<AlertListState>) => TimeRange = createSelector(
encodedSearchBarDateRange,
encodedDateRange => {
return (decode(encodedDateRange) as unknown) as TimeRange;
@ -122,7 +121,7 @@ export const searchBarDateRange: (state: AlertListState) => TimeRange = createSe
* Parses the ui query params and returns an array of filters used by the SearchBar component.
* If the 'filters' param is not present, a default is returned.
*/
export const searchBarFilters: (state: AlertListState) => Filter[] = createSelector(
export const searchBarFilters: (state: Immutable<AlertListState>) => Filter[] = createSelector(
uiQueryParams,
({ filters }) => {
if (filters !== undefined) {
@ -136,13 +135,14 @@ export const searchBarFilters: (state: AlertListState) => Filter[] = createSelec
/**
* Returns the indexPatterns used by the SearchBar component
*/
export const searchBarIndexPatterns = (state: AlertListState) => state.searchBar.patterns;
export const searchBarIndexPatterns = (state: Immutable<AlertListState>) =>
state.searchBar.patterns;
/**
* query params to use when requesting alert data.
*/
export const apiQueryParams: (
state: AlertListState
state: Immutable<AlertListState>
) => Immutable<AlertingIndexGetQueryInput> = createSelector(
uiQueryParams,
encodedSearchBarDateRange,
@ -161,7 +161,7 @@ export const apiQueryParams: (
* True if the user has selected an alert to see details about.
* Populated via the browsers query params.
*/
export const hasSelectedAlert: (state: AlertListState) => boolean = createSelector(
export const hasSelectedAlert: (state: Immutable<AlertListState>) => boolean = createSelector(
uiQueryParams,
({ selected_alert: selectedAlert }) => selectedAlert !== undefined
);

View file

@ -27,14 +27,8 @@ interface UserPaginatedHostList {
payload: HostListPagination;
}
// Why is FakeActionWithNoPayload here, see: https://github.com/elastic/endpoint-app-team/issues/273
interface FakeActionWithNoPayload {
type: 'fakeActionWithNoPayLoad';
}
export type HostAction =
| ServerReturnedHostList
| ServerReturnedHostDetails
| ServerFailedToReturnHostDetails
| UserPaginatedHostList
| FakeActionWithNoPayload;
| UserPaginatedHostList;

View file

@ -4,11 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { CoreStart, HttpSetup } from 'kibana/public';
import { applyMiddleware, createStore, Dispatch, Store } from 'redux';
import { applyMiddleware, createStore, Store } from 'redux';
import { coreMock } from '../../../../../../../../src/core/public/mocks';
import { History, createBrowserHistory } from 'history';
import { hostListReducer, hostMiddlewareFactory } from './index';
import { HostResultList } from '../../../../../common/types';
import { HostResultList, Immutable } from '../../../../../common/types';
import { HostListState } from '../../types';
import { AppAction } from '../action';
import { listData } from './selectors';
@ -20,9 +20,10 @@ describe('host list middleware', () => {
let fakeCoreStart: jest.Mocked<CoreStart>;
let depsStart: DepsStartMock;
let fakeHttpServices: jest.Mocked<HttpSetup>;
let store: Store<HostListState>;
let getState: typeof store['getState'];
let dispatch: Dispatch<AppAction>;
type HostListStore = Store<Immutable<HostListState>, Immutable<AppAction>>;
let store: HostListStore;
let getState: HostListStore['getState'];
let dispatch: HostListStore['dispatch'];
let history: History<never>;
const getEndpointListApiResponse = (): HostResultList => {

View file

@ -4,8 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Reducer } from 'redux';
import { HostListState } from '../../types';
import { HostListState, ImmutableReducer } from '../../types';
import { AppAction } from '../action';
const initialState = (): HostListState => {
@ -21,7 +20,7 @@ const initialState = (): HostListState => {
};
};
export const hostListReducer: Reducer<HostListState, AppAction> = (
export const hostListReducer: ImmutableReducer<HostListState, AppAction> = (
state = initialState(),
action
) => {

View file

@ -8,30 +8,30 @@ import { createSelector } from 'reselect';
import { Immutable } from '../../../../../common/types';
import { HostListState, HostIndexUIQueryParams } from '../../types';
export const listData = (state: HostListState) => state.hosts;
export const listData = (state: Immutable<HostListState>) => state.hosts;
export const pageIndex = (state: HostListState) => state.pageIndex;
export const pageIndex = (state: Immutable<HostListState>) => state.pageIndex;
export const pageSize = (state: HostListState) => state.pageSize;
export const pageSize = (state: Immutable<HostListState>) => state.pageSize;
export const totalHits = (state: HostListState) => state.total;
export const totalHits = (state: Immutable<HostListState>) => state.total;
export const isLoading = (state: HostListState) => state.loading;
export const isLoading = (state: Immutable<HostListState>) => state.loading;
export const detailsError = (state: HostListState) => state.detailsError;
export const detailsError = (state: Immutable<HostListState>) => state.detailsError;
export const detailsData = (state: HostListState) => {
export const detailsData = (state: Immutable<HostListState>) => {
return state.details;
};
export const isOnHostPage = (state: HostListState) =>
export const isOnHostPage = (state: Immutable<HostListState>) =>
state.location ? state.location.pathname === '/hosts' : false;
export const uiQueryParams: (
state: HostListState
state: Immutable<HostListState>
) => Immutable<HostIndexUIQueryParams> = createSelector(
(state: HostListState) => state.location,
(location: HostListState['location']) => {
(state: Immutable<HostListState>) => state.location,
(location: Immutable<HostListState>['location']) => {
const data: HostIndexUIQueryParams = {};
if (location) {
// Removes the `?` from the beginning of query string if it exists
@ -52,7 +52,7 @@ export const uiQueryParams: (
}
);
export const hasSelectedHost: (state: HostListState) => boolean = createSelector(
export const hasSelectedHost: (state: Immutable<HostListState>) => boolean = createSelector(
uiQueryParams,
({ selected_host: selectedHost }) => {
return selectedHost !== undefined;

View file

@ -0,0 +1,13 @@
/*
* 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 } from 'redux';
import { ImmutableCombineReducers } from '../types';
/**
* Works the same as `combineReducers` from `redux`, but uses the `ImmutableCombineReducers` type.
*/
export const immutableCombineReducers: ImmutableCombineReducers = combineReducers;

View file

@ -4,8 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { PolicyData, PolicyDetailsState, ServerApiError, UIPolicyConfig } from '../../types';
import { PolicyDetailsState, ServerApiError } from '../../types';
import { GetAgentStatusResponse } from '../../../../../../ingest_manager/common/types/rest_spec';
import { PolicyData, UIPolicyConfig } from '../../../../../common/types';
interface ServerReturnedPolicyDetailsData {
type: '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 { generatePolicy } from '../../models/policy';
import { factory as policyConfigFactory } from '../../../../../common/models/policy_config';
describe('policy details: ', () => {
let store: Store<PolicyDetailsState>;
@ -38,7 +38,7 @@ describe('policy details: ', () => {
streams: [],
config: {
policy: {
value: generatePolicy(),
value: policyConfigFactory(),
},
},
},

View file

@ -4,19 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
MiddlewareFactory,
PolicyData,
PolicyDetailsState,
UpdatePolicyResponse,
} from '../../types';
import { MiddlewareFactory, PolicyDetailsState, UpdatePolicyResponse } from '../../types';
import { policyIdFromParams, isOnPolicyDetailsPage, policyDetails } from './selectors';
import { generatePolicy } from '../../models/policy';
import {
sendGetDatasource,
sendGetFleetAgentStatusForConfig,
sendPutDatasource,
} from '../policy_list/services/ingest';
import { PolicyData } from '../../../../../common/types';
import { factory as policyConfigFactory } from '../../../../../common/models/policy_config';
export const policyDetailsMiddlewareFactory: MiddlewareFactory<PolicyDetailsState> = coreStart => {
const http = coreStart.http;
@ -49,7 +45,7 @@ export const policyDetailsMiddlewareFactory: MiddlewareFactory<PolicyDetailsStat
streams: [],
config: {
policy: {
value: generatePolicy(),
value: policyConfigFactory(),
},
},
},

View file

@ -4,10 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Reducer } from 'redux';
import { PolicyDetailsState, UIPolicyConfig } from '../../types';
import { AppAction } from '../action';
import { fullPolicy, isOnPolicyDetailsPage } from './selectors';
import { PolicyDetailsState, ImmutableReducer } from '../../types';
import { Immutable, PolicyConfig, UIPolicyConfig } from '../../../../../common/types';
const initialPolicyDetailsState = (): PolicyDetailsState => {
return {
@ -23,7 +23,7 @@ const initialPolicyDetailsState = (): PolicyDetailsState => {
};
};
export const policyDetailsReducer: Reducer<PolicyDetailsState, AppAction> = (
export const policyDetailsReducer: ImmutableReducer<PolicyDetailsState, AppAction> = (
state = initialPolicyDetailsState(),
action
) => {
@ -70,7 +70,7 @@ export const policyDetailsReducer: Reducer<PolicyDetailsState, AppAction> = (
}
if (action.type === 'userChangedUrl') {
const newState = {
const newState: Immutable<PolicyDetailsState> = {
...state,
location: action.payload,
};
@ -79,8 +79,10 @@ export const policyDetailsReducer: Reducer<PolicyDetailsState, AppAction> = (
// Did user just enter the Detail page? if so, then set the loading indicator and return new state
if (isCurrentlyOnDetailsPage && !wasPreviouslyOnDetailsPage) {
newState.isLoading = true;
return newState;
return {
...newState,
isLoading: true,
};
}
return {
...initialPolicyDetailsState(),
@ -93,10 +95,19 @@ export const policyDetailsReducer: Reducer<PolicyDetailsState, AppAction> = (
return state;
}
const newState = { ...state, policyItem: { ...state.policyItem } };
const newPolicy: any = { ...fullPolicy(state) };
const newPolicy: PolicyConfig = { ...fullPolicy(state) };
/**
* This is directly changing redux state because `policyItem.inputs` was copied over and not cloned.
*/
// @ts-ignore
newState.policyItem.inputs[0].config.policy.value = newPolicy;
Object.entries(action.payload.policyConfig).forEach(([section, newSettings]) => {
/**
* this is not safe because `action.payload.policyConfig` may have excess keys
*/
// @ts-ignore
newPolicy[section as keyof UIPolicyConfig] = {
...newPolicy[section as keyof UIPolicyConfig],
...newSettings,

View file

@ -5,14 +5,15 @@
*/
import { createSelector } from 'reselect';
import { PolicyConfig, PolicyDetailsState, UIPolicyConfig } from '../../types';
import { generatePolicy } from '../../models/policy';
import { PolicyDetailsState } from '../../types';
import { Immutable, PolicyConfig, UIPolicyConfig } from '../../../../../common/types';
import { factory as policyConfigFactory } from '../../../../../common/models/policy_config';
/** Returns the policy details */
export const policyDetails = (state: PolicyDetailsState) => state.policyItem;
export const policyDetails = (state: Immutable<PolicyDetailsState>) => state.policyItem;
/** Returns a boolean of whether the user is on the policy details page or not */
export const isOnPolicyDetailsPage = (state: PolicyDetailsState) => {
export const isOnPolicyDetailsPage = (state: Immutable<PolicyDetailsState>) => {
if (state.location) {
const pathnameParts = state.location.pathname.split('/');
return pathnameParts[1] === 'policy' && pathnameParts[2];
@ -32,14 +33,16 @@ export const policyIdFromParams: (state: PolicyDetailsState) => string = createS
}
);
const defaultFullPolicy: Immutable<PolicyConfig> = policyConfigFactory();
/**
* 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(
export const fullPolicy: (s: Immutable<PolicyDetailsState>) => PolicyConfig = createSelector(
policyDetails,
policyData => {
return policyData?.inputs[0]?.config?.policy?.value ?? generatePolicy();
return policyData?.inputs[0]?.config?.policy?.value ?? defaultFullPolicy;
}
);

View file

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

View file

@ -5,7 +5,7 @@
*/
import { EndpointAppLocation, PolicyListState } from '../../types';
import { applyMiddleware, createStore, Dispatch, Store } from 'redux';
import { applyMiddleware, createStore, Store } from 'redux';
import { AppAction } from '../action';
import { policyListReducer } from './reducer';
import { policyListMiddlewareFactory } from './middleware';
@ -18,13 +18,15 @@ import {
setPolicyListApiMockImplementation,
} from './test_mock_utils';
import { INGEST_API_DATASOURCES } from './services/ingest';
import { Immutable } from '../../../../../common/types';
describe('policy list store concerns', () => {
let fakeCoreStart: ReturnType<typeof coreMock.createStart>;
let depsStart: DepsStartMock;
let store: Store<PolicyListState>;
let getState: typeof store['getState'];
let dispatch: Dispatch<AppAction>;
type PolicyListStore = Store<Immutable<PolicyListState>, Immutable<AppAction>>;
let store: PolicyListStore;
let getState: PolicyListStore['getState'];
let dispatch: PolicyListStore['dispatch'];
let waitForAction: MiddlewareActionSpyHelper['waitForAction'];
beforeEach(() => {

View file

@ -4,10 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Reducer } from 'redux';
import { PolicyListState } from '../../types';
import { PolicyListState, ImmutableReducer } from '../../types';
import { AppAction } from '../action';
import { isOnPolicyListPage } from './selectors';
import { Immutable } from '../../../../../common/types';
const initialPolicyListState = (): PolicyListState => {
return {
@ -21,7 +21,7 @@ const initialPolicyListState = (): PolicyListState => {
};
};
export const policyListReducer: Reducer<PolicyListState, AppAction> = (
export const policyListReducer: ImmutableReducer<PolicyListState, AppAction> = (
state = initialPolicyListState(),
action
) => {
@ -42,7 +42,7 @@ export const policyListReducer: Reducer<PolicyListState, AppAction> = (
}
if (action.type === 'userChangedUrl') {
const newState = {
const newState: Immutable<PolicyListState> = {
...state,
location: action.payload,
};
@ -53,14 +53,15 @@ export const policyListReducer: Reducer<PolicyListState, AppAction> = (
// Also adjust some state if user is just entering the policy list view
if (isCurrentlyOnListPage) {
if (!wasPreviouslyOnListPage) {
newState.apiError = undefined;
newState.isLoading = true;
return {
...newState,
apiError: undefined,
isLoading: true,
};
}
return newState;
}
return {
...initialPolicyListState(),
};
return initialPolicyListState();
}
return state;

View file

@ -7,32 +7,33 @@
import { createSelector } from 'reselect';
import { parse } from 'query-string';
import { PolicyListState, PolicyListUrlSearchParams } from '../../types';
import { Immutable } from '../../../../../common/types';
const PAGE_SIZES = Object.freeze([10, 20, 50]);
export const selectPolicyItems = (state: PolicyListState) => state.policyItems;
export const selectPolicyItems = (state: Immutable<PolicyListState>) => state.policyItems;
export const selectPageIndex = (state: PolicyListState) => state.pageIndex;
export const selectPageIndex = (state: Immutable<PolicyListState>) => state.pageIndex;
export const selectPageSize = (state: PolicyListState) => state.pageSize;
export const selectPageSize = (state: Immutable<PolicyListState>) => state.pageSize;
export const selectTotal = (state: PolicyListState) => state.total;
export const selectTotal = (state: Immutable<PolicyListState>) => state.total;
export const selectIsLoading = (state: PolicyListState) => state.isLoading;
export const selectIsLoading = (state: Immutable<PolicyListState>) => state.isLoading;
export const selectApiError = (state: PolicyListState) => state.apiError;
export const selectApiError = (state: Immutable<PolicyListState>) => state.apiError;
export const isOnPolicyListPage = (state: PolicyListState) => {
export const isOnPolicyListPage = (state: Immutable<PolicyListState>) => {
return state.location?.pathname === '/policy';
};
const routeLocation = (state: PolicyListState) => state.location;
const routeLocation = (state: Immutable<PolicyListState>) => state.location;
/**
* Returns the supported URL search params, populated with defaults if none where present in the URL
*/
export const urlSearchParams: (
state: PolicyListState
state: Immutable<PolicyListState>
) => PolicyListUrlSearchParams = createSelector(routeLocation, location => {
const searchParams = {
page_index: 0,

View file

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

View file

@ -3,15 +3,16 @@
* 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, Reducer } from 'redux';
import { hostListReducer } from './hosts';
import { AppAction } from './action';
import { alertListReducer } from './alerts';
import { GlobalState } from '../types';
import { GlobalState, ImmutableReducer } from '../types';
import { policyListReducer } from './policy_list';
import { policyDetailsReducer } from './policy_details';
import { immutableCombineReducers } from './immutable_combine_reducers';
export const appReducer: Reducer<GlobalState, AppAction> = combineReducers({
export const appReducer: ImmutableReducer<GlobalState, AppAction> = immutableCombineReducers({
hostList: hostListReducer,
alertList: alertListReducer,
policyList: policyListReducer,

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Dispatch, MiddlewareAPI } from 'redux';
import { Dispatch, MiddlewareAPI, Action as ReduxAction, AnyAction as ReduxAnyAction } from 'redux';
import { IIndexPattern } from 'src/plugins/data/public';
import {
HostMetadata,
@ -13,13 +13,14 @@ import {
Immutable,
ImmutableArray,
AlertDetails,
MalwareFields,
UIPolicyConfig,
PolicyData,
} from '../../../common/types';
import { EndpointPluginStartDependencies } from '../../plugin';
import { AppAction } from './store/action';
import { CoreStart } from '../../../../../../src/core/public';
import {
Datasource,
NewDatasource,
GetAgentStatusResponse,
GetDatasourcesResponse,
GetOneDatasourceResponse,
@ -59,29 +60,6 @@ export interface ServerApiError {
message: string;
}
/**
* New policy data. Used when updating the policy record via ingest APIs
*/
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
*/
@ -192,30 +170,6 @@ interface PolicyConfigAdvancedOptions {
};
}
/**
* Windows-specific policy configuration that is supported via the UI
*/
type WindowsPolicyConfig = Pick<PolicyConfig['windows'], 'events' | 'malware'>;
/**
* Mac-specific policy configuration that is supported via the UI
*/
type MacPolicyConfig = Pick<PolicyConfig['mac'], 'malware' | 'events'>;
/**
* Linux-specific policy configuration that is supported via the UI
*/
type LinuxPolicyConfig = Pick<PolicyConfig['linux'], 'events'>;
/**
* 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 */
export enum OS {
windows = 'windows',
@ -246,20 +200,7 @@ export type KeysByValueCriteria<O, Criteria> = {
}[keyof O];
/** Returns an array of the policy OSes that have a malware protection field */
export type MalwareProtectionOSes = KeysByValueCriteria<UIPolicyConfig, { malware: MalwareFields }>;
/** Policy: Malware protection fields */
export interface MalwareFields {
mode: ProtectionModes;
}
/** Policy protection mode options */
export enum ProtectionModes {
detect = 'detect',
prevent = 'prevent',
preventNotify = 'preventNotify',
off = 'off',
}
export interface GlobalState {
readonly hostList: HostListState;
@ -349,3 +290,28 @@ export interface GetPolicyResponse extends GetOneDatasourceResponse {
export interface UpdatePolicyResponse extends UpdateDatasourceResponse {
item: PolicyData;
}
/**
* Like `Reducer` from `redux` but it accepts immutable versions of `state` and `action`.
* Use this type for all Reducers in order to help enforce our pattern of immutable state.
*/
export type ImmutableReducer<State, Action> = (
state: Immutable<State> | undefined,
action: Immutable<Action>
) => State | Immutable<State>;
/**
* A alternate interface for `redux`'s `combineReducers`. Will work with the same underlying implementation,
* but will enforce that `Immutable` versions of `state` and `action` are received.
*/
export type ImmutableCombineReducers = <S, A extends ReduxAction = ReduxAnyAction>(
reducers: ImmutableReducersMapObject<S, A>
) => ImmutableReducer<S, A>;
/**
* Like `redux`'s `ReducersMapObject` (which is used by `combineReducers`) but enforces that
* the `state` and `action` received are `Immutable` versions.
*/
type ImmutableReducersMapObject<S, A extends ReduxAction = ReduxAction> = {
[K in keyof S]: ImmutableReducer<S[K], A>;
};

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import React, { useMemo } from 'react';
import { memo, useEffect, useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import { encode, RisonValue } from 'rison-node';
@ -14,11 +14,18 @@ import { urlFromQueryParams } from './url_from_query_params';
import { useAlertListSelector } from './hooks/use_alerts_selector';
import * as selectors from '../../store/alerts/selectors';
import { EndpointPluginServices } from '../../../../plugin';
import { clone } from '../../models/index_pattern';
export const AlertIndexSearchBar = memo(() => {
const history = useHistory();
const queryParams = useAlertListSelector(selectors.uiQueryParams);
const searchBarIndexPatterns = useAlertListSelector(selectors.searchBarIndexPatterns);
// Deeply clone the search bar index patterns as the receiving component may mutate them
const clonedSearchBarIndexPatterns = useMemo(
() => searchBarIndexPatterns.map(pattern => clone(pattern)),
[searchBarIndexPatterns]
);
const searchBarQuery = useAlertListSelector(selectors.searchBarQuery);
const searchBarDateRange = useAlertListSelector(selectors.searchBarDateRange);
const searchBarFilters = useAlertListSelector(selectors.searchBarFilters);
@ -68,7 +75,7 @@ export const AlertIndexSearchBar = memo(() => {
dataTestSubj="alertsSearchBar"
appName="endpoint"
isLoading={false}
indexPatterns={searchBarIndexPatterns}
indexPatterns={clonedSearchBarIndexPatterns}
query={searchBarQuery}
dateRangeFrom={searchBarDateRange.from}
dateRangeTo={searchBarDateRange.to}

View file

@ -23,12 +23,14 @@ import { i18n } from '@kbn/i18n';
import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n/react';
import { createStructuredSelector } from 'reselect';
import { EuiBasicTableColumn } from '@elastic/eui';
import { HostDetailsFlyout } from './details';
import * as selectors from '../../store/hosts/selectors';
import { HostAction } from '../../store/hosts/action';
import { useHostListSelector } from './hooks';
import { CreateStructuredSelector } from '../../types';
import { urlFromQueryParams } from './url_from_query_params';
import { HostMetadata, Immutable } from '../../../../../common/types';
const selector = (createStructuredSelector as CreateStructuredSelector)(selectors);
export const HostList = () => {
@ -65,7 +67,7 @@ export const HostList = () => {
[dispatch]
);
const columns = useMemo(() => {
const columns: Array<EuiBasicTableColumn<Immutable<HostMetadata>>> = useMemo(() => {
return [
{
field: '',
@ -174,7 +176,7 @@ export const HostList = () => {
<EuiHorizontalRule margin="xs" />
<EuiBasicTable
data-test-subj="hostListTable"
items={listData}
items={useMemo(() => [...listData], [listData])}
columns={columns}
loading={isLoading}
pagination={paginationSetup}

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import React, { useMemo } from 'react';
import {
EuiCard,
EuiFlexGroup,
@ -25,14 +25,27 @@ const PolicyDetailCard = styled.div`
}
`;
export const ConfigForm: React.FC<{
/**
* A subtitle for this component.
**/
type: string;
supportedOss: string[];
/**
* Types of supported operating systems.
*/
supportedOss: React.ReactNode;
children: React.ReactNode;
id: string;
/** Takes a react component to be put on the right corner of the card */
/**
* A description for the component.
*/
description: string;
/**
* The `data-test-subj` attribute to append to a certain child element.
*/
dataTestSubj: string;
/** React Node to be put on the right corner of the card */
rightCorner: React.ReactNode;
}> = React.memo(({ type, supportedOss, children, id, rightCorner }) => {
const typeTitle = () => {
}> = React.memo(({ type, supportedOss, children, dataTestSubj, rightCorner, description }) => {
const typeTitle = useMemo(() => {
return (
<EuiFlexGroup direction="row" gutterSize="none" alignItems="center">
<EuiFlexGroup direction="column" gutterSize="none">
@ -59,28 +72,25 @@ export const ConfigForm: React.FC<{
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem className="policyDetailTitleFlexItem">
<EuiText>{supportedOss.join(', ')}</EuiText>
<EuiText>{supportedOss}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexItem grow={false}>{rightCorner}</EuiFlexItem>
</EuiFlexGroup>
);
};
}, [rightCorner, supportedOss, type]);
return (
<PolicyDetailCard>
<EuiCard
data-test-subj={id}
description={description}
data-test-subj={dataTestSubj}
textAlign="left"
title={typeTitle()}
description=""
children={
<>
<EuiHorizontalRule margin="m" />
{children}
</>
}
/>
title={typeTitle}
>
<EuiHorizontalRule margin="m" />
{children}
</EuiCard>
</PolicyDetailCard>
);
});

View file

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

View file

@ -8,24 +8,24 @@ import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui';
import { ImmutableArray } from '../../../../../../../common/types';
import { getIn, setIn } from '../../../../models/policy_details_config';
import { EventsCheckbox } from './checkbox';
import { OS, UIPolicyConfig } from '../../../../types';
import { OS } from '../../../../types';
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/types';
export const LinuxEvents = React.memo(() => {
const selected = usePolicyDetailsSelector(selectedLinuxEvents);
const total = usePolicyDetailsSelector(totalLinuxEvents);
const checkboxes: ImmutableArray<{
name: string;
os: 'linux';
protectionField: keyof UIPolicyConfig['linux']['events'];
}> = useMemo(
() => [
const checkboxes = useMemo(() => {
const items: Array<{
name: string;
os: 'linux';
protectionField: keyof UIPolicyConfig['linux']['events'];
}> = [
{
name: i18n.translate('xpack.endpoint.policyDetailsConfig.linux.events.file', {
defaultMessage: 'File',
@ -47,11 +47,7 @@ export const LinuxEvents = React.memo(() => {
os: OS.linux,
protectionField: 'network',
},
],
[]
);
const renderCheckboxes = useMemo(() => {
];
return (
<>
<EuiTitle size="xxs">
@ -63,7 +59,7 @@ export const LinuxEvents = React.memo(() => {
</h5>
</EuiTitle>
<EuiSpacer size="s" />
{checkboxes.map((item, index) => {
{items.map((item, index) => {
return (
<EventsCheckbox
name={item.name}
@ -77,7 +73,7 @@ export const LinuxEvents = React.memo(() => {
})}
</>
);
}, [checkboxes]);
}, []);
const collectionsEnabled = useMemo(() => {
return (
@ -96,13 +92,16 @@ export const LinuxEvents = React.memo(() => {
type={i18n.translate('xpack.endpoint.policy.details.eventCollection', {
defaultMessage: 'Event Collection',
})}
supportedOss={useMemo(
() => [i18n.translate('xpack.endpoint.policy.details.linux', { defaultMessage: 'Linux' })],
[]
)}
id="linuxEventsForm"
description={i18n.translate('xpack.endpoint.policy.details.eventCollectionLabel', {
defaultMessage: 'Event Collection',
})}
supportedOss={i18n.translate('xpack.endpoint.policy.details.linux', {
defaultMessage: 'Linux',
})}
dataTestSubj="linuxEventingForm"
rightCorner={collectionsEnabled}
children={renderCheckboxes}
/>
>
{checkboxes}
</ConfigForm>
);
});

View file

@ -8,24 +8,24 @@ import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui';
import { ImmutableArray } from '../../../../../../../common/types';
import { getIn, setIn } from '../../../../models/policy_details_config';
import { EventsCheckbox } from './checkbox';
import { OS, UIPolicyConfig } from '../../../../types';
import { OS } from '../../../../types';
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/types';
export const MacEvents = React.memo(() => {
const selected = usePolicyDetailsSelector(selectedMacEvents);
const total = usePolicyDetailsSelector(totalMacEvents);
const checkboxes: ImmutableArray<{
name: string;
os: 'mac';
protectionField: keyof UIPolicyConfig['mac']['events'];
}> = useMemo(
() => [
const checkboxes = useMemo(() => {
const items: Array<{
name: string;
os: 'mac';
protectionField: keyof UIPolicyConfig['mac']['events'];
}> = [
{
name: i18n.translate('xpack.endpoint.policyDetailsConfig.mac.events.file', {
defaultMessage: 'File',
@ -47,11 +47,7 @@ export const MacEvents = React.memo(() => {
os: OS.mac,
protectionField: 'network',
},
],
[]
);
const renderCheckboxes = useMemo(() => {
];
return (
<>
<EuiTitle size="xxs">
@ -63,7 +59,7 @@ export const MacEvents = React.memo(() => {
</h5>
</EuiTitle>
<EuiSpacer size="s" />
{checkboxes.map((item, index) => {
{items.map((item, index) => {
return (
<EventsCheckbox
name={item.name}
@ -77,7 +73,7 @@ export const MacEvents = React.memo(() => {
})}
</>
);
}, [checkboxes]);
}, []);
const collectionsEnabled = useMemo(() => {
return (
@ -96,13 +92,14 @@ export const MacEvents = React.memo(() => {
type={i18n.translate('xpack.endpoint.policy.details.eventCollection', {
defaultMessage: 'Event Collection',
})}
supportedOss={useMemo(
() => [i18n.translate('xpack.endpoint.policy.details.mac', { defaultMessage: 'Mac' })],
[]
)}
id="macEventsForm"
description={i18n.translate('xpack.endpoint.policy.details.eventCollectionLabel', {
defaultMessage: 'Event Collection',
})}
supportedOss={i18n.translate('xpack.endpoint.policy.details.mac', { defaultMessage: 'Mac' })}
dataTestSubj="macEventingForm"
rightCorner={collectionsEnabled}
children={renderCheckboxes}
/>
>
{checkboxes}
</ConfigForm>
);
});

View file

@ -8,27 +8,27 @@ import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui';
import { ImmutableArray } from '../../../../../../../common/types';
import { setIn, getIn } from '../../../../models/policy_details_config';
import { EventsCheckbox } from './checkbox';
import { OS, UIPolicyConfig } from '../../../../types';
import { OS } from '../../../../types';
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, ImmutableArray } from '../../../../../../../common/types';
export const WindowsEvents = React.memo(() => {
const selected = usePolicyDetailsSelector(selectedWindowsEvents);
const total = usePolicyDetailsSelector(totalWindowsEvents);
const checkboxes: ImmutableArray<{
name: string;
os: 'windows';
protectionField: keyof UIPolicyConfig['windows']['events'];
}> = useMemo(
() => [
const checkboxes = useMemo(() => {
const items: ImmutableArray<{
name: string;
os: 'windows';
protectionField: keyof UIPolicyConfig['windows']['events'];
}> = [
{
name: i18n.translate('xpack.endpoint.policyDetailsConfig.windows.events.dllDriverLoad', {
defaultMessage: 'DLL and Driver Load',
@ -78,11 +78,7 @@ export const WindowsEvents = React.memo(() => {
os: OS.windows,
protectionField: 'security',
},
],
[]
);
const renderCheckboxes = useMemo(() => {
];
return (
<>
<EuiTitle size="xxs">
@ -94,7 +90,7 @@ export const WindowsEvents = React.memo(() => {
</h5>
</EuiTitle>
<EuiSpacer size="s" />
{checkboxes.map((item, index) => {
{items.map((item, index) => {
return (
<EventsCheckbox
name={item.name}
@ -108,7 +104,7 @@ export const WindowsEvents = React.memo(() => {
})}
</>
);
}, [checkboxes]);
}, []);
const collectionsEnabled = useMemo(() => {
return (
@ -127,15 +123,16 @@ export const WindowsEvents = React.memo(() => {
type={i18n.translate('xpack.endpoint.policy.details.eventCollection', {
defaultMessage: 'Event Collection',
})}
supportedOss={useMemo(
() => [
i18n.translate('xpack.endpoint.policy.details.windows', { defaultMessage: 'Windows' }),
],
[]
)}
id="windowsEventsForm"
description={i18n.translate('xpack.endpoint.policy.details.windowsLabel', {
defaultMessage: 'Windows',
})}
supportedOss={i18n.translate('xpack.endpoint.policy.details.windows', {
defaultMessage: 'Windows',
})}
dataTestSubj="windowsEventingForm"
rightCorner={collectionsEnabled}
children={renderCheckboxes}
/>
>
{checkboxes}
</ConfigForm>
);
});

View file

@ -11,8 +11,8 @@ import { EuiRadio, EuiSwitch, EuiTitle, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { htmlIdGenerator } from '@elastic/eui';
import { Immutable } from '../../../../../../../common/types';
import { OS, ProtectionModes, MalwareProtectionOSes } from '../../../../types';
import { Immutable, ProtectionModes, ImmutableArray } from '../../../../../../../common/types';
import { OS, MalwareProtectionOSes } from '../../../../types';
import { ConfigForm } from '../config_form';
import { policyConfig } from '../../../../store/policy_details/selectors';
import { usePolicyDetailsSelector } from '../../policy_hooks';
@ -73,7 +73,7 @@ export const MalwareProtections = React.memo(() => {
// currently just taking windows.malware, but both windows.malware and mac.malware should be the same value
const selected = policyDetailsConfig && policyDetailsConfig.windows.malware.mode;
const radios: Array<{
const radios: ImmutableArray<{
id: ProtectionModes;
label: string;
protection: 'malware';
@ -123,7 +123,7 @@ export const MalwareProtections = React.memo(() => {
[dispatch, policyDetailsConfig]
);
const RadioButtons = () => {
const radioButtons = useMemo(() => {
return (
<>
<EuiTitle size="xxxs">
@ -148,9 +148,9 @@ export const MalwareProtections = React.memo(() => {
</ProtectionRadioGroup>
</>
);
};
}, [radios]);
const ProtectionSwitch = () => {
const protectionSwitch = useMemo(() => {
return (
<EuiSwitch
label={i18n.translate('xpack.endpoint.policy.details.malwareProtectionsEnabled', {
@ -163,18 +163,21 @@ export const MalwareProtections = React.memo(() => {
onChange={handleSwitchChange}
/>
);
};
}, [handleSwitchChange, selected]);
return (
<ConfigForm
type={i18n.translate('xpack.endpoint.policy.details.malware', { defaultMessage: 'Malware' })}
supportedOss={[
i18n.translate('xpack.endpoint.policy.details.windows', { defaultMessage: 'Windows' }),
i18n.translate('xpack.endpoint.policy.details.mac', { defaultMessage: 'Mac' }),
]}
id="malwareProtectionsForm"
rightCorner={ProtectionSwitch()}
children={RadioButtons()}
/>
supportedOss={i18n.translate('xpack.endpoint.policy.details.windowsAndMac', {
defaultMessage: 'Windows, Mac',
})}
dataTestSubj="malwareProtectionsForm"
description={i18n.translate('xpack.endpoint.policy.details.malwareLabel', {
defaultMessage: 'Malware',
})}
rightCorner={protectionSwitch}
>
{radioButtons}
</ConfigForm>
);
});

View file

@ -20,10 +20,10 @@ import {
} from '../../store/policy_list/selectors';
import { usePolicyListSelector } from './policy_hooks';
import { PolicyListAction } from '../../store/policy_list';
import { PolicyData } from '../../types';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { PageView } from '../components/page_view';
import { LinkToApp } from '../components/link_to_app';
import { Immutable, PolicyData } from '../../../../../common/types';
interface TableChangeCallbackArguments {
page: { index: number; size: number };
@ -44,8 +44,8 @@ const PolicyLink: React.FC<{ name: string; route: string }> = ({ name, route })
);
};
const renderPolicyNameLink = (value: string, _item: PolicyData) => {
return <PolicyLink name={value} route={`/policy/${_item.id}`} />;
const renderPolicyNameLink = (value: string, item: Immutable<PolicyData>) => {
return <PolicyLink name={value} route={`/policy/${item.id}`} />;
};
export const PolicyList = React.memo(() => {
@ -88,7 +88,7 @@ export const PolicyList = React.memo(() => {
[history, location.pathname]
);
const columns: Array<EuiTableFieldDataColumnType<PolicyData>> = useMemo(
const columns: Array<EuiTableFieldDataColumnType<Immutable<PolicyData>>> = useMemo(
() => [
{
field: 'name',
@ -160,7 +160,7 @@ export const PolicyList = React.memo(() => {
}
>
<EuiBasicTable
items={policyItems}
items={useMemo(() => [...policyItems], [policyItems])}
columns={columns}
loading={loading}
pagination={paginationSetup}