[Security Solution][Endpoint] Bug fixes to the Endpoint List (#186223)

## Summary

Fixes a display bug on the Endpoint List where the Policy name column
value was wrapping and causing the row to be miss-aligned. Changes
include:

- Moved components pieces that display the Policy Revision and the
"Out-of-date" message to the `<EndpointPolicyLink>` component
- this component now handles on displaying all of this information in
one place via input Props
- The component also dues Authz checks and ensures that if the user does
not have Authz to read the Endpoint Policy Management section, the
component will display the policy name as plain text (no link)
- It will truncate the Policy name if not enough width is available to
display its full value
- Replaced the Policy List column component for Policy Name with the use
of `<EndpointPolicyLink>`
- Replaced the Policy Details flyout component to also use
`<EndpointPolicyLink>` to display the policy name

> [!NOTE]
> Its still possible for the Policy Name column on the Endpoint list to
display across two lines - when the Policy that the Endpoint host last
reported is not longer available in Kibana. In this case/flow, the
second line will display a message indicating that. See screen captures
below.
This commit is contained in:
Paul Tavares 2024-06-21 11:42:57 -04:00 committed by GitHub
parent b632f0011d
commit 108e1fadea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 397 additions and 326 deletions

View file

@ -23,6 +23,9 @@ import type {
} from '@testing-library/react-hooks/src/types/react';
import type { UseBaseQueryResult } from '@tanstack/react-query';
import ReactDOM from 'react-dom';
import type { DeepReadonly } from 'utility-types';
import type { UserPrivilegesState } from '../../components/user_privileges/user_privileges_context';
import { getUserPrivilegesMockDefaultValue } from '../../components/user_privileges/__mocks__';
import type { AppLinkItems } from '../../links/types';
import { ExperimentalFeaturesService } from '../../experimental_features_service';
import { applyIntersectionObserverMock } from '../intersection_observer_mock';
@ -41,6 +44,7 @@ import { KibanaServices } from '../../lib/kibana';
import { appLinks } from '../../../app_links';
import { fleetGetPackageHttpMock } from '../../../management/mocks';
import { allowedExperimentalValues } from '../../../../common/experimental_features';
import type { EndpointPrivileges } from '../../../../common/endpoint/types';
const REAL_REACT_DOM_CREATE_PORTAL = ReactDOM.createPortal;
@ -116,6 +120,11 @@ export type ReactQueryHookRenderer<
options?: RenderHookOptions<TProps>
) => Promise<TResult>;
export interface UserPrivilegesMockSetter {
set: (privileges: Partial<EndpointPrivileges>) => void;
reset: () => void;
}
/**
* Mocked app root context renderer
*/
@ -155,6 +164,42 @@ export interface AppContextTestRender {
*/
setExperimentalFlag: (flags: Partial<ExperimentalFeatures>) => void;
/**
* A helper method that will return an interface to more easily manipulate Endpoint related user authz.
* Works in conjunction with `jest.mock()` at the test level.
* @param useUserPrivilegesHookMock
*
* @example
*
* // in your test
* import { useUserPrivileges as _useUserPrivileges } from 'path/to/user_privileges'
*
* jest.mock('path/to/user_privileges');
*
* const useUserPrivilegesMock = _useUserPrivileges as jest.Mock;
*
* // If you test - or more likely, in the `beforeEach` and `afterEach`
* let authMockSetter: UserPrivilegesMockSetter;
*
* beforeEach(() => {
* const appTestSetup = createAppRootMockRenderer();
*
* authMockSetter = appTestSetup.getUserPrivilegesMockSetter(useUserPrivilegesMock);
* })
*
* afterEach(() => {
* authMockSetter.reset();
* }
*
* // Manipulate the authz in your test
* it('does something', () => {
* authMockSetter({ canReadPolicyManagement: false });
* });
*/
getUserPrivilegesMockSetter: (
useUserPrivilegesHookMock: jest.MockedFn<() => DeepReadonly<UserPrivilegesState>>
) => UserPrivilegesMockSetter;
/**
* The React Query client (setup to support jest testing)
*/
@ -305,6 +350,23 @@ export const createAppRootMockRenderer = (): AppContextTestRender => {
});
};
const getUserPrivilegesMockSetter: AppContextTestRender['getUserPrivilegesMockSetter'] = (
useUserPrivilegesHookMock
) => {
return {
set: (authOverrides) => {
const newAuthz = getUserPrivilegesMockDefaultValue();
Object.assign(newAuthz.endpointPrivileges, authOverrides);
useUserPrivilegesHookMock.mockReturnValue(newAuthz);
},
reset: () => {
useUserPrivilegesHookMock.mockReset();
useUserPrivilegesHookMock.mockReturnValue(getUserPrivilegesMockDefaultValue());
},
};
};
// Initialize the singleton `KibanaServices` with global services created for this test instance.
// The module (`../../lib/kibana`) could have been mocked at the test level via `jest.mock()`,
// and if so, then we set the return value of `KibanaServices.get` instead of calling `KibanaServices.init()`
@ -336,6 +398,7 @@ export const createAppRootMockRenderer = (): AppContextTestRender => {
renderHook,
renderReactQueryHook,
setExperimentalFlag,
getUserPrivilegesMockSetter,
queryClient,
};
};

View file

@ -1,49 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { AppContextTestRender } from '../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../common/mock/endpoint';
import type { EndpointAppliedPolicyStatusProps } from './endpoint_applied_policy_status';
import { EndpointAppliedPolicyStatus } from './endpoint_applied_policy_status';
import React from 'react';
import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data';
import { POLICY_STATUS_TO_TEXT } from '../../pages/endpoint_hosts/view/host_constants';
describe('when using EndpointPolicyStatus component', () => {
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<AppContextTestRender['render']>;
let renderProps: EndpointAppliedPolicyStatusProps;
beforeEach(() => {
const appTestContext = createAppRootMockRenderer();
renderProps = {
policyApplied: new EndpointDocGenerator('seed').generateHostMetadata().Endpoint.policy
.applied,
};
render = () => {
renderResult = appTestContext.render(<EndpointAppliedPolicyStatus {...renderProps} />);
return renderResult;
};
});
it('should display status from metadata `policy.applied` value', () => {
render();
expect(renderResult.getByTestId('policyStatus').textContent).toEqual(
POLICY_STATUS_TO_TEXT[renderProps.policyApplied.status]
);
});
it('should display status passed as `children`', () => {
renderProps.children = 'status goes here';
render();
expect(renderResult.getByTestId('policyStatus').textContent).toEqual('status goes here');
});
});

View file

@ -1,86 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { PropsWithChildren } from 'react';
import React, { memo } from 'react';
import { EuiHealth, EuiToolTip, EuiText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import {
POLICY_STATUS_TO_HEALTH_COLOR,
POLICY_STATUS_TO_TEXT,
} from '../../pages/endpoint_hosts/view/host_constants';
import type { HostMetadata } from '../../../../common/endpoint/types';
/**
* Displays the status of an applied policy on the Endpoint (using the information provided
* by the endpoint in the Metadata document `Endpoint.policy.applied`.
* By default, the policy status is displayed as plain text, however, that can be overridden
* by defining the `children` prop or passing a child component to this one.
*/
export type EndpointAppliedPolicyStatusProps = PropsWithChildren<{
policyApplied: HostMetadata['Endpoint']['policy']['applied'];
}>;
/**
* Display the status of the Policy applied on an endpoint
*/
export const EndpointAppliedPolicyStatus = memo<EndpointAppliedPolicyStatusProps>(
({ policyApplied, children }) => {
return (
<EuiToolTip
title={
<FormattedMessage
id="xpack.securitySolution.endpointPolicyStatus.tooltipTitleLabel"
defaultMessage="Policy applied"
/>
}
anchorClassName="eui-textTruncate"
content={
<EuiFlexGroup
responsive={false}
gutterSize="s"
alignItems="center"
data-test-subj="endpointAppliedPolicyTooltipInfo"
>
<EuiFlexItem className="eui-textTruncate" grow>
<EuiText size="s" className="eui-textTruncate">
{policyApplied.name}
</EuiText>
</EuiFlexItem>
{policyApplied.endpoint_policy_version && (
<EuiFlexItem grow={false}>
<EuiText
color="subdued"
size="xs"
style={{ whiteSpace: 'nowrap', paddingLeft: '6px' }}
className="eui-textTruncate"
data-test-subj="policyRevision"
>
<FormattedMessage
id="xpack.securitySolution.endpointPolicyStatus.revisionNumber"
defaultMessage="rev. {revNumber}"
values={{ revNumber: policyApplied.endpoint_policy_version }}
/>
</EuiText>
</EuiFlexItem>
)}
</EuiFlexGroup>
}
>
<EuiHealth
color={POLICY_STATUS_TO_HEALTH_COLOR[policyApplied.status]}
className="eui-textTruncate eui-fullWidth"
data-test-subj="policyStatus"
>
{children !== undefined ? children : POLICY_STATUS_TO_TEXT[policyApplied.status]}
</EuiHealth>
</EuiToolTip>
);
}
);
EndpointAppliedPolicyStatus.displayName = 'EndpointAppliedPolicyStatus';

View file

@ -1,9 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { EndpointAppliedPolicyStatus } from './endpoint_applied_policy_status';
export type { EndpointAppliedPolicyStatusProps } from './endpoint_applied_policy_status';

View file

@ -0,0 +1,109 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { AppContextTestRender, UserPrivilegesMockSetter } from '../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../common/mock/endpoint';
import React from 'react';
import type { EndpointPolicyLinkProps } from './endpoint_policy_link';
import { EndpointPolicyLink, POLICY_NOT_FOUND_MESSAGE } from './endpoint_policy_link';
import { useUserPrivileges as _useUserPrivileges } from '../../common/components/user_privileges';
jest.mock('../../common/components/user_privileges');
const useUserPrivilegesMock = _useUserPrivileges as jest.Mock;
describe('EndpointPolicyLink component', () => {
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<AppContextTestRender['render']>;
let props: EndpointPolicyLinkProps;
let authzSettter: UserPrivilegesMockSetter;
beforeEach(() => {
const appTestContext = createAppRootMockRenderer();
props = {
policyId: 'abc-123',
'data-test-subj': 'test',
children: 'policy name here',
};
authzSettter = appTestContext.getUserPrivilegesMockSetter(useUserPrivilegesMock);
render = () => {
renderResult = appTestContext.render(<EndpointPolicyLink {...props} />);
return renderResult;
};
});
afterEach(() => {
authzSettter.reset();
});
it('should display policy as a link to policy details page', () => {
const { getByTestId, queryByTestId } = render();
expect(getByTestId('test-displayContent')).toHaveTextContent(props.children as string);
expect(getByTestId('test-link')).toBeTruthy();
expect(queryByTestId('test-revision')).toBeNull();
expect(queryByTestId('test-outdatedMsg')).toBeNull();
expect(queryByTestId('test-policyNotFoundMsg')).toBeNull();
});
it('should display regular text (no link) if user has no authz to read policy details', () => {
authzSettter.set({ canReadPolicyManagement: false });
const { getByTestId, queryByTestId } = render();
expect(getByTestId('test-displayContent')).toHaveTextContent(props.children as string);
expect(queryByTestId('test-link')).toBeNull();
});
it('should display regular text (no link) if policy does not exist', () => {
props.policyExists = false;
const { getByTestId, queryByTestId } = render();
expect(getByTestId('test-displayContent')).toHaveTextContent(props.children as string);
expect(queryByTestId('test-link')).toBeNull();
});
it('should display regular text (no link) if policy id is empty string', () => {
props.policyId = '';
const { getByTestId, queryByTestId } = render();
expect(getByTestId('test-displayContent')).toHaveTextContent(props.children as string);
expect(queryByTestId('test-link')).toBeNull();
});
it('should display revision', () => {
props.revision = 10;
const { getByTestId } = render();
expect(getByTestId('test-revision')).toHaveTextContent('rev. 10');
});
it('should display out-of-date message', () => {
props.isOutdated = true;
const { getByTestId } = render();
expect(getByTestId('test-outdatedMsg')).toHaveTextContent('Out-of-date');
});
it('should display policy no longer available', () => {
props.policyExists = false;
const { getByTestId } = render();
expect(getByTestId('test-policyNotFoundMsg')).toHaveTextContent(POLICY_NOT_FOUND_MESSAGE);
});
it('should display all info. when policy is missing and out of date', () => {
props.revision = 10;
props.isOutdated = true;
props.policyExists = false;
const { getByTestId } = render();
expect(getByTestId('test').textContent).toEqual('policy name hererev. 10Out-of-date');
});
});

View file

@ -6,60 +6,200 @@
*/
import React, { memo, useMemo } from 'react';
import type { EuiLinkAnchorProps } from '@elastic/eui';
import { EuiLink, EuiText, EuiIcon } from '@elastic/eui';
import type { EuiLinkAnchorProps, EuiTextProps } from '@elastic/eui';
import { EuiLink, EuiText, EuiIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { useTestIdGenerator } from '../hooks/use_test_id_generator';
import { getPolicyDetailPath } from '../common/routing';
import { useNavigateByRouterEventHandler } from '../../common/hooks/endpoint/use_navigate_by_router_event_handler';
import { useAppUrl } from '../../common/lib/kibana/hooks';
import type { PolicyDetailsRouteState } from '../../../common/endpoint/types';
import { useUserPrivileges } from '../../common/components/user_privileges';
export const POLICY_NOT_FOUND_MESSAGE = i18n.translate(
'xpack.securitySolution.endpointPolicyLink.policyNotFound',
{ defaultMessage: 'Policy no longer available!' }
);
export type EndpointPolicyLinkProps = Omit<EuiLinkAnchorProps, 'href'> & {
policyId: string;
/**
* If defined, then a tooltip will also be shown when a user hovers over the dispplayed value (`children`).
* When set to `true`, the tooltip content will be the same as the value that is
* displayed (`children`). The tooltip can also be customized by passing in the content to be shown.
*/
tooltip?: boolean | React.ReactNode;
/**
* The revision of the policy that the Endpoint is running with (normally obtained from the host's metadata.
*/
revision?: number;
/**
* Will display an "out of date" message.
*/
isOutdated?: boolean;
/** Text size to be applied to the display content (`children`) */
textSize?: EuiTextProps['size'];
/**
* If policy still exists. In some cases, we could be displaying the policy name for a policy
* that no longer exists (ex. it was deleted, but we still have data in ES that references that deleted policy)
* When set to `true`, a link to the policy wil not be shown and the display value (`children`)
* will have a message appended to it indicating policy no longer available.
*/
policyExists?: boolean;
backLink?: PolicyDetailsRouteState['backLink'];
};
/**
* A policy link (to details) that first checks to see if the policy id exists against
* the `nonExistingPolicies` value in the store. If it does not exist, then regular
* text is returned.
* Will display the provided content (`children`) as a link that takes the user to the Endpoint
* Policy Details page. A link is only displayed if the user has Authz to that page, otherwise the
* provided display content will just be shown as is.
*/
export const EndpointPolicyLink = memo<
Omit<EuiLinkAnchorProps, 'href'> & {
policyId: string;
missingPolicies?: Record<string, boolean>;
backLink?: PolicyDetailsRouteState['backLink'];
}
>(({ policyId, backLink, children, missingPolicies = {}, ...otherProps }) => {
const { getAppUrl } = useAppUrl();
const { toRoutePath, toRouteUrl } = useMemo(() => {
const path = policyId ? getPolicyDetailPath(policyId) : '';
return {
toRoutePath: backLink ? { pathname: path, state: { backLink } } : path,
toRouteUrl: getAppUrl({ path }),
};
}, [policyId, getAppUrl, backLink]);
const clickHandler = useNavigateByRouterEventHandler(toRoutePath);
export const EndpointPolicyLink = memo<EndpointPolicyLinkProps>(
({
policyId,
backLink,
children,
policyExists = true,
isOutdated = false,
tooltip = true,
revision,
textSize = 's',
...euiLinkProps
}) => {
const { getAppUrl } = useAppUrl();
const { canReadPolicyManagement } = useUserPrivileges().endpointPrivileges;
const testId = useTestIdGenerator(euiLinkProps['data-test-subj']);
if (!policyId || missingPolicies[policyId]) {
return (
<span className={otherProps.className} data-test-subj={otherProps['data-test-subj']}>
{children}
{
<EuiText color="subdued" size="xs" className="eui-textNoWrap">
const { toRoutePath, toRouteUrl } = useMemo(() => {
const path = policyId ? getPolicyDetailPath(policyId) : '';
return {
toRoutePath: backLink ? { pathname: path, state: { backLink } } : path,
toRouteUrl: getAppUrl({ path }),
};
}, [policyId, getAppUrl, backLink]);
const clickHandler = useNavigateByRouterEventHandler(toRoutePath);
const displayAsLink = useMemo(() => {
return Boolean(canReadPolicyManagement && policyId && policyExists);
}, [canReadPolicyManagement, policyExists, policyId]);
const displayValue = useMemo(() => {
const content = (
<EuiText
className="eui-displayInline eui-textTruncate"
size={textSize}
data-test-subj={testId('displayContent')}
>
{children}
</EuiText>
);
return displayAsLink ? (
// eslint-disable-next-line @elastic/eui/href-or-on-click
<EuiLink
href={toRouteUrl}
onClick={clickHandler}
{...euiLinkProps}
data-test-subj={testId('link')}
>
{content}
</EuiLink>
) : (
content
);
}, [children, clickHandler, displayAsLink, euiLinkProps, testId, textSize, toRouteUrl]);
const policyNoLongerAvailableMessage = useMemo(() => {
return (
((!policyId || !policyExists) && (
<EuiText
color="subdued"
size="xs"
className="eui-textNoWrap"
data-test-subj={testId('policyNotFoundMsg')}
>
<EuiIcon size="m" type="warning" color="warning" />
&nbsp;
<FormattedMessage
id="xpack.securitySolution.endpoint.policyNotFound"
defaultMessage="Policy not found!"
/>
{POLICY_NOT_FOUND_MESSAGE}
</EuiText>
}
</span>
)) ||
null
);
}, [policyExists, policyId, testId]);
const tooltipContent: React.ReactNode | undefined = useMemo(() => {
const content = tooltip === true ? children : tooltip || undefined;
return content ? (
<div className="eui-textBreakAll" style={{ width: '100%' }}>
{content}
{policyNoLongerAvailableMessage && <>&nbsp;{`(${POLICY_NOT_FOUND_MESSAGE})`}</>}
</div>
) : (
content
);
}, [children, policyNoLongerAvailableMessage, tooltip]);
return (
<div>
<EuiFlexGroup
wrap={false}
responsive={false}
gutterSize="xs"
alignItems="center"
data-test-subj={testId()}
>
<EuiFlexItem
data-test-subj={testId('policyName')}
className="eui-textTruncate"
grow={false}
style={{ minWidth: '40px' }}
>
{tooltipContent ? (
<EuiToolTip content={tooltipContent} anchorClassName="eui-textTruncate">
{displayValue}
</EuiToolTip>
) : (
displayValue
)}
</EuiFlexItem>
{revision && (
<EuiFlexItem grow={false}>
<EuiText
color="subdued"
size="xs"
className="eui-textTruncate"
data-test-subj={testId('revision')}
>
<FormattedMessage
id="xpack.securitySolution.endpointPolicyLink.policyVersion"
defaultMessage="rev. {revision}"
values={{ revision }}
/>
</EuiText>
</EuiFlexItem>
)}
{isOutdated && (
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="xs" className="eui-textTruncate">
<EuiIcon size="m" type="warning" color="warning" className="eui-alignTop" />
<span className="eui-displayInlineBlock" data-test-subj={testId('outdatedMsg')}>
<FormattedMessage
id="xpack.securitySolution.endpointPolicyLink.outdatedMessage"
defaultMessage="Out-of-date"
/>
</span>
</EuiText>
</EuiFlexItem>
)}
</EuiFlexGroup>
{policyNoLongerAvailableMessage}
</div>
);
}
return (
// eslint-disable-next-line @elastic/eui/href-or-on-click
<EuiLink href={toRouteUrl} onClick={clickHandler} {...otherProps}>
{children}
</EuiLink>
);
});
);
EndpointPolicyLink.displayName = 'EndpointPolicyLink';

View file

@ -125,7 +125,7 @@ describe('Endpoints page', { tags: ['@ess', '@serverless'] }, () => {
cy.get<number>('@originalPolicyRevision').then((originalRevision: number) => {
const revisionRegex = new RegExp(`^rev\\. ${originalRevision + 1}$`);
cy.get('@endpointRow').findByTestSubj('policyListRevNo').contains(revisionRegex);
cy.get('@endpointRow').findByTestSubj('policyNameCellLink-revision').contains(revisionRegex);
});
});

View file

@ -1,27 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiText, EuiIcon } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
export const OutOfDate = React.memo<{ style?: React.CSSProperties }>(({ style, ...otherProps }) => {
return (
<EuiText
color="subdued"
size="xs"
className="eui-textNoWrap eui-displayInlineBlock"
style={style}
{...otherProps}
>
<EuiIcon className={'eui-alignTop'} size="m" type="warning" color="warning" />
<FormattedMessage id="xpack.securitySolution.outOfDateLabel" defaultMessage="Out-of-date" />
</EuiText>
);
});
OutOfDate.displayName = 'OutOfDate';

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import styled from 'styled-components';
import {
EuiDescriptionList,
EuiFlexGroup,
@ -17,12 +16,12 @@ import {
} from '@elastic/eui';
import React, { memo, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { isPolicyOutOfDate } from '../../utils';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import {
AgentStatus,
EndpointAgentStatus,
} from '../../../../../common/components/endpoint/agents/agent_status';
import { isPolicyOutOfDate } from '../../utils';
import type { HostInfo } from '../../../../../../common/endpoint/types';
import { useEndpointSelector } from '../hooks';
import {
@ -35,13 +34,6 @@ import { FormattedDate } from '../../../../../common/components/formatted_date';
import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
import { getEndpointDetailsPath } from '../../../../common/routing';
import { EndpointPolicyLink } from '../../../../components/endpoint_policy_link';
import { OutOfDate } from '../components/out_of_date';
const EndpointDetailsContentStyled = styled.div`
.policyLineText {
padding-right: 5px;
}
`;
const ColumnTitle = ({ children }: { children: React.ReactNode }) => {
return (
@ -138,35 +130,15 @@ export const EndpointDetailsContent = memo<EndpointDetailsContentProps>(
</ColumnTitle>
),
description: (
<EuiText size="xs" className={'eui-textBreakWord'}>
<EndpointPolicyLink
policyId={hostInfo.metadata.Endpoint.policy.applied.id}
data-test-subj="policyDetailsValue"
className={'policyLineText'}
missingPolicies={missingPolicies}
>
{hostInfo.metadata.Endpoint.policy.applied.name}
</EndpointPolicyLink>
{hostInfo.metadata.Endpoint.policy.applied.endpoint_policy_version && (
<EuiText
color="subdued"
size="xs"
className={'eui-displayInlineBlock eui-textNoWrap policyLineText'}
data-test-subj="policyDetailsRevNo"
>
<FormattedMessage
id="xpack.securitySolution.endpoint.details.policy.revisionNumber"
defaultMessage="rev. {revNumber}"
values={{
revNumber: hostInfo.metadata.Endpoint.policy.applied.endpoint_policy_version,
}}
/>
</EuiText>
)}
{isPolicyOutOfDate(hostInfo.metadata.Endpoint.policy.applied, policyInfo) && (
<OutOfDate />
)}
</EuiText>
<EndpointPolicyLink
policyId={hostInfo.metadata.Endpoint.policy.applied.id}
revision={hostInfo.metadata.Endpoint.policy.applied.endpoint_policy_version}
isOutdated={isPolicyOutOfDate(hostInfo.metadata.Endpoint.policy.applied, policyInfo)}
policyExists={!missingPolicies[hostInfo.metadata.Endpoint.policy.applied.id]}
data-test-subj="policyDetailsValue"
>
{hostInfo.metadata.Endpoint.policy.applied.name}
</EndpointPolicyLink>
),
},
{
@ -227,17 +199,17 @@ export const EndpointDetailsContent = memo<EndpointDetailsContentProps>(
},
];
}, [
agentStatusClientEnabled,
hostInfo,
agentStatusClientEnabled,
getHostPendingActions,
missingPolicies,
policyInfo,
missingPolicies,
policyStatus,
policyStatusClickHandler,
]);
return (
<EndpointDetailsContentStyled>
<div>
<EuiSpacer size="s" />
<EuiDescriptionList
columnWidths={[1, 3]}
@ -247,7 +219,7 @@ export const EndpointDetailsContent = memo<EndpointDetailsContentProps>(
listItems={detailsResults}
data-test-subj="endpointDetailsList"
/>
</EndpointDetailsContentStyled>
</div>
);
}
);

View file

@ -385,12 +385,11 @@ describe('when on the endpoint list page', () => {
await reactTestingLibrary.act(async () => {
await middlewareSpy.waitForAction('serverReturnedEndpointList');
});
const outOfDates = await renderResult.findAllByTestId('rowPolicyOutOfDate');
const outOfDates = await renderResult.findAllByTestId('policyNameCellLink-outdatedMsg');
expect(outOfDates).toHaveLength(4);
outOfDates.forEach((item) => {
expect(item.textContent).toEqual('Out-of-date');
expect(item.querySelector(`[data-euiicon-type][color=warning]`)).not.toBeNull();
});
});
@ -399,7 +398,7 @@ describe('when on the endpoint list page', () => {
await reactTestingLibrary.act(async () => {
await middlewareSpy.waitForAction('serverReturnedEndpointList');
});
const firstPolicyName = (await renderResult.findAllByTestId('policyNameCellLink'))[0];
const firstPolicyName = (await renderResult.findAllByTestId('policyNameCellLink-link'))[0];
expect(firstPolicyName).not.toBeNull();
expect(firstPolicyName.getAttribute('href')).toEqual(
`${APP_PATH}${MANAGEMENT_PATH}/policy/${firstPolicyID}/settings`
@ -452,7 +451,9 @@ describe('when on the endpoint list page', () => {
await reactTestingLibrary.act(async () => {
await middlewareSpy.waitForAction('serverReturnedEndpointList');
});
const firstPolicyRevElement = (await renderResult.findAllByTestId('policyListRevNo'))[0];
const firstPolicyRevElement = (
await renderResult.findAllByTestId('policyNameCellLink-revision')
)[0];
expect(firstPolicyRevElement).not.toBeNull();
expect(firstPolicyRevElement.textContent).toEqual(`rev. ${firstPolicyRev}`);
});
@ -589,7 +590,7 @@ describe('when on the endpoint list page', () => {
it('should display policy name value as a link', async () => {
const renderResult = render();
const policyDetailsLink = await renderResult.findByTestId('policyDetailsValue');
const policyDetailsLink = await renderResult.findByTestId('policyNameCellLink-link');
expect(policyDetailsLink).not.toBeNull();
expect(policyDetailsLink.getAttribute('href')).toEqual(
`${APP_PATH}${MANAGEMENT_PATH}/policy/${hostInfo.metadata.Endpoint.policy.applied.id}/settings`
@ -598,7 +599,9 @@ describe('when on the endpoint list page', () => {
it('should display policy revision number', async () => {
const renderResult = render();
const policyDetailsRevElement = await renderResult.findByTestId('policyDetailsRevNo');
const policyDetailsRevElement = await renderResult.findByTestId(
'policyNameCellLink-revision'
);
expect(policyDetailsRevElement).not.toBeNull();
expect(policyDetailsRevElement.textContent).toEqual(
`rev. ${hostInfo.metadata.Endpoint.policy.applied.endpoint_policy_version}`
@ -607,7 +610,7 @@ describe('when on the endpoint list page', () => {
it('should update the URL when policy name link is clicked', async () => {
const renderResult = render();
const policyDetailsLink = await renderResult.findByTestId('policyDetailsValue');
const policyDetailsLink = await renderResult.findByTestId('policyNameCellLink-link');
const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl');
reactTestingLibrary.act(() => {
reactTestingLibrary.fireEvent.click(policyDetailsLink);

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { type CSSProperties, useCallback, useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
import type { CriteriaWithPagination } from '@elastic/eui';
import {
@ -32,6 +32,7 @@ import type {
AgentPolicyDetailsDeployAgentAction,
CreatePackagePolicyRouteState,
} from '@kbn/fleet-plugin/public';
import { isPolicyOutOfDate } from '../utils';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { TransformFailedCallout } from './components/transform_failed_callout';
import type { EndpointIndexUIQueryParams } from '../types';
@ -42,9 +43,8 @@ import {
} from '../../../../common/components/endpoint/agents/agent_status';
import { EndpointDetailsFlyout } from './details';
import * as selectors from '../store/selectors';
import { getEndpointPendingActionsCallback } from '../store/selectors';
import { getEndpointPendingActionsCallback, nonExistingPolicies } from '../store/selectors';
import { useEndpointSelector } from './hooks';
import { isPolicyOutOfDate } from '../utils';
import { POLICY_STATUS_TO_HEALTH_COLOR, POLICY_STATUS_TO_TEXT } from './host_constants';
import type { CreateStructuredSelector } from '../../../../common/store';
import type {
@ -64,7 +64,6 @@ import { getEndpointDetailsPath, getEndpointListPath } from '../../../common/rou
import { useFormatUrl } from '../../../../common/components/link_to';
import { useAppUrl } from '../../../../common/lib/kibana/hooks';
import type { EndpointAction } from '../store/action';
import { OutOfDate } from './components/out_of_date';
import { AdminSearchBar } from './components/search_bar';
import { AdministrationListPage } from '../../../components/administration_list_page';
import { TableRowActions } from './components/table_row_actions';
@ -83,7 +82,7 @@ const StyledDatePicker = styled.div`
interface GetEndpointListColumnsProps {
agentStatusClientEnabled: boolean;
canReadPolicyManagement: boolean;
missingPolicies: ReturnType<typeof nonExistingPolicies>;
backToEndpointList: PolicyDetailsRouteState['backLink'];
getHostPendingActions: ReturnType<typeof getEndpointPendingActionsCallback>;
queryParams: Immutable<EndpointIndexUIQueryParams>;
@ -108,7 +107,7 @@ const columnWidths: Record<
const getEndpointListColumns = ({
agentStatusClientEnabled,
canReadPolicyManagement,
missingPolicies,
backToEndpointList,
getHostPendingActions,
queryParams,
@ -118,7 +117,6 @@ const getEndpointListColumns = ({
const lastActiveColumnName = i18n.translate('xpack.securitySolution.endpoint.list.lastActive', {
defaultMessage: 'Last active',
});
const padLeft: CSSProperties = { paddingLeft: '6px' };
return [
{
@ -183,42 +181,17 @@ const getEndpointListColumns = ({
item: HostInfo
) => {
const policy = item.metadata.Endpoint.policy.applied;
return (
<>
<EuiToolTip content={policyName} anchorClassName="eui-textTruncate">
{canReadPolicyManagement ? (
<EndpointPolicyLink
policyId={policy.id}
className="eui-textTruncate"
data-test-subj="policyNameCellLink"
backLink={backToEndpointList}
>
{policyName}
</EndpointPolicyLink>
) : (
<>{policyName}</>
)}
</EuiToolTip>
{policy.endpoint_policy_version && (
<EuiText
color="subdued"
size="xs"
style={{ whiteSpace: 'nowrap', ...padLeft }}
className="eui-textTruncate"
data-test-subj="policyListRevNo"
>
<FormattedMessage
id="xpack.securitySolution.endpoint.list.policy.revisionNumber"
defaultMessage="rev. {revNumber}"
values={{ revNumber: policy.endpoint_policy_version }}
/>
</EuiText>
)}
{isPolicyOutOfDate(policy, item.policy_info) && (
<OutOfDate style={padLeft} data-test-subj="rowPolicyOutOfDate" />
)}
</>
<EndpointPolicyLink
policyId={policy.id}
revision={policy.endpoint_policy_version}
isOutdated={isPolicyOutOfDate(policy, item.policy_info)}
policyExists={!missingPolicies[policy.id]}
data-test-subj="policyNameCellLink"
backLink={backToEndpointList}
>
{policyName}
</EndpointPolicyLink>
);
},
},
@ -375,11 +348,11 @@ export const EndpointList = () => {
metadataTransformStats,
isInitialized,
} = useEndpointSelector(selector);
const missingPolicies = useEndpointSelector(nonExistingPolicies);
const getHostPendingActions = useEndpointSelector(getEndpointPendingActionsCallback);
const {
canReadEndpointList,
canAccessFleet,
canReadPolicyManagement,
loading: endpointPrivilegesLoading,
} = useUserPrivileges().endpointPrivileges;
const { search } = useFormatUrl(SecurityPageName.administration);
@ -540,9 +513,9 @@ export const EndpointList = () => {
() =>
getEndpointListColumns({
agentStatusClientEnabled,
canReadPolicyManagement,
backToEndpointList,
getAppUrl,
missingPolicies,
getHostPendingActions,
queryParams,
search,
@ -550,9 +523,9 @@ export const EndpointList = () => {
[
agentStatusClientEnabled,
backToEndpointList,
canReadPolicyManagement,
getAppUrl,
getHostPendingActions,
missingPolicies,
queryParams,
search,
]

View file

@ -32643,7 +32643,6 @@
"xpack.securitySolution.enableRiskScore.enableRiskScoreDescription": "Une fois que vous avez activé cette fonctionnalité, vous pouvez obtenir un accès rapide aux scores de risque de {riskEntity} dans cette section. Les données pourront prendre jusqu'à une heure pour être générées après l'activation du module.",
"xpack.securitySolution.enableRiskScore.upgradeRiskScore": "Mettre à niveau le score de risque de {riskEntity}",
"xpack.securitySolution.endpoint.actions.unsupported.message": "La version actuelle de l'agent {agentType} ne prend pas en charge {command}. Mettez à niveau votre Elastic Agent via Fleet vers la dernière version pour activer cette action de réponse.",
"xpack.securitySolution.endpoint.details.policy.revisionNumber": "rév. {revNumber}",
"xpack.securitySolution.endpoint.details.policyStatusValue": "{policyStatus, select, success {Succès} warning {Avertissement} failure {Échec} other {Inconnu}}",
"xpack.securitySolution.endpoint.fleetCustomExtension.artifactsSummaryError": "Une erreur s'est produite lors de la tentative de récupération des statistiques d'artefacts : \"{error}\"",
"xpack.securitySolution.endpoint.fleetCustomExtension.blocklistsSummary.error": "Une erreur s'est produite lors de la tentative de récupération des statistiques de liste noire : \"{error}\"",
@ -32663,7 +32662,6 @@
"xpack.securitySolution.endpoint.hostIsolation.unisolate.successfulMessage": "La libération de l'hôte {hostName} a été soumise",
"xpack.securitySolution.endpoint.hostIsolation.unIsolateThisHost": "{hostName} est actuellement {isolated}. Voulez-vous vraiment {unisolate} cet hôte ?",
"xpack.securitySolution.endpoint.list.hostStatusValue": "{hostStatus, select, healthy {Sain} unhealthy {En mauvais état} updating {En cours de mise à jour} offline {Hors ligne} inactive {Inactif} unenrolled {Désinscrit} other {En mauvais état}}",
"xpack.securitySolution.endpoint.list.policy.revisionNumber": "rév. {revNumber}",
"xpack.securitySolution.endpoint.list.totalCount": "Affichage de {totalItemCount, plural, one {# point de terminaison} other {# points de terminaison}}",
"xpack.securitySolution.endpoint.list.totalCount.limited": "Affichage de {limit} de {totalItemCount, plural, one {# point de terminaison} other {# points de terminaison}}",
"xpack.securitySolution.endpoint.list.transformFailed.message": "Une transformation requise, {transformId}, est actuellement en échec. La plupart du temps, ce problème peut être corrigé grâce aux {transformsPage}. Pour une assistance supplémentaire, veuillez visitez la {docsPage}",
@ -32734,7 +32732,6 @@
"xpack.securitySolution.endpoint.resolver.panel.relatedEventList.countByCategory": "{count} {category}",
"xpack.securitySolution.endpoint.resolver.panel.relatedEventList.numberOfEvents": "{totalCount} événements",
"xpack.securitySolution.endpoint.resolver.relatedEventLimitTitle": "Cette liste inclut {numberOfEntries} événements de processus.",
"xpack.securitySolution.endpointPolicyStatus.revisionNumber": "rév. {revNumber}",
"xpack.securitySolution.endpointResponseActions.actionError.errorMessage": "{ errorCount, plural, =1 {Erreur rencontrée} other {Erreurs rencontrées}} :",
"xpack.securitySolution.enrichment.noInvestigationEnrichment": "Aucun renseignement supplémentaire sur les menaces n'a été détecté sur la période sélectionnée. Sélectionnez une autre plage temporelle ou {link} afin de collecter des renseignements sur les menaces pour les détecter et les comparer.",
"xpack.securitySolution.entityAnalytics.anomalies.moduleNotCompatibleTitle": "{incompatibleJobCount} {incompatibleJobCount, plural, =1 {tâche est actuellement indisponible} other {tâches sont actuellement indisponibles}}",
@ -35613,7 +35610,6 @@
"xpack.securitySolution.endpoint.policyList.onboardingSectionTwo.fromEndpointPage": "À partir de cette page, vous pourrez afficher et gérer les hôtes dans votre environnement exécutant Elastic Defend.",
"xpack.securitySolution.endpoint.policyList.onboardingSectionTwo.fromPolicyPage": "À partir de cette page, vous pourrez afficher et gérer les politiques d'intégration Elastic Defend dans votre environnement exécutant Elastic Defend.",
"xpack.securitySolution.endpoint.policyList.onboardingTitle": "Lancez-vous avec Elastic Defend",
"xpack.securitySolution.endpoint.policyNotFound": "Politique introuvable !",
"xpack.securitySolution.endpoint.policyResponse.backLinkTitle": "Détails de point de terminaison",
"xpack.securitySolution.endpoint.policyResponse.title": "Réponse de politique",
"xpack.securitySolution.endpoint.protectionUpdates.automaticUpdates.enabled.toggleName": "Activer les mises à jour automatique",
@ -35750,7 +35746,6 @@
"xpack.securitySolution.endpointDetails.activityLog.logEntry.response.unisolationSuccessful": "Requête de libération de l'hôte reçue par Endpoint",
"xpack.securitySolution.endpointDetails.overview": "Aperçu",
"xpack.securitySolution.endpointDetails.responseActionsHistory": "Historique des actions de réponse",
"xpack.securitySolution.endpointPolicyStatus.tooltipTitleLabel": "Politique appliquée",
"xpack.securitySolution.endpointResponseActions.actionSubmitter.apiErrorDetails": "L'erreur suivante a été rencontrée :",
"xpack.securitySolution.endpointResponseActions.executeAction.successTitle": "L'exécution de la commande a réussi.",
"xpack.securitySolution.endpointResponseActions.getFileAction.successTitle": "Fichier récupéré à partir de l'hôte.",
@ -36831,7 +36826,6 @@
"xpack.securitySolution.osquery.action.permissionDenied": "Autorisation refusée",
"xpack.securitySolution.osquery.action.shortEmptyTitle": "Osquery nest pas disponible.",
"xpack.securitySolution.osquery.action.unavailable": "Lintégration Osquery Manager n'a pas été ajoutée à la politique d'agent. Pour exécuter des requêtes sur l'hôte, ajoutez l'intégration Osquery Manager à la politique d'agent dans Fleet.",
"xpack.securitySolution.outOfDateLabel": "Obsolète",
"xpack.securitySolution.overview.auditBeatAuditTitle": "Audit",
"xpack.securitySolution.overview.auditBeatFimTitle": "File Integrity Module",
"xpack.securitySolution.overview.auditBeatLoginTitle": "Connexion",

View file

@ -32617,7 +32617,6 @@
"xpack.securitySolution.enableRiskScore.enableRiskScoreDescription": "この機能を有効化すると、このセクションで{riskEntity}リスクスコアにすばやくアクセスできます。モジュールを有効化した後、データの生成までに1時間かかる場合があります。",
"xpack.securitySolution.enableRiskScore.upgradeRiskScore": "{riskEntity}リスクスコアをアップグレード",
"xpack.securitySolution.endpoint.actions.unsupported.message": "現在のバージョンの{agentType}エージェントは、{command}をサポートしていません。この応答アクションを有効化するには、Fleet経由でElasticエージェントを最新バージョンにアップグレードしてください。",
"xpack.securitySolution.endpoint.details.policy.revisionNumber": "rev. {revNumber}",
"xpack.securitySolution.endpoint.details.policyStatusValue": "{policyStatus, select, success {成功} warning {警告} failure {失敗} other {不明}}",
"xpack.securitySolution.endpoint.fleetCustomExtension.artifactsSummaryError": "アーティファクト統計情報の取得中にエラーが発生しました:\"{error}\"",
"xpack.securitySolution.endpoint.fleetCustomExtension.blocklistsSummary.error": "ブロックリスト統計情報の取得中にエラーが発生しました:\"{error}\"",
@ -32637,7 +32636,6 @@
"xpack.securitySolution.endpoint.hostIsolation.unisolate.successfulMessage": "ホスト{hostName}でのリリースは正常に送信されました",
"xpack.securitySolution.endpoint.hostIsolation.unIsolateThisHost": "現在{hostName}は{isolated}です。このホストを{unisolate}しますか?",
"xpack.securitySolution.endpoint.list.hostStatusValue": "{hostStatus, select, healthy {正常} unhealthy {異常} updating {更新中} offline {オフライン} inactive {無効} unenrolled {登録解除済み} other {異常}}",
"xpack.securitySolution.endpoint.list.policy.revisionNumber": "rev. {revNumber}",
"xpack.securitySolution.endpoint.list.totalCount": "{totalItemCount, plural, other {# 個のエンドポイント}}を表示しています",
"xpack.securitySolution.endpoint.list.totalCount.limited": "{totalItemCount, plural, other {# 個のエンドポイント}}の{limit}を表示しています",
"xpack.securitySolution.endpoint.list.transformFailed.message": "現在、必須の変換{transformId}が失敗しています。通常、これは{transformsPage}で修正できます。ヘルプについては、{docsPage}をご覧ください",
@ -32707,7 +32705,6 @@
"xpack.securitySolution.endpoint.resolver.panel.relatedEventList.countByCategory": "{count} {category}",
"xpack.securitySolution.endpoint.resolver.panel.relatedEventList.numberOfEvents": "{totalCount}件のイベント",
"xpack.securitySolution.endpoint.resolver.relatedEventLimitTitle": "このリストには、{numberOfEntries} 件のプロセスイベントが含まれています。",
"xpack.securitySolution.endpointPolicyStatus.revisionNumber": "rev. {revNumber}",
"xpack.securitySolution.endpointResponseActions.actionError.errorMessage": "次の{ errorCount, plural, other {件のエラー}}が発生しました:",
"xpack.securitySolution.enrichment.noInvestigationEnrichment": "選択した期間内に追加の脅威情報が見つかりませんでした。別の時間枠、または{link}を試して、脅威の検出と照合のための脅威インテリジェンスを収集します。",
"xpack.securitySolution.entityAnalytics.anomalies.moduleNotCompatibleTitle": "{incompatibleJobCount} {incompatibleJobCount, plural, other {件のジョブ}}が現在使用できません",
@ -35588,7 +35585,6 @@
"xpack.securitySolution.endpoint.policyList.onboardingSectionTwo.fromEndpointPage": "このページでは、Elastic Defendを実行している環境でホストを表示して管理できます。",
"xpack.securitySolution.endpoint.policyList.onboardingSectionTwo.fromPolicyPage": "このページでは、Elastic Defendを実行している環境で、Elastic Defend統合ポリシーを表示して管理できます。",
"xpack.securitySolution.endpoint.policyList.onboardingTitle": "Elastic Defendをはじめよう",
"xpack.securitySolution.endpoint.policyNotFound": "ポリシーが見つかりません。",
"xpack.securitySolution.endpoint.policyResponse.backLinkTitle": "エンドポイント詳細",
"xpack.securitySolution.endpoint.policyResponse.title": "ポリシー応答",
"xpack.securitySolution.endpoint.protectionUpdates.automaticUpdates.enabled.toggleName": "自動更新を有効化",
@ -35725,7 +35721,6 @@
"xpack.securitySolution.endpointDetails.activityLog.logEntry.response.unisolationSuccessful": "エンドポイントが受信したホストリリースリクエスト",
"xpack.securitySolution.endpointDetails.overview": "概要",
"xpack.securitySolution.endpointDetails.responseActionsHistory": "対応アクション履歴",
"xpack.securitySolution.endpointPolicyStatus.tooltipTitleLabel": "ポリシーが適用されました",
"xpack.securitySolution.endpointResponseActions.actionSubmitter.apiErrorDetails": "次のエラーが発生しました:",
"xpack.securitySolution.endpointResponseActions.executeAction.successTitle": "コマンド実行が成功しました。",
"xpack.securitySolution.endpointResponseActions.getFileAction.successTitle": "ファイルがホストから取得されました。",
@ -36806,7 +36801,6 @@
"xpack.securitySolution.osquery.action.permissionDenied": "パーミッションが拒否されました",
"xpack.securitySolution.osquery.action.shortEmptyTitle": "Osqueryが使用できません",
"xpack.securitySolution.osquery.action.unavailable": "Osqueryマネージャー統合がエージェントポリシーに追加されていません。ホストでクエリを実行するには、FleetでOsqueryマネージャー統合をエージェントポリシーに追加してください。",
"xpack.securitySolution.outOfDateLabel": "最新ではありません",
"xpack.securitySolution.overview.auditBeatAuditTitle": "監査",
"xpack.securitySolution.overview.auditBeatFimTitle": "File Integrityモジュール",
"xpack.securitySolution.overview.auditBeatLoginTitle": "ログイン",

View file

@ -32660,7 +32660,6 @@
"xpack.securitySolution.enableRiskScore.enableRiskScoreDescription": "一旦启用此功能,您将可以在此部分快速访问{riskEntity}风险分数。启用此模板后,可能需要一小时才能生成数据。",
"xpack.securitySolution.enableRiskScore.upgradeRiskScore": "升级{riskEntity}风险分数",
"xpack.securitySolution.endpoint.actions.unsupported.message": "当前版本的 {agentType} 代理不支持 {command}。通过 Fleet 将您的 Elastic 代理升级到最新版本以启用此响应操作。",
"xpack.securitySolution.endpoint.details.policy.revisionNumber": "修订版 {revNumber}",
"xpack.securitySolution.endpoint.details.policyStatusValue": "{policyStatus, select, success {成功} warning {警告} failure {失败} other {未知}}",
"xpack.securitySolution.endpoint.fleetCustomExtension.artifactsSummaryError": "尝试提取项目统计时出错:“{error}”",
"xpack.securitySolution.endpoint.fleetCustomExtension.blocklistsSummary.error": "尝试提取阻止列表统计时出错:“{error}”",
@ -32680,7 +32679,6 @@
"xpack.securitySolution.endpoint.hostIsolation.unisolate.successfulMessage": "已成功提交主机 {hostName} 的释放",
"xpack.securitySolution.endpoint.hostIsolation.unIsolateThisHost": "{hostName} 当前{isolated}。是否确定要{unisolate}此主机?",
"xpack.securitySolution.endpoint.list.hostStatusValue": "{hostStatus, select, healthy {运行正常} unhealthy {运行不正常} updating {正在更新} offline {脱机} inactive {非活动} unenrolled {未注册} other {运行不正常}}",
"xpack.securitySolution.endpoint.list.policy.revisionNumber": "修订版 {revNumber}",
"xpack.securitySolution.endpoint.list.totalCount": "正在显示 {totalItemCount, plural, other {# 个终端}}",
"xpack.securitySolution.endpoint.list.totalCount.limited": "正在显示 {totalItemCount, plural, other {# 个终端}}中的 {limit} 个",
"xpack.securitySolution.endpoint.list.transformFailed.message": "所需的转换 {transformId} 当前失败。多数时候,这可以通过 {transformsPage} 解决。要获取更多帮助,请访问{docsPage}",
@ -32751,7 +32749,6 @@
"xpack.securitySolution.endpoint.resolver.panel.relatedEventList.countByCategory": "{count} 个{category}",
"xpack.securitySolution.endpoint.resolver.panel.relatedEventList.numberOfEvents": "{totalCount} 个事件",
"xpack.securitySolution.endpoint.resolver.relatedEventLimitTitle": "此列表包括 {numberOfEntries} 个进程事件。",
"xpack.securitySolution.endpointPolicyStatus.revisionNumber": "修订版 {revNumber}",
"xpack.securitySolution.endpointResponseActions.actionError.errorMessage": "遇到以下{ errorCount, plural, other {错误}}",
"xpack.securitySolution.enrichment.noInvestigationEnrichment": "在选定时间范围内未发现其他威胁情报。请尝试不同时间范围,或 {link} 以收集威胁情报用于威胁检测和匹配。",
"xpack.securitySolution.entityAnalytics.anomalies.moduleNotCompatibleTitle": "{incompatibleJobCount} 个{incompatibleJobCount, plural, other {作业}}当前不可用",
@ -35631,7 +35628,6 @@
"xpack.securitySolution.endpoint.policyList.onboardingSectionTwo.fromEndpointPage": "从此页面,您将能够查看和管理环境中运行 Elastic Defend 的主机。",
"xpack.securitySolution.endpoint.policyList.onboardingSectionTwo.fromPolicyPage": "从此页面,您将能够查看和管理运行 Elastic Defend 的环境中的 Elastic Defend 集成策略。",
"xpack.securitySolution.endpoint.policyList.onboardingTitle": "开始使用 Elastic Defend",
"xpack.securitySolution.endpoint.policyNotFound": "未找到策略!",
"xpack.securitySolution.endpoint.policyResponse.backLinkTitle": "终端详情",
"xpack.securitySolution.endpoint.policyResponse.title": "策略响应",
"xpack.securitySolution.endpoint.protectionUpdates.automaticUpdates.enabled.toggleName": "启用自动更新",
@ -35768,7 +35764,6 @@
"xpack.securitySolution.endpointDetails.activityLog.logEntry.response.unisolationSuccessful": "终端收到释放主机请求",
"xpack.securitySolution.endpointDetails.overview": "概览",
"xpack.securitySolution.endpointDetails.responseActionsHistory": "响应操作历史记录",
"xpack.securitySolution.endpointPolicyStatus.tooltipTitleLabel": "已应用策略",
"xpack.securitySolution.endpointResponseActions.actionSubmitter.apiErrorDetails": "遇到以下错误:",
"xpack.securitySolution.endpointResponseActions.executeAction.successTitle": "命令执行成功。",
"xpack.securitySolution.endpointResponseActions.getFileAction.successTitle": "已从主机检索文件。",
@ -36849,7 +36844,6 @@
"xpack.securitySolution.osquery.action.permissionDenied": "权限被拒绝",
"xpack.securitySolution.osquery.action.shortEmptyTitle": "Osquery 不可用",
"xpack.securitySolution.osquery.action.unavailable": "Osquery 管理器集成未添加到代理策略。要在此主机上运行查询,请在 Fleet 中将 Osquery 管理器集成添加到代理策略。",
"xpack.securitySolution.outOfDateLabel": "过时",
"xpack.securitySolution.overview.auditBeatAuditTitle": "审计",
"xpack.securitySolution.overview.auditBeatFimTitle": "文件完整性模块",
"xpack.securitySolution.overview.auditBeatLoginTitle": "登录",