Trusted Apps Signer UI (#84628) (#85413)

* Added default value for type parameter in ConditionEntry type.

* Added signer field UI. Flattened a bit component structure and reused some translations.

* Reverted the condition for signer option.

* Fixed the import.

* Removed unused translations.

* Fixed the test.

* Consolidated a bit the deletion and creation flows in redux.
This commit is contained in:
Bohdan Tsymbala 2020-12-09 17:07:48 +01:00 committed by GitHub
parent 2bd250108e
commit 1094b751fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 409 additions and 397 deletions

View file

@ -58,7 +58,7 @@ const createNewTrustedAppForOsScheme = <O extends OperatingSystem, F extends Con
for (const entry of entries) {
// unfortunately combination of generics and Type<...> for "field" causes type errors
const { field, value } = entry as ConditionEntry<ConditionEntryField>;
const { field, value } = entry as ConditionEntry;
if (usedFields.has(field)) {
return `[${entryFieldLabels[field]}] field can only be used once`;

View file

@ -39,7 +39,7 @@ export enum ConditionEntryField {
SIGNER = 'process.Ext.code_signature',
}
export interface ConditionEntry<T extends ConditionEntryField> {
export interface ConditionEntry<T extends ConditionEntryField = ConditionEntryField> {
field: T;
type: 'match';
operator: 'included';

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ServerApiError } from '../../../../common/types';
import { NewTrustedApp, TrustedApp } from '../../../../../common/endpoint/types/trusted_apps';
import { AsyncResourceState } from '.';
@ -23,24 +22,6 @@ export interface TrustedAppsListData {
totalItemsCount: number;
}
/** Store State when an API request has been sent to create a new trusted app entry */
export interface TrustedAppCreatePending {
type: 'pending';
data: NewTrustedApp;
}
/** Store State when creation of a new Trusted APP entry was successful */
export interface TrustedAppCreateSuccess {
type: 'success';
data: TrustedApp;
}
/** Store State when creation of a new Trusted App Entry failed */
export interface TrustedAppCreateFailure {
type: 'failure';
data: ServerApiError;
}
export type ViewType = 'list' | 'grid';
export interface TrustedAppsListPageLocation {
@ -60,11 +41,14 @@ export interface TrustedAppsListPageState {
confirmed: boolean;
submissionResourceState: AsyncResourceState;
};
createView:
| undefined
| TrustedAppCreatePending
| TrustedAppCreateSuccess
| TrustedAppCreateFailure;
creationDialog: {
formState?: {
entry: NewTrustedApp;
isValid: boolean;
};
confirmed: boolean;
submissionResourceState: AsyncResourceState<TrustedApp>;
};
location: TrustedAppsListPageLocation;
active: boolean;
}

View file

@ -4,50 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
TrustedAppCreatePending,
TrustedAppsListPageState,
TrustedAppCreateFailure,
TrustedAppCreateSuccess,
} from './trusted_apps_list_page_state';
import {
ConditionEntry,
ConditionEntryField,
Immutable,
MacosLinuxConditionEntry,
WindowsConditionEntry,
} from '../../../../../common/endpoint/types';
type CreateViewPossibleStates =
| TrustedAppsListPageState['createView']
| Immutable<TrustedAppsListPageState['createView']>;
export const isTrustedAppCreatePendingState = (
data: CreateViewPossibleStates
): data is TrustedAppCreatePending => {
return data?.type === 'pending';
};
export const isTrustedAppCreateSuccessState = (
data: CreateViewPossibleStates
): data is TrustedAppCreateSuccess => {
return data?.type === 'success';
};
export const isTrustedAppCreateFailureState = (
data: CreateViewPossibleStates
): data is TrustedAppCreateFailure => {
return data?.type === 'failure';
};
export const isWindowsTrustedAppCondition = (
condition: ConditionEntry<ConditionEntryField>
condition: ConditionEntry
): condition is WindowsConditionEntry => {
return condition.field === ConditionEntryField.SIGNER || true;
};
export const isMacosLinuxTrustedAppCondition = (
condition: ConditionEntry<ConditionEntryField>
condition: ConditionEntry
): condition is MacosLinuxConditionEntry => {
return condition.field !== ConditionEntryField.SIGNER;
};

View file

@ -6,14 +6,8 @@
import { Action } from 'redux';
import { TrustedApp } from '../../../../../common/endpoint/types';
import {
AsyncResourceState,
TrustedAppCreateFailure,
TrustedAppCreatePending,
TrustedAppCreateSuccess,
TrustedAppsListData,
} from '../state';
import { NewTrustedApp, TrustedApp } from '../../../../../common/endpoint/types';
import { AsyncResourceState, TrustedAppsListData } from '../state';
export type TrustedAppsListDataOutdated = Action<'trustedAppsListDataOutdated'>;
@ -38,20 +32,27 @@ export type TrustedAppDeletionDialogConfirmed = Action<'trustedAppDeletionDialog
export type TrustedAppDeletionDialogClosed = Action<'trustedAppDeletionDialogClosed'>;
export interface UserClickedSaveNewTrustedAppButton {
type: 'userClickedSaveNewTrustedAppButton';
payload: TrustedAppCreatePending;
}
export type TrustedAppCreationSubmissionResourceStateChanged = ResourceStateChanged<
'trustedAppCreationSubmissionResourceStateChanged',
TrustedApp
>;
export interface ServerReturnedCreateTrustedAppSuccess {
type: 'serverReturnedCreateTrustedAppSuccess';
payload: TrustedAppCreateSuccess;
}
export type TrustedAppCreationDialogStarted = Action<'trustedAppCreationDialogStarted'> & {
payload: {
entry: NewTrustedApp;
};
};
export interface ServerReturnedCreateTrustedAppFailure {
type: 'serverReturnedCreateTrustedAppFailure';
payload: TrustedAppCreateFailure;
}
export type TrustedAppCreationDialogFormStateUpdated = Action<'trustedAppCreationDialogFormStateUpdated'> & {
payload: {
entry: NewTrustedApp;
isValid: boolean;
};
};
export type TrustedAppCreationDialogConfirmed = Action<'trustedAppCreationDialogConfirmed'>;
export type TrustedAppCreationDialogClosed = Action<'trustedAppCreationDialogClosed'>;
export type TrustedAppsPageAction =
| TrustedAppsListDataOutdated
@ -60,6 +61,8 @@ export type TrustedAppsPageAction =
| TrustedAppDeletionDialogStarted
| TrustedAppDeletionDialogConfirmed
| TrustedAppDeletionDialogClosed
| UserClickedSaveNewTrustedAppButton
| ServerReturnedCreateTrustedAppSuccess
| ServerReturnedCreateTrustedAppFailure;
| TrustedAppCreationSubmissionResourceStateChanged
| TrustedAppCreationDialogStarted
| TrustedAppCreationDialogFormStateUpdated
| TrustedAppCreationDialogConfirmed
| TrustedAppCreationDialogClosed;

View file

@ -0,0 +1,56 @@
/*
* 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 {
ConditionEntry,
ConditionEntryField,
NewTrustedApp,
OperatingSystem,
} from '../../../../../common/endpoint/types';
import { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../../common/constants';
import { TrustedAppsListPageState } from '../state';
export const defaultConditionEntry = (): ConditionEntry<ConditionEntryField.HASH> => ({
field: ConditionEntryField.HASH,
operator: 'included',
type: 'match',
value: '',
});
export const defaultNewTrustedApp = (): NewTrustedApp => ({
name: '',
os: OperatingSystem.WINDOWS,
entries: [defaultConditionEntry()],
description: '',
});
export const initialDeletionDialogState = (): TrustedAppsListPageState['deletionDialog'] => ({
confirmed: false,
submissionResourceState: { type: 'UninitialisedResourceState' },
});
export const initialCreationDialogState = (): TrustedAppsListPageState['creationDialog'] => ({
confirmed: false,
submissionResourceState: { type: 'UninitialisedResourceState' },
});
export const initialTrustedAppsPageState = (): TrustedAppsListPageState => ({
listView: {
listResourceState: { type: 'UninitialisedResourceState' },
freshDataTimestamp: Date.now(),
},
deletionDialog: initialDeletionDialogState(),
creationDialog: initialCreationDialogState(),
location: {
page_index: MANAGEMENT_DEFAULT_PAGE,
page_size: MANAGEMENT_DEFAULT_PAGE_SIZE,
show: undefined,
view_type: 'grid',
},
active: false,
});

View file

@ -21,7 +21,8 @@ import {
import { TrustedAppsService } from '../service';
import { Pagination, TrustedAppsListPageState } from '../state';
import { initialTrustedAppsPageState, trustedAppsPageReducer } from './reducer';
import { initialTrustedAppsPageState } from './builders';
import { trustedAppsPageReducer } from './reducer';
import { createTrustedAppsPageMiddleware } from './middleware';
const initialNow = 111111;

View file

@ -4,7 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Immutable, PostTrustedAppCreateRequest } from '../../../../../common/endpoint/types';
import {
Immutable,
PostTrustedAppCreateRequest,
TrustedApp,
} from '../../../../../common/endpoint/types';
import { AppAction } from '../../../../common/store/actions';
import {
ImmutableMiddleware,
@ -23,7 +27,10 @@ import {
TrustedAppsListPageState,
} from '../state';
import { defaultNewTrustedApp } from './builders';
import {
TrustedAppCreationSubmissionResourceStateChanged,
TrustedAppDeletionSubmissionResourceStateChanged,
TrustedAppsListResourceStateChanged,
} from './action';
@ -35,9 +42,11 @@ import {
getLastLoadedListResourceState,
getCurrentLocationPageIndex,
getCurrentLocationPageSize,
getTrustedAppCreateData,
isCreatePending,
needsRefreshOfListData,
getCreationSubmissionResourceState,
getCreationDialogFormEntry,
isCreationDialogLocation,
isCreationDialogFormValid,
} from './selectors';
const createTrustedAppsListResourceStateChangedAction = (
@ -101,6 +110,71 @@ const createTrustedAppDeletionSubmissionResourceStateChanged = (
payload: { newState },
});
const updateCreationDialogIfNeeded = (
store: ImmutableMiddlewareAPI<TrustedAppsListPageState, AppAction>
) => {
const newEntry = getCreationDialogFormEntry(store.getState());
const shouldShow = isCreationDialogLocation(store.getState());
if (shouldShow && !newEntry) {
store.dispatch({
type: 'trustedAppCreationDialogStarted',
payload: { entry: defaultNewTrustedApp() },
});
} else if (!shouldShow && newEntry) {
store.dispatch({
type: 'trustedAppCreationDialogClosed',
});
}
};
const createTrustedAppCreationSubmissionResourceStateChanged = (
newState: Immutable<AsyncResourceState<TrustedApp>>
): Immutable<TrustedAppCreationSubmissionResourceStateChanged> => ({
type: 'trustedAppCreationSubmissionResourceStateChanged',
payload: { newState },
});
const submitCreationIfNeeded = async (
store: ImmutableMiddlewareAPI<TrustedAppsListPageState, AppAction>,
trustedAppsService: TrustedAppsService
) => {
const submissionResourceState = getCreationSubmissionResourceState(store.getState());
const isValid = isCreationDialogFormValid(store.getState());
const entry = getCreationDialogFormEntry(store.getState());
if (isStaleResourceState(submissionResourceState) && entry !== undefined && isValid) {
store.dispatch(
createTrustedAppCreationSubmissionResourceStateChanged({
type: 'LoadingResourceState',
previousState: submissionResourceState,
})
);
try {
store.dispatch(
createTrustedAppCreationSubmissionResourceStateChanged({
type: 'LoadedResourceState',
// TODO: try to remove the cast
data: (await trustedAppsService.createTrustedApp(entry as PostTrustedAppCreateRequest))
.data,
})
);
store.dispatch({
type: 'trustedAppsListDataOutdated',
});
} catch (error) {
store.dispatch(
createTrustedAppCreationSubmissionResourceStateChanged({
type: 'FailedResourceState',
error,
lastLoadedState: getLastLoadedResourceState(submissionResourceState),
})
);
}
}
};
const submitDeletionIfNeeded = async (
store: ImmutableMiddlewareAPI<TrustedAppsListPageState, AppAction>,
trustedAppsService: TrustedAppsService
@ -143,40 +217,6 @@ const submitDeletionIfNeeded = async (
}
};
const createTrustedApp = async (
store: ImmutableMiddlewareAPI<TrustedAppsListPageState, AppAction>,
trustedAppsService: TrustedAppsService
) => {
const { dispatch, getState } = store;
if (isCreatePending(getState())) {
try {
const newTrustedApp = getTrustedAppCreateData(getState());
const createdTrustedApp = (
await trustedAppsService.createTrustedApp(newTrustedApp as PostTrustedAppCreateRequest)
).data;
dispatch({
type: 'serverReturnedCreateTrustedAppSuccess',
payload: {
type: 'success',
data: createdTrustedApp,
},
});
store.dispatch({
type: 'trustedAppsListDataOutdated',
});
} catch (error) {
dispatch({
type: 'serverReturnedCreateTrustedAppFailure',
payload: {
type: 'failure',
data: error.body || error,
},
});
}
}
};
export const createTrustedAppsPageMiddleware = (
trustedAppsService: TrustedAppsService
): ImmutableMiddleware<TrustedAppsListPageState, AppAction> => {
@ -188,12 +228,16 @@ export const createTrustedAppsPageMiddleware = (
await refreshListIfNeeded(store, trustedAppsService);
}
if (action.type === 'trustedAppDeletionDialogConfirmed') {
await submitDeletionIfNeeded(store, trustedAppsService);
if (action.type === 'userChangedUrl') {
updateCreationDialogIfNeeded(store);
}
if (action.type === 'userClickedSaveNewTrustedAppButton') {
createTrustedApp(store, trustedAppsService);
if (action.type === 'trustedAppCreationDialogConfirmed') {
await submitCreationIfNeeded(store, trustedAppsService);
}
if (action.type === 'trustedAppDeletionDialogConfirmed') {
await submitDeletionIfNeeded(store, trustedAppsService);
}
};
};

View file

@ -5,7 +5,8 @@
*/
import { AsyncResourceState } from '../state';
import { initialTrustedAppsPageState, trustedAppsPageReducer } from './reducer';
import { initialTrustedAppsPageState } from './builders';
import { trustedAppsPageReducer } from './reducer';
import {
createSampleTrustedApp,
createListLoadedResourceState,

View file

@ -13,25 +13,28 @@ import { UserChangedUrl } from '../../../../common/store/routing/action';
import { AppAction } from '../../../../common/store/actions';
import { extractTrustedAppsListPageLocation } from '../../../common/routing';
import {
MANAGEMENT_ROUTING_TRUSTED_APPS_PATH,
MANAGEMENT_DEFAULT_PAGE,
MANAGEMENT_DEFAULT_PAGE_SIZE,
} from '../../../common/constants';
import { MANAGEMENT_ROUTING_TRUSTED_APPS_PATH } from '../../../common/constants';
import {
TrustedAppDeletionDialogClosed,
TrustedAppDeletionDialogConfirmed,
TrustedAppDeletionDialogStarted,
TrustedAppDeletionSubmissionResourceStateChanged,
TrustedAppCreationSubmissionResourceStateChanged,
TrustedAppsListDataOutdated,
TrustedAppsListResourceStateChanged,
ServerReturnedCreateTrustedAppFailure,
ServerReturnedCreateTrustedAppSuccess,
UserClickedSaveNewTrustedAppButton,
TrustedAppCreationDialogStarted,
TrustedAppCreationDialogFormStateUpdated,
TrustedAppCreationDialogConfirmed,
TrustedAppCreationDialogClosed,
} from './action';
import { TrustedAppsListPageState } from '../state';
import {
initialCreationDialogState,
initialDeletionDialogState,
initialTrustedAppsPageState,
} from './builders';
type StateReducer = ImmutableReducer<TrustedAppsListPageState, AppAction>;
type CaseReducer<T extends AppAction> = (
@ -49,26 +52,14 @@ const isTrustedAppsPageLocation = (location: Immutable<AppLocation>) => {
};
const trustedAppsListDataOutdated: CaseReducer<TrustedAppsListDataOutdated> = (state, action) => {
return {
...state,
listView: {
...state.listView,
freshDataTimestamp: Date.now(),
},
};
return { ...state, listView: { ...state.listView, freshDataTimestamp: Date.now() } };
};
const trustedAppsListResourceStateChanged: CaseReducer<TrustedAppsListResourceStateChanged> = (
state,
action
) => {
return {
...state,
listView: {
...state.listView,
listResourceState: action.payload.newState,
},
};
return { ...state, listView: { ...state.listView, listResourceState: action.payload.newState } };
};
const trustedAppDeletionSubmissionResourceStateChanged: CaseReducer<TrustedAppDeletionSubmissionResourceStateChanged> = (
@ -85,78 +76,72 @@ const trustedAppDeletionDialogStarted: CaseReducer<TrustedAppDeletionDialogStart
state,
action
) => {
return {
...state,
deletionDialog: {
entry: action.payload.entry,
confirmed: false,
submissionResourceState: { type: 'UninitialisedResourceState' },
},
};
return { ...state, deletionDialog: { ...initialDeletionDialogState(), ...action.payload } };
};
const trustedAppDeletionDialogConfirmed: CaseReducer<TrustedAppDeletionDialogConfirmed> = (
state,
action
state
) => {
return { ...state, deletionDialog: { ...state.deletionDialog, confirmed: true } };
};
const trustedAppDeletionDialogClosed: CaseReducer<TrustedAppDeletionDialogClosed> = (
const trustedAppDeletionDialogClosed: CaseReducer<TrustedAppDeletionDialogClosed> = (state) => {
return { ...state, deletionDialog: initialDeletionDialogState() };
};
const trustedAppCreationSubmissionResourceStateChanged: CaseReducer<TrustedAppCreationSubmissionResourceStateChanged> = (
state,
action
) => {
return { ...state, deletionDialog: initialDeletionDialogState() };
return {
...state,
creationDialog: { ...state.creationDialog, submissionResourceState: action.payload.newState },
};
};
const trustedAppCreationDialogStarted: CaseReducer<TrustedAppCreationDialogStarted> = (
state,
action
) => {
return {
...state,
creationDialog: {
...initialCreationDialogState(),
formState: { ...action.payload, isValid: true },
},
};
};
const trustedAppCreationDialogFormStateUpdated: CaseReducer<TrustedAppCreationDialogFormStateUpdated> = (
state,
action
) => {
return {
...state,
creationDialog: { ...state.creationDialog, formState: { ...action.payload } },
};
};
const trustedAppCreationDialogConfirmed: CaseReducer<TrustedAppCreationDialogConfirmed> = (
state
) => {
return { ...state, creationDialog: { ...state.creationDialog, confirmed: true } };
};
const trustedAppCreationDialogClosed: CaseReducer<TrustedAppCreationDialogClosed> = (state) => {
return { ...state, creationDialog: initialCreationDialogState() };
};
const userChangedUrl: CaseReducer<UserChangedUrl> = (state, action) => {
if (isTrustedAppsPageLocation(action.payload)) {
const parsedUrlsParams = parse(action.payload.search.slice(1));
const location = extractTrustedAppsListPageLocation(parsedUrlsParams);
const location = extractTrustedAppsListPageLocation(parse(action.payload.search.slice(1)));
return {
...state,
createView: location.show ? state.createView : undefined,
active: true,
location,
};
return { ...state, active: true, location };
} else {
return initialTrustedAppsPageState();
}
};
const trustedAppsCreateResourceChanged: CaseReducer<
| UserClickedSaveNewTrustedAppButton
| ServerReturnedCreateTrustedAppFailure
| ServerReturnedCreateTrustedAppSuccess
> = (state, action) => {
return {
...state,
createView: action.payload,
};
};
const initialDeletionDialogState = (): TrustedAppsListPageState['deletionDialog'] => ({
confirmed: false,
submissionResourceState: { type: 'UninitialisedResourceState' },
});
export const initialTrustedAppsPageState = (): TrustedAppsListPageState => ({
listView: {
listResourceState: { type: 'UninitialisedResourceState' },
freshDataTimestamp: Date.now(),
},
deletionDialog: initialDeletionDialogState(),
createView: undefined,
location: {
page_index: MANAGEMENT_DEFAULT_PAGE,
page_size: MANAGEMENT_DEFAULT_PAGE_SIZE,
show: undefined,
view_type: 'grid',
},
active: false,
});
export const trustedAppsPageReducer: StateReducer = (
state = initialTrustedAppsPageState(),
action
@ -180,13 +165,23 @@ export const trustedAppsPageReducer: StateReducer = (
case 'trustedAppDeletionDialogClosed':
return trustedAppDeletionDialogClosed(state, action);
case 'trustedAppCreationSubmissionResourceStateChanged':
return trustedAppCreationSubmissionResourceStateChanged(state, action);
case 'trustedAppCreationDialogStarted':
return trustedAppCreationDialogStarted(state, action);
case 'trustedAppCreationDialogFormStateUpdated':
return trustedAppCreationDialogFormStateUpdated(state, action);
case 'trustedAppCreationDialogConfirmed':
return trustedAppCreationDialogConfirmed(state, action);
case 'trustedAppCreationDialogClosed':
return trustedAppCreationDialogClosed(state, action);
case 'userChangedUrl':
return userChangedUrl(state, action);
case 'userClickedSaveNewTrustedAppButton':
case 'serverReturnedCreateTrustedAppSuccess':
case 'serverReturnedCreateTrustedAppFailure':
return trustedAppsCreateResourceChanged(state, action);
}
return state;

View file

@ -9,7 +9,7 @@ import {
TrustedAppsListPageLocation,
TrustedAppsListPageState,
} from '../state';
import { initialTrustedAppsPageState } from './reducer';
import { initialTrustedAppsPageState } from './builders';
import {
getListResourceState,
getLastLoadedListResourceState,

View file

@ -18,16 +18,10 @@ import {
isOutdatedResourceState,
LoadedResourceState,
Pagination,
TrustedAppCreateFailure,
TrustedAppsListData,
TrustedAppsListPageLocation,
TrustedAppsListPageState,
} from '../state';
import {
isTrustedAppCreateFailureState,
isTrustedAppCreatePendingState,
isTrustedAppCreateSuccessState,
} from '../state/type_guards';
export const needsRefreshOfListData = (state: Immutable<TrustedAppsListPageState>): boolean => {
const freshDataTimestamp = state.listView.freshDataTimestamp;
@ -133,26 +127,38 @@ export const getDeletionDialogEntry = (
return state.deletionDialog.entry;
};
export const isCreatePending: (state: Immutable<TrustedAppsListPageState>) => boolean = ({
createView,
}) => {
return isTrustedAppCreatePendingState(createView);
export const isCreationDialogLocation = (state: Immutable<TrustedAppsListPageState>): boolean => {
return state.location.show === 'create';
};
export const getTrustedAppCreateData: (
export const getCreationSubmissionResourceState = (
state: Immutable<TrustedAppsListPageState>
) => undefined | Immutable<NewTrustedApp> = ({ createView }) => {
return (isTrustedAppCreatePendingState(createView) && createView.data) || undefined;
): Immutable<AsyncResourceState<TrustedApp>> => {
return state.creationDialog.submissionResourceState;
};
export const getApiCreateErrors: (
export const getCreationDialogFormEntry = (
state: Immutable<TrustedAppsListPageState>
) => undefined | TrustedAppCreateFailure['data'] = ({ createView }) => {
return (isTrustedAppCreateFailureState(createView) && createView.data) || undefined;
): Immutable<NewTrustedApp> | undefined => {
return state.creationDialog.formState?.entry;
};
export const wasCreateSuccessful: (state: Immutable<TrustedAppsListPageState>) => boolean = ({
createView,
}) => {
return isTrustedAppCreateSuccessState(createView);
export const isCreationDialogFormValid = (state: Immutable<TrustedAppsListPageState>): boolean => {
return state.creationDialog.formState?.isValid || false;
};
export const isCreationInProgress = (state: Immutable<TrustedAppsListPageState>): boolean => {
return isLoadingResourceState(state.creationDialog.submissionResourceState);
};
export const isCreationSuccessful = (state: Immutable<TrustedAppsListPageState>): boolean => {
return isLoadedResourceState(state.creationDialog.submissionResourceState);
};
export const getCreationError = (
state: Immutable<TrustedAppsListPageState>
): Immutable<ServerApiError> | undefined => {
const submissionResourceState = state.creationDialog.submissionResourceState;
return isFailedResourceState(submissionResourceState) ? submissionResourceState.error : undefined;
};

View file

@ -7,17 +7,22 @@
import React, { ChangeEventHandler, memo, useCallback, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiButtonIcon,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiSuperSelect,
EuiFieldText,
EuiButtonIcon,
EuiSuperSelectOption,
} from '@elastic/eui';
import { ConditionEntryField, TrustedApp } from '../../../../../../../../common/endpoint/types';
import { CONDITION_FIELD_TITLE } from '../../../translations';
import {
ConditionEntry,
ConditionEntryField,
OperatingSystem,
} from '../../../../../../../common/endpoint/types';
import { CONDITION_FIELD_TITLE, ENTRY_PROPERTY_TITLES, OPERATOR_TITLE } from '../../translations';
const ConditionEntryCell = memo<{
showLabel: boolean;
@ -35,25 +40,27 @@ const ConditionEntryCell = memo<{
ConditionEntryCell.displayName = 'ConditionEntryCell';
export interface ConditionEntryProps {
os: TrustedApp['os'];
entry: TrustedApp['entries'][0];
export interface ConditionEntryInputProps {
os: OperatingSystem;
entry: ConditionEntry;
/** controls if remove button is enabled/disabled */
isRemoveDisabled?: boolean;
/** If the labels for each Column in the input row should be shown. Normally set on the first row entry */
showLabels: boolean;
onRemove: (entry: TrustedApp['entries'][0]) => void;
onChange: (newEntry: TrustedApp['entries'][0], oldEntry: TrustedApp['entries'][0]) => void;
onRemove: (entry: ConditionEntry) => void;
onChange: (newEntry: ConditionEntry, oldEntry: ConditionEntry) => void;
/**
* invoked when at least one field in the entry was visited (triggered when `onBlur` DOM event is dispatched)
* For this component, that will be triggered only when the `value` field is visited, since that is the
* only one needs user input.
*/
onVisited?: (entry: TrustedApp['entries'][0]) => void;
onVisited?: (entry: ConditionEntry) => void;
'data-test-subj'?: string;
}
export const ConditionEntry = memo<ConditionEntryProps>(
export const ConditionEntryInput = memo<ConditionEntryInputProps>(
({
os,
entry,
showLabels = false,
onRemove,
@ -62,14 +69,9 @@ export const ConditionEntry = memo<ConditionEntryProps>(
onVisited,
'data-test-subj': dataTestSubj,
}) => {
const getTestId = useCallback(
(suffix: string): string | undefined => {
if (dataTestSubj) {
return `${dataTestSubj}-${suffix}`;
}
},
[dataTestSubj]
);
const getTestId = useCallback((suffix: string) => dataTestSubj && `${dataTestSubj}-${suffix}`, [
dataTestSubj,
]);
const fieldOptions = useMemo<Array<EuiSuperSelectOption<string>>>(() => {
return [
@ -81,38 +83,28 @@ export const ConditionEntry = memo<ConditionEntryProps>(
inputDisplay: CONDITION_FIELD_TITLE[ConditionEntryField.PATH],
value: ConditionEntryField.PATH,
},
...(os === OperatingSystem.WINDOWS
? [
{
inputDisplay: CONDITION_FIELD_TITLE[ConditionEntryField.SIGNER],
value: ConditionEntryField.SIGNER,
},
]
: []),
];
}, []);
}, [os]);
const handleValueUpdate = useCallback<ChangeEventHandler<HTMLInputElement>>(
(ev) => {
onChange(
{
...entry,
value: ev.target.value,
},
entry
);
},
(ev) => onChange({ ...entry, value: ev.target.value }, entry),
[entry, onChange]
);
const handleFieldUpdate = useCallback(
(newField) => {
onChange(
{
...entry,
field: newField,
},
entry
);
},
(newField) => onChange({ ...entry, field: newField }, entry),
[entry, onChange]
);
const handleRemoveClick = useCallback(() => {
onRemove(entry);
}, [entry, onRemove]);
const handleRemoveClick = useCallback(() => onRemove(entry), [entry, onRemove]);
const handleValueOnBlur = useCallback(() => {
if (onVisited) {
@ -129,13 +121,7 @@ export const ConditionEntry = memo<ConditionEntryProps>(
responsive={false}
>
<EuiFlexItem grow={2}>
<ConditionEntryCell
showLabel={showLabels}
label={i18n.translate(
'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field',
{ defaultMessage: 'Field' }
)}
>
<ConditionEntryCell showLabel={showLabels} label={ENTRY_PROPERTY_TITLES.field}>
<EuiSuperSelect
options={fieldOptions}
valueOfSelected={entry.field}
@ -145,31 +131,12 @@ export const ConditionEntry = memo<ConditionEntryProps>(
</ConditionEntryCell>
</EuiFlexItem>
<EuiFlexItem>
<ConditionEntryCell
showLabel={showLabels}
label={i18n.translate(
'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.operator',
{ defaultMessage: 'Operator' }
)}
>
<EuiFieldText
name="operator"
value={i18n.translate(
'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.operator.is',
{ defaultMessage: 'is' }
)}
readOnly
/>
<ConditionEntryCell showLabel={showLabels} label={ENTRY_PROPERTY_TITLES.operator}>
<EuiFieldText name="operator" value={OPERATOR_TITLE.included} readOnly />
</ConditionEntryCell>
</EuiFlexItem>
<EuiFlexItem grow={3}>
<ConditionEntryCell
showLabel={showLabels}
label={i18n.translate(
'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.value',
{ defaultMessage: 'Value' }
)}
>
<ConditionEntryCell showLabel={showLabels} label={ENTRY_PROPERTY_TITLES.value}>
<EuiFieldText
name="value"
value={entry.value}
@ -202,4 +169,4 @@ export const ConditionEntry = memo<ConditionEntryProps>(
}
);
ConditionEntry.displayName = 'ConditionEntry';
ConditionEntryInput.displayName = 'ConditionEntryInput';

View file

@ -8,9 +8,9 @@ import React, { memo, useCallback } from 'react';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiHideFor, EuiSpacer } from '@elastic/eui';
import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n/react';
import { TrustedApp, WindowsConditionEntry } from '../../../../../../../../common/endpoint/types';
import { ConditionEntry, ConditionEntryProps } from './condition_entry';
import { AndOrBadge } from '../../../../../../../common/components/and_or_badge';
import { ConditionEntry, OperatingSystem } from '../../../../../../../common/endpoint/types';
import { AndOrBadge } from '../../../../../../common/components/and_or_badge';
import { ConditionEntryInput, ConditionEntryInputProps } from '../condition_entry_input';
const ConditionGroupFlexGroup = styled(EuiFlexGroup)`
// The positioning of the 'and-badge' is done by using the EuiButton's height and adding on to it
@ -41,14 +41,14 @@ const ConditionGroupFlexGroup = styled(EuiFlexGroup)`
`;
export interface ConditionGroupProps {
os: TrustedApp['os'];
entries: TrustedApp['entries'];
onEntryRemove: ConditionEntryProps['onRemove'];
onEntryChange: ConditionEntryProps['onChange'];
os: OperatingSystem;
entries: ConditionEntry[];
onEntryRemove: ConditionEntryInputProps['onRemove'];
onEntryChange: ConditionEntryInputProps['onChange'];
onAndClicked: () => void;
isAndDisabled?: boolean;
/** called when any of the entries is visited (triggered via `onBlur` DOM event) */
onVisited?: ConditionEntryProps['onVisited'];
onVisited?: ConditionEntryInputProps['onVisited'];
'data-test-subj'?: string;
}
export const ConditionGroup = memo<ConditionGroupProps>(
@ -85,8 +85,8 @@ export const ConditionGroup = memo<ConditionGroupProps>(
)}
<EuiFlexItem grow={1}>
<div data-test-subj={getTestId('entries')} className="group-entries">
{(entries as WindowsConditionEntry[]).map((entry, index) => (
<ConditionEntry
{(entries as ConditionEntry[]).map((entry, index) => (
<ConditionEntryInput
key={index}
os={os}
entry={entry}

View file

@ -17,33 +17,31 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
import React, { memo, useCallback, useEffect, useState } from 'react';
import React, { memo, useCallback, useEffect } from 'react';
import { EuiFlyoutProps } from '@elastic/eui/src/components/flyout/flyout';
import { FormattedMessage } from '@kbn/i18n/react';
import { useDispatch } from 'react-redux';
import { i18n } from '@kbn/i18n';
import { CreateTrustedAppForm, CreateTrustedAppFormProps } from './create_trusted_app_form';
import {
CreateTrustedAppForm,
CreateTrustedAppFormProps,
TrustedAppFormState,
} from './create_trusted_app_form';
import { useTrustedAppsSelector } from '../hooks';
import { getApiCreateErrors, isCreatePending, wasCreateSuccessful } from '../../store/selectors';
getCreationError,
isCreationDialogFormValid,
isCreationInProgress,
isCreationSuccessful,
} from '../../store/selectors';
import { AppAction } from '../../../../../common/store/actions';
import { useToasts } from '../../../../../common/lib/kibana';
import { useTrustedAppsSelector } from '../hooks';
import { ABOUT_TRUSTED_APPS } from '../translations';
type CreateTrustedAppFlyoutProps = Omit<EuiFlyoutProps, 'hideCloseButton'>;
export const CreateTrustedAppFlyout = memo<CreateTrustedAppFlyoutProps>(
({ onClose, ...flyoutProps }) => {
const dispatch = useDispatch<(action: AppAction) => void>();
const toasts = useToasts();
const pendingCreate = useTrustedAppsSelector(isCreatePending);
const apiErrors = useTrustedAppsSelector(getApiCreateErrors);
const wasCreated = useTrustedAppsSelector(wasCreateSuccessful);
const creationInProgress = useTrustedAppsSelector(isCreationInProgress);
const creationErrors = useTrustedAppsSelector(getCreationError);
const creationSuccessful = useTrustedAppsSelector(isCreationSuccessful);
const isFormValid = useTrustedAppsSelector(isCreationDialogFormValid);
const [formState, setFormState] = useState<undefined | TrustedAppFormState>();
const dataTestSubj = flyoutProps['data-test-subj'];
const getTestId = useCallback(
@ -55,47 +53,34 @@ export const CreateTrustedAppFlyout = memo<CreateTrustedAppFlyoutProps>(
[dataTestSubj]
);
const handleCancelClick = useCallback(() => {
if (pendingCreate) {
if (creationInProgress) {
return;
}
onClose();
}, [onClose, pendingCreate]);
const handleSaveClick = useCallback(() => {
if (formState) {
dispatch({
type: 'userClickedSaveNewTrustedAppButton',
payload: {
type: 'pending',
data: formState.item,
},
});
}
}, [dispatch, formState]);
}, [onClose, creationInProgress]);
const handleSaveClick = useCallback(
() => dispatch({ type: 'trustedAppCreationDialogConfirmed' }),
[dispatch]
);
const handleFormOnChange = useCallback<CreateTrustedAppFormProps['onChange']>(
(newFormState) => {
setFormState(newFormState);
dispatch({
type: 'trustedAppCreationDialogFormStateUpdated',
payload: { entry: newFormState.item, isValid: newFormState.isValid },
});
},
[]
[dispatch]
);
// If it was created, then close flyout
useEffect(() => {
if (wasCreated) {
toasts.addSuccess(
i18n.translate(
'xpack.securitySolution.trustedapps.createTrustedAppFlyout.successToastTitle',
{
defaultMessage: '"{name}" has been added to the Trusted Applications list.',
values: { name: formState?.item.name },
}
)
);
if (creationSuccessful) {
onClose();
}
}, [formState?.item?.name, onClose, toasts, wasCreated]);
}, [onClose, creationSuccessful]);
return (
<EuiFlyout onClose={handleCancelClick} {...flyoutProps} hideCloseButton={pendingCreate}>
<EuiFlyout onClose={handleCancelClick} {...flyoutProps} hideCloseButton={creationInProgress}>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2 data-test-subj={getTestId('headerTitle')}>
@ -115,8 +100,8 @@ export const CreateTrustedAppFlyout = memo<CreateTrustedAppFlyoutProps>(
<CreateTrustedAppForm
fullWidth
onChange={handleFormOnChange}
isInvalid={!!apiErrors}
error={apiErrors?.message}
isInvalid={!!creationErrors}
error={creationErrors?.message}
data-test-subj={getTestId('createForm')}
/>
</EuiFlyoutBody>
@ -127,7 +112,7 @@ export const CreateTrustedAppFlyout = memo<CreateTrustedAppFlyoutProps>(
<EuiButtonEmpty
onClick={handleCancelClick}
flush="left"
isDisabled={pendingCreate}
isDisabled={creationInProgress}
data-test-subj={getTestId('cancelButton')}
>
<FormattedMessage
@ -140,8 +125,8 @@ export const CreateTrustedAppFlyout = memo<CreateTrustedAppFlyoutProps>(
<EuiButton
onClick={handleSaveClick}
fill
isDisabled={!formState?.isValid || pendingCreate}
isLoading={pendingCreate}
isDisabled={!isFormValid || creationInProgress}
isLoading={creationInProgress}
data-test-subj={getTestId('createButton')}
>
<FormattedMessage
@ -156,4 +141,5 @@ export const CreateTrustedAppFlyout = memo<CreateTrustedAppFlyoutProps>(
);
}
);
CreateTrustedAppFlyout.displayName = 'NewTrustedAppFlyout';

View file

@ -164,7 +164,7 @@ describe('When showing the Trusted App Create Form', () => {
'.euiSuperSelect__listbox button.euiSuperSelect__item'
)
).map((button) => button.textContent);
expect(options).toEqual(['Hash', 'Path']);
expect(options).toEqual(['Hash', 'Path', 'Signature']);
});
it('should show the value field as required', () => {

View file

@ -15,20 +15,18 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EuiFormProps } from '@elastic/eui/src/components/form/form';
import { LogicalConditionBuilder } from './logical_condition';
import {
ConditionEntry,
ConditionEntryField,
MacosLinuxConditionEntry,
NewTrustedApp,
OperatingSystem,
} from '../../../../../../common/endpoint/types';
import { LogicalConditionBuilderProps } from './logical_condition/logical_condition_builder';
import { OS_TITLES } from '../translations';
import {
isMacosLinuxTrustedAppCondition,
isWindowsTrustedAppCondition,
} from '../../state/type_guards';
import { defaultConditionEntry, defaultNewTrustedApp } from '../../store/builders';
import { OS_TITLES } from '../translations';
import { LogicalConditionBuilder, LogicalConditionBuilderProps } from './logical_condition';
const OPERATING_SYSTEMS: readonly OperatingSystem[] = [
OperatingSystem.MAC,
@ -36,15 +34,6 @@ const OPERATING_SYSTEMS: readonly OperatingSystem[] = [
OperatingSystem.LINUX,
];
const generateNewEntry = (): ConditionEntry<ConditionEntryField.HASH> => {
return {
field: ConditionEntryField.HASH,
operator: 'included',
type: 'match',
value: '',
};
};
interface FieldValidationState {
/** If this fields state is invalid. Drives display of errors on the UI */
isInvalid: boolean;
@ -170,12 +159,7 @@ export const CreateTrustedAppForm = memo<CreateTrustedAppFormProps>(
[]
);
const [formValues, setFormValues] = useState<NewTrustedApp>({
name: '',
os: OperatingSystem.WINDOWS,
entries: [generateNewEntry()],
description: '',
});
const [formValues, setFormValues] = useState<NewTrustedApp>(defaultNewTrustedApp());
const [validationResult, setValidationResult] = useState<ValidationResult>(() =>
validateFormValues(formValues)
@ -204,7 +188,7 @@ export const CreateTrustedAppForm = memo<CreateTrustedAppFormProps>(
if (prevState.os === OperatingSystem.WINDOWS) {
return {
...prevState,
entries: [...prevState.entries, generateNewEntry()].filter(
entries: [...prevState.entries, defaultConditionEntry()].filter(
isWindowsTrustedAppCondition
),
};
@ -213,7 +197,7 @@ export const CreateTrustedAppForm = memo<CreateTrustedAppFormProps>(
...prevState,
entries: [
...prevState.entries.filter(isMacosLinuxTrustedAppCondition),
generateNewEntry(),
defaultConditionEntry(),
],
};
}
@ -261,7 +245,7 @@ export const CreateTrustedAppForm = memo<CreateTrustedAppFormProps>(
) as MacosLinuxConditionEntry[])
);
if (updatedState.entries.length === 0) {
updatedState.entries.push(generateNewEntry());
updatedState.entries.push(defaultConditionEntry());
}
} else {
updatedState.entries.push(...prevState.entries);

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { LogicalConditionBuilder } from './logical_condition_builder';
export { LogicalConditionBuilder, LogicalConditionBuilderProps } from './logical_condition_builder';

View file

@ -7,7 +7,7 @@
import React, { memo, useCallback } from 'react';
import { CommonProps, EuiText, EuiPanel } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { ConditionGroup, ConditionGroupProps } from './components/condition_group';
import { ConditionGroup, ConditionGroupProps } from '../condition_group';
export type LogicalConditionBuilderProps = CommonProps & ConditionGroupProps;
export const LogicalConditionBuilder = memo<LogicalConditionBuilderProps>(

View file

@ -36,7 +36,7 @@ export const CONDITION_FIELD_TITLE: { [K in ConditionEntryField]: string } = {
),
};
export const OPERATOR_TITLE: { [K in ConditionEntry<ConditionEntryField>['operator']]: string } = {
export const OPERATOR_TITLE: { [K in ConditionEntry['operator']]: string } = {
included: i18n.translate('xpack.securitySolution.trustedapps.card.operator.includes', {
defaultMessage: 'is',
}),

View file

@ -7,8 +7,14 @@ import React, { memo } from 'react';
import { i18n } from '@kbn/i18n';
import { ServerApiError } from '../../../../common/types';
import { Immutable, TrustedApp } from '../../../../../common/endpoint/types';
import { getDeletionDialogEntry, getDeletionError, isDeletionSuccessful } from '../store/selectors';
import { Immutable, NewTrustedApp, TrustedApp } from '../../../../../common/endpoint/types';
import {
getCreationDialogFormEntry,
getDeletionDialogEntry,
getDeletionError,
isCreationSuccessful,
isDeletionSuccessful,
} from '../store/selectors';
import { useToasts } from '../../../../common/lib/kibana';
import { useTrustedAppsSelector } from './hooks';
@ -38,10 +44,22 @@ const getDeletionSuccessMessage = (entry: Immutable<TrustedApp>) => {
};
};
const getCreationSuccessMessage = (entry: Immutable<NewTrustedApp>) => {
return i18n.translate(
'xpack.securitySolution.trustedapps.createTrustedAppFlyout.successToastTitle',
{
defaultMessage: '"{name}" has been added to the Trusted Applications list.',
values: { name: entry.name },
}
);
};
export const TrustedAppsNotifications = memo(() => {
const deletionError = useTrustedAppsSelector(getDeletionError);
const deletionDialogEntry = useTrustedAppsSelector(getDeletionDialogEntry);
const deletionSuccessful = useTrustedAppsSelector(isDeletionSuccessful);
const creationDialogNewEntry = useTrustedAppsSelector(getCreationDialogFormEntry);
const creationSuccessful = useTrustedAppsSelector(isCreationSuccessful);
const toasts = useToasts();
if (deletionError && deletionDialogEntry) {
@ -52,6 +70,10 @@ export const TrustedAppsNotifications = memo(() => {
toasts.addSuccess(getDeletionSuccessMessage(deletionDialogEntry));
}
if (creationSuccessful && creationDialogNewEntry) {
toasts.addSuccess(getCreationSuccessMessage(creationDialogNewEntry));
}
return <></>;
});

View file

@ -175,7 +175,7 @@ describe('When on the Trusted Apps Page', () => {
renderResult = await renderAndClickAddButton();
fillInCreateForm(renderResult);
const userClickedSaveActionWatcher = waitForAction('userClickedSaveNewTrustedAppButton');
const userClickedSaveActionWatcher = waitForAction('trustedAppCreationDialogConfirmed');
reactTestingLibrary.act(() => {
fireEvent.click(renderResult.getByTestId('addTrustedAppFlyout-createButton'), {
button: 1,
@ -225,7 +225,9 @@ describe('When on the Trusted Apps Page', () => {
},
};
await reactTestingLibrary.act(async () => {
const serverResponseAction = waitForAction('serverReturnedCreateTrustedAppSuccess');
const serverResponseAction = waitForAction(
'trustedAppCreationSubmissionResourceStateChanged'
);
coreStart.http.get.mockClear();
resolveHttpPost(successCreateApiResponse);
await serverResponseAction;
@ -256,7 +258,9 @@ describe('When on the Trusted Apps Page', () => {
message: 'bad call',
};
await reactTestingLibrary.act(async () => {
const serverResponseAction = waitForAction('serverReturnedCreateTrustedAppFailure');
const serverResponseAction = waitForAction(
'trustedAppCreationSubmissionResourceStateChanged'
);
coreStart.http.get.mockClear();
rejectHttpPost(failedCreateApiResponse);
await serverResponseAction;

View file

@ -26,10 +26,8 @@ import {
endpointListReducer,
initialEndpointListState,
} from '../pages/endpoint_hosts/store/reducer';
import {
initialTrustedAppsPageState,
trustedAppsPageReducer,
} from '../pages/trusted_apps/store/reducer';
import { initialTrustedAppsPageState } from '../pages/trusted_apps/store/builders';
import { trustedAppsPageReducer } from '../pages/trusted_apps/store/reducer';
const immutableCombineReducers: ImmutableCombineReducers = combineReducers;

View file

@ -140,9 +140,7 @@ export const createEntryNested = (field: string, entries: NestedEntriesArray): E
return { field, entries, type: 'nested' };
};
export const conditionEntriesToEntries = (
conditionEntries: Array<ConditionEntry<ConditionEntryField>>
): EntriesArray => {
export const conditionEntriesToEntries = (conditionEntries: ConditionEntry[]): EntriesArray => {
return conditionEntries.map((conditionEntry) => {
if (conditionEntry.field === ConditionEntryField.HASH) {
return createEntryMatch(

View file

@ -18144,13 +18144,9 @@
"xpack.securitySolution.trustedapps.list.columns.actions": "アクション",
"xpack.securitySolution.trustedapps.list.pageTitle": "信頼できるアプリケーション",
"xpack.securitySolution.trustedapps.list.totalCount": "{totalItemCount, plural, one {#個の信頼できるアプリケーション} other {#個の信頼できるアプリケーション}}",
"xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field": "フィールド",
"xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.hash": "ハッシュ",
"xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.path": "パス",
"xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.operator": "演算子",
"xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.operator.is": "is",
"xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.removeLabel": "エントリを削除",
"xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.value": "値",
"xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND",
"xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "条件が定義されていません",
"xpack.securitySolution.trustedapps.noResults": "項目が見つかりません",

View file

@ -18162,13 +18162,9 @@
"xpack.securitySolution.trustedapps.list.columns.actions": "操作",
"xpack.securitySolution.trustedapps.list.pageTitle": "受信任的应用程序",
"xpack.securitySolution.trustedapps.list.totalCount": "{totalItemCount, plural, one {# 个受信任的应用程序} other {# 个受信任的应用程序}}",
"xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field": "字段",
"xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.hash": "哈希",
"xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.path": "路径",
"xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.operator": "运算符",
"xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.operator.is": "is",
"xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.removeLabel": "移除条目",
"xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.value": "值",
"xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND",
"xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "未定义条件",
"xpack.securitySolution.trustedapps.noResults": "找不到项目",