[Logs UI] Add missing ML capabilities checks (#72606)

This adds several missing Machine Learning capabilities checks to the UI to make sure the user doesn't run into downstream errors resulting from the lack of permissions. It also updates the messages of the permission prompt screens to refer to the new Kibana Machine Learning permissions instead of the old built-in roles.
This commit is contained in:
Felix Stürmer 2020-07-22 18:32:17 +02:00 committed by GitHub
parent fe5b4b81a2
commit ba55ca9e86
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 175 additions and 94 deletions

View file

@ -6,4 +6,3 @@
export * from './log_analysis_job_problem_indicator';
export * from './notices_section';
export * from './recreate_job_button';

View file

@ -11,10 +11,12 @@ import React from 'react';
import { RecreateJobCallout } from './recreate_job_callout';
export const JobConfigurationOutdatedCallout: React.FC<{
hasSetupCapabilities: boolean;
moduleName: string;
onRecreateMlJob: () => void;
}> = ({ moduleName, onRecreateMlJob }) => (
}> = ({ hasSetupCapabilities, moduleName, onRecreateMlJob }) => (
<RecreateJobCallout
hasSetupCapabilities={hasSetupCapabilities}
title={i18n.translate('xpack.infra.logs.analysis.jobConfigurationOutdatedCalloutTitle', {
defaultMessage: 'The {moduleName} ML job configuration is outdated',
values: {

View file

@ -11,10 +11,12 @@ import React from 'react';
import { RecreateJobCallout } from './recreate_job_callout';
export const JobDefinitionOutdatedCallout: React.FC<{
hasSetupCapabilities: boolean;
moduleName: string;
onRecreateMlJob: () => void;
}> = ({ moduleName, onRecreateMlJob }) => (
}> = ({ hasSetupCapabilities, moduleName, onRecreateMlJob }) => (
<RecreateJobCallout
hasSetupCapabilities={hasSetupCapabilities}
title={i18n.translate('xpack.infra.logs.analysis.jobDefinitionOutdatedCalloutTitle', {
defaultMessage: 'The {moduleName} ML job definition is outdated',
values: {

View file

@ -14,6 +14,7 @@ import { FirstUseCallout } from '../log_analysis_results';
export const LogAnalysisJobProblemIndicator: React.FC<{
hasOutdatedJobConfigurations: boolean;
hasOutdatedJobDefinitions: boolean;
hasSetupCapabilities: boolean;
hasStoppedJobs: boolean;
isFirstUse: boolean;
moduleName: string;
@ -22,6 +23,7 @@ export const LogAnalysisJobProblemIndicator: React.FC<{
}> = ({
hasOutdatedJobConfigurations,
hasOutdatedJobDefinitions,
hasSetupCapabilities,
hasStoppedJobs,
isFirstUse,
moduleName,
@ -32,12 +34,14 @@ export const LogAnalysisJobProblemIndicator: React.FC<{
<>
{hasOutdatedJobDefinitions ? (
<JobDefinitionOutdatedCallout
hasSetupCapabilities={hasSetupCapabilities}
moduleName={moduleName}
onRecreateMlJob={onRecreateMlJobForUpdate}
/>
) : null}
{hasOutdatedJobConfigurations ? (
<JobConfigurationOutdatedCallout
hasSetupCapabilities={hasSetupCapabilities}
moduleName={moduleName}
onRecreateMlJob={onRecreateMlJobForReconfiguration}
/>

View file

@ -12,6 +12,7 @@ import { CategoryQualityWarnings } from './quality_warning_notices';
export const CategoryJobNoticesSection: React.FC<{
hasOutdatedJobConfigurations: boolean;
hasOutdatedJobDefinitions: boolean;
hasSetupCapabilities: boolean;
hasStoppedJobs: boolean;
isFirstUse: boolean;
moduleName: string;
@ -21,6 +22,7 @@ export const CategoryJobNoticesSection: React.FC<{
}> = ({
hasOutdatedJobConfigurations,
hasOutdatedJobDefinitions,
hasSetupCapabilities,
hasStoppedJobs,
isFirstUse,
moduleName,
@ -32,6 +34,7 @@ export const CategoryJobNoticesSection: React.FC<{
<LogAnalysisJobProblemIndicator
hasOutdatedJobConfigurations={hasOutdatedJobConfigurations}
hasOutdatedJobDefinitions={hasOutdatedJobDefinitions}
hasSetupCapabilities={hasSetupCapabilities}
hasStoppedJobs={hasStoppedJobs}
isFirstUse={isFirstUse}
moduleName={moduleName}

View file

@ -1,18 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiButton, PropsOf } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
export const RecreateJobButton: React.FunctionComponent<PropsOf<typeof EuiButton>> = (props) => (
<EuiButton {...props}>
<FormattedMessage
id="xpack.infra.logs.analysis.recreateJobButtonLabel"
defaultMessage="Recreate ML job"
/>
</EuiButton>
);

View file

@ -4,17 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiCallOut } from '@elastic/eui';
import { RecreateJobButton } from './recreate_job_button';
import React from 'react';
import { RecreateJobButton } from '../log_analysis_setup/create_job_button';
export const RecreateJobCallout: React.FC<{
hasSetupCapabilities?: boolean;
onRecreateMlJob: () => void;
title?: React.ReactNode;
}> = ({ children, onRecreateMlJob, title }) => (
}> = ({ children, hasSetupCapabilities, onRecreateMlJob, title }) => (
<EuiCallOut color="warning" iconType="alert" title={title}>
<p>{children}</p>
<RecreateJobButton color="warning" onClick={onRecreateMlJob} />
<RecreateJobButton
color="warning"
hasSetupCapabilities={hasSetupCapabilities}
onClick={onRecreateMlJob}
/>
</EuiCallOut>
);

View file

@ -0,0 +1,49 @@
/*
* 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 { EuiButton, PropsOf } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { MissingSetupPrivilegesToolTip } from './missing_setup_privileges_tooltip';
export const CreateJobButton: React.FunctionComponent<
{
hasSetupCapabilities?: boolean;
} & PropsOf<typeof EuiButton>
> = ({ hasSetupCapabilities = true, children, ...buttonProps }) => {
const button = (
<EuiButton isDisabled={!hasSetupCapabilities} {...buttonProps}>
{children ?? (
<FormattedMessage
id="xpack.infra.logs.analysis.createJobButtonLabel"
defaultMessage="Create ML jobs"
/>
)}
</EuiButton>
);
return hasSetupCapabilities ? (
button
) : (
<MissingSetupPrivilegesToolTip position="bottom" delay="regular">
{button}
</MissingSetupPrivilegesToolTip>
);
};
export const RecreateJobButton: React.FunctionComponent<PropsOf<typeof CreateJobButton>> = ({
children,
...otherProps
}) => (
<CreateJobButton {...otherProps}>
{children ?? (
<FormattedMessage
id="xpack.infra.logs.analysis.recreateJobButtonLabel"
defaultMessage="Recreate ML job"
/>
)}
</CreateJobButton>
);

View file

@ -0,0 +1,30 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const missingMlPrivilegesTitle = i18n.translate(
'xpack.infra.logs.analysis.missingMlPrivilegesTitle',
{
defaultMessage: 'Additional Machine Learning privileges required',
}
);
export const missingMlResultsPrivilegesDescription = i18n.translate(
'xpack.infra.logs.analysis.missingMlResultsPrivilegesDescription',
{
defaultMessage:
'This feature makes use of Machine Learning jobs, which require at least the read permission for the Machine Learning app in order to access their status and results.',
}
);
export const missingMlSetupPrivilegesDescription = i18n.translate(
'xpack.infra.logs.analysis.missingMlSetupPrivilegesDescription',
{
defaultMessage:
'This feature makes use of Machine Learning jobs, which require all permissions for the Machine Learning app in order to be set up.',
}
);

View file

@ -4,34 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiEmptyPrompt, EuiCode } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiEmptyPrompt } from '@elastic/eui';
import React from 'react';
import { euiStyled } from '../../../../../observability/public';
import {
missingMlPrivilegesTitle,
missingMlResultsPrivilegesDescription,
} from './missing_privileges_messages';
import { UserManagementLink } from './user_management_link';
export const MissingResultsPrivilegesPrompt: React.FunctionComponent = () => (
<EmptyPrompt
title={
<h2>
<FormattedMessage
id="xpack.infra.logs.analysis.missingMlResultsPrivilegesTitle"
defaultMessage="Additional Machine Learning privileges required"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.infra.logs.analysis.missingMlResultsPrivilegesBody"
defaultMessage="This feature makes use of Machine Learning jobs, which require at least the {machineLearningUserRole} role in order to access their status and results."
values={{
machineLearningUserRole: <EuiCode>machine_learning_user</EuiCode>,
}}
/>
</p>
}
title={<h2>{missingMlPrivilegesTitle}</h2>}
body={<p>{missingMlResultsPrivilegesDescription}</p>}
actions={<UserManagementLink />}
/>
);

View file

@ -4,34 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiEmptyPrompt, EuiCode } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiEmptyPrompt } from '@elastic/eui';
import React from 'react';
import { euiStyled } from '../../../../../observability/public';
import {
missingMlPrivilegesTitle,
missingMlSetupPrivilegesDescription,
} from './missing_privileges_messages';
import { UserManagementLink } from './user_management_link';
export const MissingSetupPrivilegesPrompt: React.FunctionComponent = () => (
<EmptyPrompt
title={
<h2>
<FormattedMessage
id="xpack.infra.logs.analysis.missingMlSetupPrivilegesTitle"
defaultMessage="Additional Machine Learning privileges required"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.infra.logs.analysis.missingMlSetupPrivilegesBody"
defaultMessage="This feature makes use of Machine Learning jobs, which require the {machineLearningAdminRole} role in order to be set up."
values={{
machineLearningAdminRole: <EuiCode>machine_learning_admin</EuiCode>,
}}
/>
</p>
}
title={<h2>{missingMlPrivilegesTitle}</h2>}
body={<p>{missingMlSetupPrivilegesDescription}</p>}
actions={<UserManagementLink />}
/>
);

View file

@ -0,0 +1,23 @@
/*
* 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 { EuiToolTip, PropsOf } from '@elastic/eui';
import React from 'react';
import {
missingMlPrivilegesTitle,
missingMlSetupPrivilegesDescription,
} from './missing_privileges_messages';
export const MissingSetupPrivilegesToolTip: React.FC<Omit<
PropsOf<EuiToolTip>,
'content' | 'title'
>> = (props) => (
<EuiToolTip
{...props}
content={missingMlSetupPrivilegesDescription}
title={missingMlPrivilegesTitle}
/>
);

View file

@ -6,6 +6,7 @@
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { useCallback } from 'react';
import { useLogAnalysisCapabilitiesContext } from '../../../../containers/logs/log_analysis';
import {
logEntryCategoriesModule,
useLogEntryCategoriesModuleContext,
@ -20,6 +21,7 @@ import type { ModuleId } from './setup_flyout_state';
export const LogAnalysisModuleList: React.FC<{
onViewModuleSetup: (module: ModuleId) => void;
}> = ({ onViewModuleSetup }) => {
const { hasLogAnalysisSetupCapabilities } = useLogAnalysisCapabilitiesContext();
const { setupStatus: logEntryRateSetupStatus } = useLogEntryRateModuleContext();
const { setupStatus: logEntryCategoriesSetupStatus } = useLogEntryCategoriesModuleContext();
@ -35,6 +37,7 @@ export const LogAnalysisModuleList: React.FC<{
<EuiFlexGroup>
<EuiFlexItem>
<LogAnalysisModuleListCard
hasSetupCapabilities={hasLogAnalysisSetupCapabilities}
moduleDescription={logEntryRateModule.moduleDescription}
moduleName={logEntryRateModule.moduleName}
moduleStatus={logEntryRateSetupStatus}
@ -43,6 +46,7 @@ export const LogAnalysisModuleList: React.FC<{
</EuiFlexItem>
<EuiFlexItem>
<LogAnalysisModuleListCard
hasSetupCapabilities={hasLogAnalysisSetupCapabilities}
moduleDescription={logEntryCategoriesModule.moduleDescription}
moduleName={logEntryCategoriesModule.moduleName}
moduleStatus={logEntryCategoriesSetupStatus}

View file

@ -5,41 +5,42 @@
*/
import { EuiCard, EuiIcon } from '@elastic/eui';
import React from 'react';
import { EuiButton } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { RecreateJobButton } from '../../log_analysis_job_status';
import React from 'react';
import { SetupStatus } from '../../../../../common/log_analysis';
import { CreateJobButton, RecreateJobButton } from '../../log_analysis_setup/create_job_button';
export const LogAnalysisModuleListCard: React.FC<{
hasSetupCapabilities: boolean;
moduleDescription: string;
moduleName: string;
moduleStatus: SetupStatus;
onViewSetup: () => void;
}> = ({ moduleDescription, moduleName, moduleStatus, onViewSetup }) => {
const icon =
}> = ({ hasSetupCapabilities, moduleDescription, moduleName, moduleStatus, onViewSetup }) => {
const moduleIcon =
moduleStatus.type === 'required' ? (
<EuiIcon size="xxl" type="machineLearningApp" />
) : (
<EuiIcon color="secondary" size="xxl" type="check" />
);
const footerContent =
const moduleSetupButton =
moduleStatus.type === 'required' ? (
<EuiButton onClick={onViewSetup}>
<CreateJobButton hasSetupCapabilities={hasSetupCapabilities} onClick={onViewSetup}>
<FormattedMessage
id="xpack.infra.logs.analysis.enableAnomalyDetectionButtonLabel"
defaultMessage="Enable anomaly detection"
/>
</EuiButton>
</CreateJobButton>
) : (
<RecreateJobButton onClick={onViewSetup} />
<RecreateJobButton hasSetupCapabilities={hasSetupCapabilities} onClick={onViewSetup} />
);
return (
<EuiCard
description={moduleDescription}
footer={<div>{footerContent}</div>}
icon={icon}
footer={<div>{moduleSetupButton}</div>}
icon={moduleIcon}
title={moduleName}
/>
);

View file

@ -11,8 +11,8 @@ import { useLinkProps } from '../../../hooks/use_link_props';
export const UserManagementLink: React.FunctionComponent<EuiButtonProps> = (props) => {
const linkProps = useLinkProps({
app: 'kibana',
hash: '/management/security/users',
app: 'management',
pathname: '/security/users',
});
return (
<EuiButton color="primary" fill {...linkProps} {...props}>

View file

@ -23,6 +23,7 @@ import {
StringTimeRange,
useLogEntryCategoriesResultsUrlState,
} from './use_log_entry_categories_results_url_state';
import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis/log_analysis_capabilities';
const JOB_STATUS_POLLING_INTERVAL = 30000;
@ -36,6 +37,8 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent<LogEntryC
useTrackPageview({ app: 'infra_logs', path: 'log_entry_categories_results' });
useTrackPageview({ app: 'infra_logs', path: 'log_entry_categories_results', delay: 15000 });
const { hasLogAnalysisSetupCapabilities } = useLogAnalysisCapabilitiesContext();
const {
fetchJobStatus,
fetchModuleDefinition,
@ -197,6 +200,7 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent<LogEntryC
<CategoryJobNoticesSection
hasOutdatedJobConfigurations={hasOutdatedJobConfigurations}
hasOutdatedJobDefinitions={hasOutdatedJobDefinitions}
hasSetupCapabilities={hasLogAnalysisSetupCapabilities}
hasStoppedJobs={hasStoppedJobs}
isFirstUse={isFirstUse}
moduleName={moduleDescriptor.moduleName}
@ -209,6 +213,7 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent<LogEntryC
<EuiPanel paddingSize="m">
<TopCategoriesSection
availableDatasets={logEntryCategoryDatasets}
hasSetupCapabilities={hasLogAnalysisSetupCapabilities}
isLoadingDatasets={isLoadingLogEntryCategoryDatasets}
isLoadingTopCategories={isLoadingTopLogEntryCategories}
jobId={jobIds['log-entry-categories-count']}

View file

@ -12,13 +12,14 @@ import { LogEntryCategory } from '../../../../../../common/http_api/log_analysis
import { TimeRange } from '../../../../../../common/http_api/shared';
import { BetaBadge } from '../../../../../components/beta_badge';
import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay_wrapper';
import { RecreateJobButton } from '../../../../../components/logging/log_analysis_job_status';
import { RecreateJobButton } from '../../../../../components/logging/log_analysis_setup/create_job_button';
import { AnalyzeInMlButton } from '../../../../../components/logging/log_analysis_results';
import { DatasetsSelector } from '../../../../../components/logging/log_analysis_results/datasets_selector';
import { TopCategoriesTable } from './top_categories_table';
export const TopCategoriesSection: React.FunctionComponent<{
availableDatasets: string[];
hasSetupCapabilities: boolean;
isLoadingDatasets?: boolean;
isLoadingTopCategories?: boolean;
jobId: string;
@ -30,6 +31,7 @@ export const TopCategoriesSection: React.FunctionComponent<{
topCategories: LogEntryCategory[];
}> = ({
availableDatasets,
hasSetupCapabilities,
isLoadingDatasets = false,
isLoadingTopCategories = false,
jobId,
@ -51,7 +53,11 @@ export const TopCategoriesSection: React.FunctionComponent<{
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<RecreateJobButton onClick={onRequestRecreateMlJob} size="s" />
<RecreateJobButton
hasSetupCapabilities={hasSetupCapabilities}
onClick={onRequestRecreateMlJob}
size="s"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<AnalyzeInMlButton jobId={jobId} timeRange={timeRange} />

View file

@ -15,7 +15,9 @@ import {
CategoryJobNoticesSection,
LogAnalysisJobProblemIndicator,
} from '../../../components/logging/log_analysis_job_status';
import { DatasetsSelector } from '../../../components/logging/log_analysis_results/datasets_selector';
import { useLogAnalysisSetupFlyoutStateContext } from '../../../components/logging/log_analysis_setup/setup_flyout';
import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis/log_analysis_capabilities';
import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { useLogEntryRateModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_rate';
import { useLogSourceContext } from '../../../containers/logs/log_source';
@ -27,7 +29,6 @@ import {
StringTimeRange,
useLogAnalysisResultsUrlState,
} from './use_log_entry_rate_results_url_state';
import { DatasetsSelector } from '../../../components/logging/log_analysis_results/datasets_selector';
export const SORT_DEFAULTS = {
direction: 'desc' as const,
@ -44,6 +45,8 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => {
const { sourceId } = useLogSourceContext();
const { hasLogAnalysisSetupCapabilities } = useLogAnalysisCapabilitiesContext();
const {
hasOutdatedJobConfigurations: hasOutdatedLogEntryRateJobConfigurations,
hasOutdatedJobDefinitions: hasOutdatedLogEntryRateJobDefinitions,
@ -223,6 +226,7 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => {
<LogAnalysisJobProblemIndicator
hasOutdatedJobConfigurations={hasOutdatedLogEntryRateJobConfigurations}
hasOutdatedJobDefinitions={hasOutdatedLogEntryRateJobDefinitions}
hasSetupCapabilities={hasLogAnalysisSetupCapabilities}
hasStoppedJobs={hasStoppedLogEntryRateJobs}
isFirstUse={false /* the first use message is already shown by the section below */}
moduleName={logEntryRateModuleDescriptor.moduleName}
@ -232,6 +236,7 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => {
<CategoryJobNoticesSection
hasOutdatedJobConfigurations={hasOutdatedLogEntryCategoriesJobConfigurations}
hasOutdatedJobDefinitions={hasOutdatedLogEntryCategoriesJobDefinitions}
hasSetupCapabilities={hasLogAnalysisSetupCapabilities}
hasStoppedJobs={hasStoppedLogEntryCategoriesJobs}
isFirstUse={isFirstUse}
moduleName={logEntryCategoriesModuleDescriptor.moduleName}

View file

@ -7415,10 +7415,6 @@
"xpack.infra.logs.analysis.anomalySectionNoDataTitle": "表示するデータがありません。",
"xpack.infra.logs.analysis.jobStoppedCalloutMessage": "ML ジョブが手動またはリソース不足により停止しました。新しいログエントリーはジョブが再起動するまで処理されません。",
"xpack.infra.logs.analysis.jobStoppedCalloutTitle": "ML ジョブが停止しました",
"xpack.infra.logs.analysis.missingMlResultsPrivilegesBody": "本機能は機械学習ジョブを利用し、そのステータスと結果にアクセスするためには、少なくとも{machineLearningUserRole}ロールが必要です。",
"xpack.infra.logs.analysis.missingMlResultsPrivilegesTitle": "追加の機械学習の権限が必要です",
"xpack.infra.logs.analysis.missingMlSetupPrivilegesBody": "本機能は機械学習ジョブを利用し、設定には{machineLearningAdminRole}ロールが必要です。",
"xpack.infra.logs.analysis.missingMlSetupPrivilegesTitle": "追加の機械学習の権限が必要です",
"xpack.infra.logs.analysis.mlAppButton": "機械学習を開く",
"xpack.infra.logs.analysis.mlUnavailableBody": "詳細は{machineLearningAppLink}をご覧ください。",
"xpack.infra.logs.analysis.mlUnavailableTitle": "この機能には機械学習が必要です",

View file

@ -7420,10 +7420,6 @@
"xpack.infra.logs.analysis.anomalySectionNoDataTitle": "没有可显示的数据。",
"xpack.infra.logs.analysis.jobStoppedCalloutMessage": "ML 作业已手动停止或由于缺乏资源而停止。作业重新启动后,才会处理新的日志条目。",
"xpack.infra.logs.analysis.jobStoppedCalloutTitle": "ML 作业已停止",
"xpack.infra.logs.analysis.missingMlResultsPrivilegesBody": "此功能使用 Machine Learning 作业,要访问这些作业的状态和结果,至少需要 {machineLearningUserRole} 角色。",
"xpack.infra.logs.analysis.missingMlResultsPrivilegesTitle": "需要额外的 Machine Learning 权限",
"xpack.infra.logs.analysis.missingMlSetupPrivilegesBody": "此功能使用 Machine Learning 作业,这需要 {machineLearningAdminRole} 角色才能设置。",
"xpack.infra.logs.analysis.missingMlSetupPrivilegesTitle": "需要额外的 Machine Learning 权限",
"xpack.infra.logs.analysis.mlAppButton": "打开 Machine Learning",
"xpack.infra.logs.analysis.mlUnavailableBody": "查看 {machineLearningAppLink} 以获取更多信息。",
"xpack.infra.logs.analysis.mlUnavailableTitle": "此功能需要 Machine Learning",