[ML] Update the Overview page (#159609)

## Summary

Resolves https://github.com/elastic/kibana/issues/154294 and updates the
UI of the Overview page

- Updates panels layout 
- Stores expand/collapsed state of the panels in the local storage 
- Update empty states text and layout 

<img width="1341" alt="image"
src="8833fa2a-b574-44ee-bacb-e974186dd35f">


### Checklist

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
This commit is contained in:
Dima Arnautov 2023-06-20 13:56:00 +02:00 committed by GitHub
parent 12a2203d10
commit 6ac52fb9ec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 457 additions and 319 deletions

View file

@ -14,6 +14,7 @@ export const ML_GETTING_STARTED_CALLOUT_DISMISSED = 'ml.gettingStarted.isDismiss
export const ML_FROZEN_TIER_PREFERENCE = 'ml.frozenDataTierPreference';
export const ML_ANOMALY_EXPLORER_PANELS = 'ml.anomalyExplorerPanels';
export const ML_NOTIFICATIONS_LAST_CHECKED_AT = 'ml.notificationsLastCheckedAt';
export const ML_OVERVIEW_PANELS = 'ml.overviewPanels';
export type PartitionFieldConfig =
| {
@ -52,6 +53,12 @@ export interface AnomalyExplorerPanelsState {
mainPage: { size: number };
}
export interface OverviewPanelsState {
nodes: boolean;
adJobs: boolean;
dfaJobs: boolean;
}
export interface MlStorageRecord {
[key: string]: unknown;
[ML_ENTITY_FIELDS_CONFIG]: PartitionFieldsConfig;
@ -60,6 +67,7 @@ export interface MlStorageRecord {
[ML_FROZEN_TIER_PREFERENCE]: FrozenTierPreference;
[ML_ANOMALY_EXPLORER_PANELS]: AnomalyExplorerPanelsState | undefined;
[ML_NOTIFICATIONS_LAST_CHECKED_AT]: number | undefined;
[ML_OVERVIEW_PANELS]: OverviewPanelsState;
}
export type MlStorage = Partial<MlStorageRecord> | null;
@ -78,6 +86,8 @@ export type TMlStorageMapped<T extends MlStorageKey> = T extends typeof ML_ENTIT
? AnomalyExplorerPanelsState | undefined
: T extends typeof ML_NOTIFICATIONS_LAST_CHECKED_AT
? number | undefined
: T extends typeof ML_OVERVIEW_PANELS
? OverviewPanelsState | undefined
: null;
export const ML_STORAGE_KEYS = [
@ -87,4 +97,5 @@ export const ML_STORAGE_KEYS = [
ML_FROZEN_TIER_PREFERENCE,
ML_ANOMALY_EXPLORER_PANELS,
ML_NOTIFICATIONS_LAST_CHECKED_AT,
ML_OVERVIEW_PANELS,
] as const;

View file

@ -0,0 +1,137 @@
/*
* 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 {
EuiBadge,
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiSplitPanel,
EuiText,
EuiTitle,
} from '@elastic/eui';
import React, { type FC } from 'react';
import { css } from '@emotion/react/dist/emotion-react.cjs';
import { useCurrentThemeVars } from '../../contexts/kibana';
export interface CollapsiblePanelProps {
isOpen: boolean;
onToggle: (isOpen: boolean) => void;
header: React.ReactElement;
headerItems?: React.ReactElement[];
}
export const CollapsiblePanel: FC<CollapsiblePanelProps> = ({
isOpen,
onToggle,
children,
header,
headerItems,
}) => {
const { euiTheme } = useCurrentThemeVars();
return (
<EuiSplitPanel.Outer
grow
hasShadow={false}
css={{
border: `${euiTheme.euiBorderWidthThin} solid ${
isOpen ? euiTheme.euiBorderColor : 'transparent'
}`,
}}
>
<EuiSplitPanel.Inner color={isOpen ? 'plain' : 'subdued'}>
<EuiFlexGroup justifyContent={'spaceBetween'} alignItems={'center'}>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize={'s'}>
<EuiFlexItem grow={false}>
<EuiButtonIcon
color={'text'}
iconType={isOpen ? 'arrowDown' : 'arrowRight'}
onClick={() => {
onToggle(!isOpen);
}}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiTitle size="xxs">
<h2>{header}</h2>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
{headerItems ? (
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize={'l'} alignItems={'center'}>
{headerItems.map((item, i) => {
return (
<EuiFlexItem key={i} grow={false}>
<div
css={
i < headerItems?.length - 1
? css`
border-right: ${euiTheme.euiBorderWidthThin} solid
${euiTheme.euiBorderColor};
padding-right: ${euiTheme.euiPanelPaddingModifiers.paddingLarge};
`
: null
}
>
{item}
</div>
</EuiFlexItem>
);
})}
</EuiFlexGroup>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
</EuiSplitPanel.Inner>
{isOpen ? (
<EuiSplitPanel.Inner
css={{ borderTop: `${euiTheme.euiBorderWidthThin} solid ${euiTheme.euiBorderColor}` }}
grow={false}
>
{children}
</EuiSplitPanel.Inner>
) : null}
</EuiSplitPanel.Outer>
);
};
export interface StatEntry {
label: string;
value: number;
'data-test-subj'?: string;
}
export interface OverviewStatsBarProps {
inputStats: StatEntry[];
dataTestSub?: string;
}
export const OverviewStatsBar: FC<OverviewStatsBarProps> = ({ inputStats, dataTestSub }) => {
return (
<EuiFlexGroup data-test-subj={dataTestSub} alignItems={'center'} gutterSize={'m'}>
{inputStats.map(({ value, label, 'data-test-subj': dataTestSubjValue }) => {
return (
<EuiFlexItem grow={false} key={label}>
<EuiFlexGroup alignItems={'center'} gutterSize={'s'}>
<EuiFlexItem grow={false}>
<EuiText size={'s'}>{label}:</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadge data-test-subj={dataTestSubjValue}>{value}</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
);
})}
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,8 @@
/*
* 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 { CollapsiblePanel } from './collapsible_panel';

View file

@ -6,16 +6,7 @@
*/
import React, { FC } from 'react';
import {
EuiButton,
EuiCallOut,
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiImage,
EuiLink,
EuiTitle,
} from '@elastic/eui';
import { EuiButton, EuiEmptyPrompt, EuiImage, EuiLink } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import dfaImage from './data_frame_analytics_kibana.png';
@ -26,10 +17,7 @@ import { usePermissionCheck } from '../../../../../capabilities/check_capabiliti
export const AnalyticsEmptyPrompt: FC = () => {
const {
services: {
docLinks,
http: { basePath },
},
services: { docLinks },
} = useMlKibana();
const [canCreateDataFrameAnalytics, canStartStopDataFrameAnalytics] = usePermissionCheck([
@ -40,7 +28,6 @@ export const AnalyticsEmptyPrompt: FC = () => {
const disabled =
!mlNodesAvailable() || !canCreateDataFrameAnalytics || !canStartStopDataFrameAnalytics;
const transformsLink = `${basePath.get()}/app/management/data/transform`;
const navigateToPath = useNavigateToPath();
const navigateToSourceSelection = async () => {
@ -57,16 +44,15 @@ export const AnalyticsEmptyPrompt: FC = () => {
size="fullWidth"
src={dfaImage}
alt={i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptTitle', {
defaultMessage: 'Create your first data frame analytics job',
defaultMessage: 'Analyze your data with data frame analytics',
})}
/>
}
color="subdued"
title={
<h2>
<FormattedMessage
id="xpack.ml.dataFrame.analyticsList.emptyPromptTitle"
defaultMessage="Create your first data frame analytics job"
defaultMessage="Analyze your data with data frame analytics"
/>
</h2>
}
@ -78,39 +64,6 @@ export const AnalyticsEmptyPrompt: FC = () => {
defaultMessage="Train outlier detection, regression, or classification machine learning models using data frame analytics."
/>
</p>
<EuiCallOut
size="s"
title={
<FormattedMessage
id="xpack.ml.overview.analyticsList.emptyPromptHelperText"
defaultMessage="Before building a data frame analytics job, use {transforms} to construct an {sourcedata}."
values={{
transforms: (
<EuiLink href={transformsLink} target="blank" color={'accent'}>
<FormattedMessage
id="xpack.ml.overview.gettingStartedSectionTransforms"
defaultMessage="transforms"
/>
</EuiLink>
),
sourcedata: (
<EuiLink
href={docLinks.links.ml.dFAPrepareData}
target="blank"
color={'accent'}
external
>
<FormattedMessage
id="xpack.ml.overview.gettingStartedSectionSourceData"
defaultMessage="entity-centric source data set"
/>
</EuiLink>
),
}}
/>
}
iconType="iInCircle"
/>
</>
}
actions={[
@ -118,37 +71,19 @@ export const AnalyticsEmptyPrompt: FC = () => {
onClick={navigateToSourceSelection}
isDisabled={disabled}
color="primary"
iconType="plusInCircle"
fill
data-test-subj="mlAnalyticsCreateFirstButton"
>
{i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptButtonText', {
defaultMessage: 'Create job',
defaultMessage: 'Create data frame analytics job',
})}
</EuiButton>,
<EuiLink href={docLinks.links.ml.dataFrameAnalytics} target="_blank" external>
<FormattedMessage
id="xpack.ml.common.readDocumentationLink"
defaultMessage="Read documentation"
/>
</EuiLink>,
]}
footer={
<EuiFlexGroup gutterSize={'xs'} alignItems={'center'}>
<EuiFlexItem grow={false}>
<EuiTitle size="xxs">
<h3>
<FormattedMessage
id="xpack.ml.common.learnMoreQuestion"
defaultMessage="Want to learn more?"
/>
</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLink href={docLinks.links.ml.dataFrameAnalytics} target="_blank" external>
<FormattedMessage
id="xpack.ml.common.readDocumentationLink"
defaultMessage="Read documentation"
/>
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
}
data-test-subj="mlNoDataFrameAnalyticsFound"
/>
);

View file

@ -67,7 +67,7 @@ describe('get_analytics', () => {
// act and assert
expect(getAnalyticsJobsStats(mockResponse)).toEqual({
total: {
label: 'Total analytics jobs',
label: 'Total',
value: 2,
show: true,
},

View file

@ -47,7 +47,7 @@ export function getInitialAnalyticsStats(): AnalyticStatsBarStats {
return {
total: {
label: i18n.translate('xpack.ml.overview.statsBar.totalAnalyticsLabel', {
defaultMessage: 'Total analytics jobs',
defaultMessage: 'Total',
}),
value: 0,
show: true,
@ -97,12 +97,18 @@ export function getAnalyticsJobsStats(
);
resultStats.failed.show = resultStats.failed.value > 0;
resultStats.total.value = analyticsStats.count;
if (resultStats.total.value === 0) {
resultStats.started.show = false;
resultStats.stopped.show = false;
}
return resultStats;
}
export const getAnalyticsFactory = (
setAnalytics: React.Dispatch<React.SetStateAction<DataFrameAnalyticsListRow[]>>,
setAnalyticsStats: React.Dispatch<React.SetStateAction<AnalyticStatsBarStats | undefined>>,
setAnalyticsStats: (update: AnalyticStatsBarStats | undefined) => void,
setErrorMessage: React.Dispatch<
React.SetStateAction<GetDataFrameAnalyticsStatsResponseError | undefined>
>,

View file

@ -7,15 +7,7 @@
import React, { FC } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiButton,
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiImage,
EuiLink,
EuiTitle,
} from '@elastic/eui';
import { EuiButton, EuiEmptyPrompt, EuiImage, EuiLink } from '@elastic/eui';
import adImage from './anomaly_detection_kibana.png';
import { ML_PAGES } from '../../../../../../common/constants/locator';
import { useMlKibana, useMlLocator, useNavigateToPath } from '../../../../contexts/kibana';
@ -47,12 +39,11 @@ export const AnomalyDetectionEmptyState: FC = () => {
hasBorder={false}
hasShadow={false}
icon={<EuiImage size="fullWidth" src={adImage} alt="anomaly_detection" />}
color="subdued"
title={
<h2>
<FormattedMessage
id="xpack.ml.overview.anomalyDetection.createFirstJobMessage"
defaultMessage="Create your first anomaly detection job"
defaultMessage="Start detecting anomalies"
/>
</h2>
}
@ -66,43 +57,25 @@ export const AnomalyDetectionEmptyState: FC = () => {
</p>
</>
}
actions={
actions={[
<EuiButton
color="primary"
onClick={redirectToCreateJobSelectIndexPage}
fill
iconType="plusInCircle"
isDisabled={disableCreateAnomalyDetectionJob}
data-test-subj="mlCreateNewJobButton"
>
<FormattedMessage
id="xpack.ml.overview.anomalyDetection.createJobButtonText"
defaultMessage="Create job"
defaultMessage="Create anomaly detection job"
/>
</EuiButton>
}
footer={
<EuiFlexGroup gutterSize={'xs'} alignItems={'center'}>
<EuiFlexItem grow={false}>
<EuiTitle size="xxs">
<h3>
<FormattedMessage
id="xpack.ml.common.learnMoreQuestion"
defaultMessage="Want to learn more?"
/>
</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLink href={docLinks.links.ml.anomalyDetection} target="_blank" external>
<FormattedMessage
id="xpack.ml.common.readDocumentationLink"
defaultMessage="Read documentation"
/>
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
}
</EuiButton>,
<EuiLink href={docLinks.links.ml.anomalyDetection} target="_blank" external>
<FormattedMessage
id="xpack.ml.common.readDocumentationLink"
defaultMessage="Read documentation"
/>
</EuiLink>,
]}
data-test-subj="mlAnomalyDetectionEmptyState"
/>
);

View file

@ -200,15 +200,20 @@ export const NodesList: FC<NodesListProps> = ({ compactView = false }) => {
return (
<div data-test-subj={'mlNodesOverviewPanel'}>
<EuiSpacer size="m" />
<EuiFlexGroup justifyContent="spaceBetween">
{nodesStats && (
<EuiFlexItem grow={false}>
<StatsBar stats={nodesStats} dataTestSub={'mlTrainedModelsNodesStatsBar'} />
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiSpacer size="m" />
{nodesStats && !compactView ? (
<>
<EuiSpacer size="m" />
<EuiFlexGroup justifyContent="spaceBetween">
{nodesStats && (
<EuiFlexItem grow={false}>
<StatsBar stats={nodesStats} dataTestSub={'mlTrainedModelsNodesStatsBar'} />
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiSpacer size="m" />
</>
) : null}
<div data-test-subj="mlNodesTableContainer">
<EuiInMemoryTable<NodeItem>
allowNeutralSort={false}

View file

@ -43,3 +43,7 @@ export function lazyMlNodesAvailable() {
export function permissionToViewMlNodeCount() {
return userHasPermissionToViewMlNodeCount;
}
export function getMlNodesCount(): number {
return mlNodeCount;
}

View file

@ -5,28 +5,32 @@
* 2.0.
*/
import React, { FC, useEffect, useState } from 'react';
import {
EuiButton,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiLoadingSpinner,
EuiPanel,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import React, { FC, useCallback, useEffect, useState } from 'react';
import { EuiCallOut, EuiLink, EuiLoadingSpinner } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useStorage } from '@kbn/ml-local-storage';
import { FormattedMessage } from '@kbn/i18n-react';
import { type AnalyticStatsBarStats } from '../../../components/stats_bar';
import {
OverviewStatsBar,
type StatEntry,
} from '../../../components/collapsible_panel/collapsible_panel';
import {
ML_OVERVIEW_PANELS,
MlStorageKey,
TMlStorageMapped,
} from '../../../../../common/types/storage';
import { AnalyticsTable } from './table';
import { getAnalyticsFactory } from '../../../data_frame_analytics/pages/analytics_management/services/analytics_service';
import { DataFrameAnalyticsListRow } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common';
import { AnalyticStatsBarStats, StatsBar } from '../../../components/stats_bar';
import { useMlLink } from '../../../contexts/kibana';
import { ML_PAGES } from '../../../../../common/constants/locator';
import { useRefresh } from '../../../routing/use_refresh';
import type { GetDataFrameAnalyticsStatsResponseError } from '../../../services/ml_api_service/data_frame_analytics';
import { AnalyticsEmptyPrompt } from '../../../data_frame_analytics/pages/analytics_management/components/empty_prompt';
import { overviewPanelDefaultState } from '../../overview_page';
import { CollapsiblePanel } from '../../../components/collapsible_panel';
interface Props {
setLazyJobCount: React.Dispatch<React.SetStateAction<number>>;
@ -35,9 +39,7 @@ export const AnalyticsPanel: FC<Props> = ({ setLazyJobCount }) => {
const refresh = useRefresh();
const [analytics, setAnalytics] = useState<DataFrameAnalyticsListRow[]>([]);
const [analyticsStats, setAnalyticsStats] = useState<AnalyticStatsBarStats | undefined>(
undefined
);
const [analyticsStats, setAnalyticsStats] = useState<StatEntry[] | undefined>(undefined);
const [errorMessage, setErrorMessage] = useState<GetDataFrameAnalyticsStatsResponseError>();
const [isInitialized, setIsInitialized] = useState(false);
@ -45,9 +47,24 @@ export const AnalyticsPanel: FC<Props> = ({ setLazyJobCount }) => {
page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE,
});
const [panelsState, setPanelsState] = useStorage<
MlStorageKey,
TMlStorageMapped<typeof ML_OVERVIEW_PANELS>
>(ML_OVERVIEW_PANELS, overviewPanelDefaultState);
const setAnalyticsStatsCustom = useCallback((stats: AnalyticStatsBarStats | undefined) => {
if (!stats) return;
const result = Object.entries(stats)
.filter(([k, v]) => v.show)
.map(([k, v]) => v);
setAnalyticsStats(result);
}, []);
const getAnalytics = getAnalyticsFactory(
setAnalytics,
setAnalyticsStats,
setAnalyticsStatsCustom,
setErrorMessage,
setIsInitialized,
setLazyJobCount,
@ -78,58 +95,40 @@ export const AnalyticsPanel: FC<Props> = ({ setLazyJobCount }) => {
const noDFAJobs = errorMessage === undefined && isInitialized === true && analytics.length === 0;
return (
<>
{noDFAJobs ? (
<AnalyticsEmptyPrompt />
) : (
<EuiPanel
css={isInitialized ? {} : { textAlign: 'center', padding: '10%' }}
hasShadow={false}
hasBorder
>
{typeof errorMessage !== 'undefined' ? errorDisplay : null}
{isInitialized === false && (
<EuiLoadingSpinner css={{ display: 'inline-block' }} size="xl" />
)}
<CollapsiblePanel
isOpen={panelsState.dfaJobs}
onToggle={(update) => {
setPanelsState({ ...panelsState, dfaJobs: update });
}}
header={
<FormattedMessage
id="xpack.ml.overview.analyticsList.PanelTitle"
defaultMessage="Data Frame Analytics Jobs"
/>
}
headerItems={[
...(analyticsStats
? [
<OverviewStatsBar
inputStats={analyticsStats}
dataTestSub={'mlOverviewAnalyticsStatsBar'}
/>,
]
: []),
<EuiLink href={manageJobsLink}>
{i18n.translate('xpack.ml.overview.analyticsList.manageJobsButtonText', {
defaultMessage: 'Manage jobs',
})}
</EuiLink>,
]}
>
{noDFAJobs ? <AnalyticsEmptyPrompt /> : null}
{isInitialized === true && analytics.length > 0 && (
<>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiText size="m">
<h3>
{i18n.translate('xpack.ml.overview.analyticsList.PanelTitle', {
defaultMessage: 'Analytics',
})}
</h3>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize={'s'} alignItems="center">
{analyticsStats !== undefined ? (
<EuiFlexItem grow={false}>
<StatsBar
stats={analyticsStats}
dataTestSub={'mlOverviewAnalyticsStatsBar'}
/>
</EuiFlexItem>
) : null}
<EuiFlexItem grow={false}>
<EuiButton size="m" fill href={manageJobsLink}>
{i18n.translate('xpack.ml.overview.analyticsList.manageJobsButtonText', {
defaultMessage: 'Manage jobs',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<AnalyticsTable items={analytics} />
</>
)}
</EuiPanel>
)}
</>
{typeof errorMessage !== 'undefined' ? errorDisplay : null}
{isInitialized === false && <EuiLoadingSpinner css={{ display: 'inline-block' }} size="xl" />}
{isInitialized === true && analytics.length > 0 ? <AnalyticsTable items={analytics} /> : null}
</CollapsiblePanel>
);
};

View file

@ -6,10 +6,20 @@
*/
import React, { FC, Fragment, useEffect, useState } from 'react';
import { EuiCallOut, EuiLoadingSpinner, EuiPanel } from '@elastic/eui';
import { EuiCallOut, EuiLink, EuiLoadingSpinner } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { zipObject } from 'lodash';
import { useMlKibana } from '../../../contexts/kibana';
import { zipObject, groupBy } from 'lodash';
import { FormattedMessage } from '@kbn/i18n-react';
import { useStorage } from '@kbn/ml-local-storage';
import {
ML_OVERVIEW_PANELS,
MlStorageKey,
TMlStorageMapped,
} from '../../../../../common/types/storage';
import { ML_PAGES } from '../../../../../common/constants/locator';
import { OverviewStatsBar } from '../../../components/collapsible_panel/collapsible_panel';
import { CollapsiblePanel } from '../../../components/collapsible_panel';
import { useMlKibana, useMlLink } from '../../../contexts/kibana';
import { AnomalyDetectionTable } from './table';
import { ml } from '../../../services/ml_api_service';
import { getGroupsFromJobs, getStatsBarData } from './utils';
@ -19,8 +29,8 @@ import { useRefresh } from '../../../routing/use_refresh';
import { useToastNotificationService } from '../../../services/toast_notification_service';
import { AnomalyTimelineService } from '../../../services/anomaly_timeline_service';
import type { OverallSwimlaneData } from '../../../explorer/explorer_utils';
import { JobStatsBarStats } from '../../../components/stats_bar';
import { AnomalyDetectionEmptyState } from '../../../jobs/jobs_list/components/anomaly_detection_empty_state';
import { overviewPanelDefaultState } from '../../overview_page';
export type GroupsDictionary = Dictionary<Group>;
@ -50,10 +60,21 @@ export const AnomalyDetectionPanel: FC<Props> = ({ anomalyTimelineService, setLa
const refresh = useRefresh();
const [panelsState, setPanelsState] = useStorage<
MlStorageKey,
TMlStorageMapped<typeof ML_OVERVIEW_PANELS>
>(ML_OVERVIEW_PANELS, overviewPanelDefaultState);
const manageJobsLink = useMlLink({
page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE,
});
const [isLoading, setIsLoading] = useState(false);
const [groups, setGroups] = useState<GroupsDictionary>({});
const [groupsCount, setGroupsCount] = useState<number>(0);
const [statsBarData, setStatsBarData] = useState<JobStatsBarStats>();
const [statsBarData, setStatsBarData] = useState<Array<{ label: string; value: number }>>();
const [restStatsBarData, setRestStatsBarData] =
useState<Array<{ label: string; value: number }>>();
const [errorMessage, setErrorMessage] = useState<string>();
const loadJobs = async () => {
@ -71,9 +92,20 @@ export const AnomalyDetectionPanel: FC<Props> = ({ anomalyTimelineService, setLa
});
const { groups: jobsGroups, count } = getGroupsFromJobs(jobsSummaryList);
const stats = getStatsBarData(jobsSummaryList);
const statGroups = groupBy(
Object.entries(stats)
.filter(([k, v]) => v.show)
.map(([k, v]) => v),
'group'
);
setIsLoading(false);
setErrorMessage(undefined);
setStatsBarData(stats);
setStatsBarData(statGroups[0]);
setRestStatsBarData(statGroups[1]);
setGroupsCount(count);
setGroups(jobsGroups);
loadOverallSwimLanes(jobsGroups);
@ -138,30 +170,52 @@ export const AnomalyDetectionPanel: FC<Props> = ({ anomalyTimelineService, setLa
</Fragment>
);
const panelClass = isLoading ? 'mlOverviewPanel__isLoading' : 'mlOverviewPanel';
const noAdJobs =
!errorMessage &&
isLoading === false &&
typeof errorMessage === 'undefined' &&
groupsCount === 0;
if (noAdJobs) {
return <AnomalyDetectionEmptyState />;
}
return (
<EuiPanel className={panelClass} hasShadow={false} hasBorder>
<CollapsiblePanel
isOpen={panelsState.adJobs}
onToggle={(update) => {
setPanelsState({ ...panelsState, adJobs: update });
}}
header={
<FormattedMessage
id="xpack.ml.overview.adJobsPanel.header"
defaultMessage="Anomaly Detection Jobs"
/>
}
headerItems={[
...(statsBarData
? [<OverviewStatsBar inputStats={statsBarData} dataTestSub={'mlOverviewJobStatsBar'} />]
: []),
...(restStatsBarData
? [
<OverviewStatsBar
inputStats={restStatsBarData}
dataTestSub={'mlOverviewJobStatsBarExtra'}
/>,
]
: []),
<EuiLink href={manageJobsLink}>
{i18n.translate('xpack.ml.overview.anomalyDetection.manageJobsButtonText', {
defaultMessage: 'Manage jobs',
})}
</EuiLink>,
]}
>
{noAdJobs ? <AnomalyDetectionEmptyState /> : null}
{typeof errorMessage !== 'undefined' && errorDisplay}
{isLoading && <EuiLoadingSpinner className="mlOverviewPanel__spinner" size="xl" />}
{isLoading ? <EuiLoadingSpinner className="mlOverviewPanel__spinner" size="xl" /> : null}
{isLoading === false && typeof errorMessage === 'undefined' && groupsCount > 0 ? (
<AnomalyDetectionTable
items={groups}
statsBarData={statsBarData!}
chartsService={chartsService}
/>
<AnomalyDetectionTable items={groups} chartsService={chartsService} />
) : null}
</EuiPanel>
</CollapsiblePanel>
);
};

View file

@ -9,13 +9,8 @@ import React, { FC, useState } from 'react';
import {
Direction,
EuiBasicTableColumn,
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiInMemoryTable,
EuiSpacer,
EuiText,
EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@ -24,13 +19,10 @@ import { ChartsPluginStart } from '@kbn/charts-plugin/public';
import { formatHumanReadableDateTime } from '@kbn/ml-date-utils';
import { useGroupActions } from './actions';
import { Group, GroupsDictionary } from './anomaly_detection_panel';
import { JobStatsBarStats, StatsBar } from '../../../components/stats_bar';
import { JobSelectorBadge } from '../../../components/job_selector/job_selector_badge';
import { toLocaleString } from '../../../util/string_utils';
import { SwimlaneContainer } from '../../../explorer/swimlane_container';
import { useTimeBuckets } from '../../../components/custom_hooks/use_time_buckets';
import { ML_PAGES } from '../../../../../common/constants/locator';
import { useMlLink } from '../../../contexts/kibana';
export enum AnomalyDetectionListColumns {
id = 'id',
@ -44,11 +36,10 @@ export enum AnomalyDetectionListColumns {
interface Props {
items: GroupsDictionary;
statsBarData: JobStatsBarStats;
chartsService: ChartsPluginStart;
}
export const AnomalyDetectionTable: FC<Props> = ({ items, statsBarData, chartsService }) => {
export const AnomalyDetectionTable: FC<Props> = ({ items, chartsService }) => {
const groupsList = Object.values(items);
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
@ -58,10 +49,6 @@ export const AnomalyDetectionTable: FC<Props> = ({ items, statsBarData, chartsSe
const timeBuckets = useTimeBuckets();
const manageJobsLink = useMlLink({
page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE,
});
const columns: Array<EuiBasicTableColumn<Group>> = [
{
field: AnomalyDetectionListColumns.id,
@ -195,47 +182,19 @@ export const AnomalyDetectionTable: FC<Props> = ({ items, statsBarData, chartsSe
};
return (
<>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize={'s'}>
<EuiFlexItem grow={false}>
<EuiText size="m">
<h3>
{i18n.translate('xpack.ml.overview.anomalyDetection.panelTitle', {
defaultMessage: 'Anomaly Detection',
})}
</h3>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize={'s'} alignItems="center">
<EuiFlexItem grow={false}>
<StatsBar stats={statsBarData} dataTestSub={'mlOverviewJobStatsBar'} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton size="m" fill href={manageJobsLink}>
{i18n.translate('xpack.ml.overview.anomalyDetection.manageJobsButtonText', {
defaultMessage: 'Manage jobs',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiInMemoryTable<Group>
allowNeutralSort={false}
className="mlAnomalyDetectionTable"
columns={columns}
hasActions={true}
isExpandable={false}
isSelectable={false}
items={groupsList}
itemId={AnomalyDetectionListColumns.id}
onTableChange={onTableChange}
pagination={pagination}
sorting={sorting}
data-test-subj="mlOverviewTableAnomalyDetection"
/>
</>
<EuiInMemoryTable<Group>
allowNeutralSort={false}
className="mlAnomalyDetectionTable"
columns={columns}
hasActions={true}
isExpandable={false}
isSelectable={false}
items={groupsList}
itemId={AnomalyDetectionListColumns.id}
onTableChange={onTableChange}
pagination={pagination}
sorting={sorting}
data-test-subj="mlOverviewTableAnomalyDetection"
/>
);
};

View file

@ -76,40 +76,45 @@ export function getGroupsFromJobs(jobs: MlSummaryJobs): {
export function getStatsBarData(jobsList: any) {
const jobStats = {
total: {
label: i18n.translate('xpack.ml.overviewJobsList.statsBar.totalJobsLabel', {
defaultMessage: 'Total',
}),
value: 0,
show: true,
group: 0,
},
open: {
label: i18n.translate('xpack.ml.overviewJobsList.statsBar.openJobsLabel', {
defaultMessage: 'Open',
}),
value: 0,
show: true,
group: 0,
},
closed: {
label: i18n.translate('xpack.ml.overviewJobsList.statsBar.closedJobsLabel', {
defaultMessage: 'Closed',
}),
value: 0,
show: true,
group: 0,
},
failed: {
label: i18n.translate('xpack.ml.overviewJobsList.statsBar.failedJobsLabel', {
defaultMessage: 'Failed',
}),
value: 0,
show: false,
group: 0,
},
activeNodes: {
label: i18n.translate('xpack.ml.overviewJobsList.statsBar.activeMLNodesLabel', {
defaultMessage: 'Active ML nodes',
}),
value: 0,
show: true,
},
total: {
label: i18n.translate('xpack.ml.overviewJobsList.statsBar.totalJobsLabel', {
defaultMessage: 'Total jobs',
}),
value: 0,
show: true,
},
open: {
label: i18n.translate('xpack.ml.overviewJobsList.statsBar.openJobsLabel', {
defaultMessage: 'Open jobs',
}),
value: 0,
show: true,
},
closed: {
label: i18n.translate('xpack.ml.overviewJobsList.statsBar.closedJobsLabel', {
defaultMessage: 'Closed jobs',
}),
value: 0,
show: true,
},
failed: {
label: i18n.translate('xpack.ml.overviewJobsList.statsBar.failedJobsLabel', {
defaultMessage: 'Failed jobs',
}),
value: 0,
show: false,
group: 1,
},
activeDatafeeds: {
label: i18n.translate('xpack.ml.jobsList.statsBar.activeDatafeedsLabel', {
@ -117,6 +122,7 @@ export function getStatsBarData(jobsList: any) {
}),
value: 0,
show: true,
group: 1,
},
};
@ -158,5 +164,13 @@ export function getStatsBarData(jobsList: any) {
jobStats.activeNodes.value = Object.keys(mlNodes).length;
if (jobStats.total.value === 0) {
for (const [statKey, val] of Object.entries(jobStats)) {
if (statKey !== 'total') {
val.show = false;
}
}
}
return jobStats;
}

View file

@ -6,9 +6,15 @@
*/
import React, { FC, useState } from 'react';
import { EuiPanel, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiLink, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { mlTimefilterRefresh$, useTimefilter } from '@kbn/ml-date-picker';
import { useStorage } from '@kbn/ml-local-storage';
import { OverviewStatsBar } from '../components/collapsible_panel/collapsible_panel';
import { ML_PAGES } from '../../../common/constants/locator';
import { ML_OVERVIEW_PANELS, MlStorageKey, TMlStorageMapped } from '../../../common/types/storage';
import { CollapsiblePanel } from '../components/collapsible_panel';
import { usePermissionCheck } from '../capabilities/check_capabilities';
import { mlNodesAvailable } from '../ml_nodes_check';
import { OverviewContent } from './components/content';
@ -17,11 +23,18 @@ import { JobsAwaitingNodeWarning } from '../components/jobs_awaiting_node_warnin
import { SavedObjectsWarning } from '../components/saved_objects_warning';
import { UpgradeWarning } from '../components/upgrade';
import { HelpMenu } from '../components/help_menu';
import { useMlKibana } from '../contexts/kibana';
import { useMlKibana, useMlLink } from '../contexts/kibana';
import { NodesList } from '../memory_usage/nodes_overview';
import { MlPageHeader } from '../components/page_header';
import { PageTitle } from '../components/page_title';
import { useIsServerless } from '../contexts/kibana/use_is_serverless';
import { getMlNodesCount } from '../ml_nodes_check/check_ml_nodes';
export const overviewPanelDefaultState = Object.freeze({
nodes: true,
adJobs: true,
dfaJobs: true,
});
export const OverviewPage: FC = () => {
const serverless = useIsServerless();
@ -33,11 +46,20 @@ export const OverviewPage: FC = () => {
} = useMlKibana();
const helpLink = docLinks.links.ml.guide;
const viewNodesLink = useMlLink({
page: ML_PAGES.MEMORY_USAGE,
});
const timefilter = useTimefilter({ timeRangeSelector: true, autoRefreshSelector: true });
const [adLazyJobCount, setAdLazyJobCount] = useState(0);
const [dfaLazyJobCount, setDfaLazyJobCount] = useState(0);
const [panelsState, setPanelsState] = useStorage<
MlStorageKey,
TMlStorageMapped<typeof ML_OVERVIEW_PANELS>
>(ML_OVERVIEW_PANELS, overviewPanelDefaultState);
return (
<div>
<MlPageHeader>
@ -63,9 +85,36 @@ export const OverviewPage: FC = () => {
{canViewMlNodes && serverless === false ? (
<>
<EuiPanel hasShadow={false} hasBorder>
<CollapsiblePanel
isOpen={panelsState.nodes}
onToggle={(update) => {
setPanelsState({ ...panelsState, nodes: update });
}}
header={
<FormattedMessage id="xpack.ml.overview.nodesPanel.header" defaultMessage="Nodes" />
}
headerItems={[
<OverviewStatsBar
inputStats={[
{
label: i18n.translate('xpack.ml.overview.nodesPanel.totalNodesLabel', {
defaultMessage: 'Total',
}),
value: getMlNodesCount(),
'data-test-subj': 'mlTotalNodesCount',
},
]}
dataTestSub={'mlOverviewAnalyticsStatsBar'}
/>,
<EuiLink href={viewNodesLink}>
{i18n.translate('xpack.ml.overview.nodesPanel.viewNodeLink', {
defaultMessage: 'View nodes',
})}
</EuiLink>,
]}
>
<NodesList compactView />
</EuiPanel>
</CollapsiblePanel>
<EuiSpacer size="m" />
</>
) : null}

View file

@ -35,7 +35,6 @@ export const useRouteResolver = (
): {
context: RouteResolverContext;
results: ResolverResults;
component?: React.Component;
} => {
const requiredCapabilitiesRef = useRef(requiredCapabilities);
const customResolversRef = useRef(customResolvers);

View file

@ -22521,7 +22521,6 @@
"xpack.ml.notifications.newNotificationsMessage": "Il y a eu {newNotificationsCount, plural, one {# notification} many {# notifications} other {# notifications}} depuis {sinceDate}. Actualisez la page pour afficher les mises à jour.",
"xpack.ml.notificationsIndicator.errorsAndWarningLabel": "Il y a eu {count, plural, one {# notification} many {# notifications} other {# notifications}} avec un niveau d'avertissement ou d'erreur depuis {lastCheckedAt}",
"xpack.ml.notificationsIndicator.unreadLabel": "Vous avez des notifications non lues depuis {lastCheckedAt}",
"xpack.ml.overview.analyticsList.emptyPromptHelperText": "Avant de créer une tâche d'analyse du cadre de données, utilisez des {transforms} pour créer une {sourcedata}.",
"xpack.ml.previewAlert.otherValuesLabel": "et {count, plural, one {# autre} many {# autres} other {# autres}}",
"xpack.ml.previewAlert.previewMessage": "{alertsCount, plural, one {# anomalie a été trouvée} many {# anomalies ont été trouvées} other {# anomalies ont été trouvées}} au cours des dernières {interval}.",
"xpack.ml.privilege.pleaseContactAdministratorTooltip": "{message} Veuillez contacter votre administrateur.",
@ -22898,7 +22897,6 @@
"xpack.ml.cases.anomalySwimLane.embeddableAddedEvent": "couloir d'anomalie ajouté",
"xpack.ml.changePointDetection.pageHeader": "Modifier la détection du point",
"xpack.ml.chrome.help.appName": "Machine Learning",
"xpack.ml.common.learnMoreQuestion": "Envie d'en savoir plus ?",
"xpack.ml.common.readDocumentationLink": "Lire la documentation",
"xpack.ml.components.colorRangeLegend.blueColorRangeLabel": "Bleu",
"xpack.ml.components.colorRangeLegend.greenRedColorRangeLabel": "Vert Rouge",
@ -24525,7 +24523,6 @@
"xpack.ml.overview.anomalyDetection.noAnomaliesFoundMessage": "Aucune anomalie n'a été trouvée",
"xpack.ml.overview.anomalyDetection.noResultsFoundMessage": "Résultat introuvable",
"xpack.ml.overview.anomalyDetection.overallScore": "Score général",
"xpack.ml.overview.anomalyDetection.panelTitle": "Détection des anomalies",
"xpack.ml.overview.anomalyDetection.resultActions.openInJobManagementText": "Afficher les tâches",
"xpack.ml.overview.anomalyDetection.resultActions.openJobsInAnomalyExplorerText": "Afficher dans lExplorateur d'anomalies",
"xpack.ml.overview.anomalyDetection.tableActionLabel": "Actions",
@ -24539,8 +24536,6 @@
"xpack.ml.overview.anomalyDetection.tableTypicalTooltip": "Valeurs typiques dans les résultats d'enregistrement des anomalies.",
"xpack.ml.overview.anomalyDetection.viewJobsActionName": "Afficher les tâches",
"xpack.ml.overview.anomalyDetection.viewResultsActionName": "Afficher dans lExplorateur d'anomalies",
"xpack.ml.overview.gettingStartedSectionSourceData": "ensemble de données source centré sur les entités",
"xpack.ml.overview.gettingStartedSectionTransforms": "transformations",
"xpack.ml.overview.notificationsLabel": "Notifications",
"xpack.ml.overview.overviewLabel": "Aperçu",
"xpack.ml.overview.statsBar.failedAnalyticsLabel": "Échoué",

View file

@ -22512,7 +22512,6 @@
"xpack.ml.notifications.newNotificationsMessage": "{sinceDate}以降に{newNotificationsCount, plural, other {#件の通知があります}}。更新を表示するには、ページを更新してください。",
"xpack.ml.notificationsIndicator.errorsAndWarningLabel": "{lastCheckedAt}以降にエラーまたは警告レベルの{count, plural, other {#件の通知があります}}",
"xpack.ml.notificationsIndicator.unreadLabel": "{lastCheckedAt}以降に未読の通知があります",
"xpack.ml.overview.analyticsList.emptyPromptHelperText": "データフレーム分析ジョブを構築する前に、{transforms}を使用して{sourcedata}を作成してください。",
"xpack.ml.previewAlert.otherValuesLabel": "および{count, plural, other {#個のその他}}",
"xpack.ml.previewAlert.previewMessage": "過去{interval}に{alertsCount, plural, other {#個の異常}}が見つかりました。",
"xpack.ml.privilege.pleaseContactAdministratorTooltip": "{message} 管理者にお問い合わせください。",
@ -22884,7 +22883,6 @@
"xpack.ml.cases.anomalySwimLane.embeddableAddedEvent": "追加された異常スイムレーン",
"xpack.ml.changePointDetection.pageHeader": "変化点検出",
"xpack.ml.chrome.help.appName": "機械学習",
"xpack.ml.common.learnMoreQuestion": "詳細について",
"xpack.ml.common.readDocumentationLink": "ドキュメンテーションを表示",
"xpack.ml.components.colorRangeLegend.blueColorRangeLabel": "青",
"xpack.ml.components.colorRangeLegend.greenRedColorRangeLabel": "緑 - 赤",
@ -24511,7 +24509,6 @@
"xpack.ml.overview.anomalyDetection.noAnomaliesFoundMessage": "異常値が見つかりませんでした",
"xpack.ml.overview.anomalyDetection.noResultsFoundMessage": "結果が見つかりませんでした",
"xpack.ml.overview.anomalyDetection.overallScore": "全体スコア",
"xpack.ml.overview.anomalyDetection.panelTitle": "異常検知",
"xpack.ml.overview.anomalyDetection.resultActions.openInJobManagementText": "ジョブを表示",
"xpack.ml.overview.anomalyDetection.resultActions.openJobsInAnomalyExplorerText": "異常エクスプローラーで表示",
"xpack.ml.overview.anomalyDetection.tableActionLabel": "アクション",
@ -24525,8 +24522,6 @@
"xpack.ml.overview.anomalyDetection.tableTypicalTooltip": "異常レコード結果の標準的な値。",
"xpack.ml.overview.anomalyDetection.viewJobsActionName": "ジョブを表示",
"xpack.ml.overview.anomalyDetection.viewResultsActionName": "異常エクスプローラーで表示",
"xpack.ml.overview.gettingStartedSectionSourceData": "エンティティ中心のソースデータセット",
"xpack.ml.overview.gettingStartedSectionTransforms": "トランスフォーム",
"xpack.ml.overview.notificationsLabel": "通知",
"xpack.ml.overview.overviewLabel": "概要",
"xpack.ml.overview.statsBar.failedAnalyticsLabel": "失敗",

View file

@ -22511,7 +22511,6 @@
"xpack.ml.notifications.newNotificationsMessage": "自 {sinceDate}以来有 {newNotificationsCount, plural, other {# 个通知}}。刷新页面以查看更新。",
"xpack.ml.notificationsIndicator.errorsAndWarningLabel": "自 {lastCheckedAt}以来有 {count, plural, other {# 个通知}}包含错误或警告级别",
"xpack.ml.notificationsIndicator.unreadLabel": "自 {lastCheckedAt}以来您有未计通知",
"xpack.ml.overview.analyticsList.emptyPromptHelperText": "构建数据帧分析作业之前,请使用 {transforms} 构造一个 {sourcedata}。",
"xpack.ml.previewAlert.otherValuesLabel": "和{count, plural, other {另外 # 个}}",
"xpack.ml.previewAlert.previewMessage": "在过去 {interval}找到 {alertsCount, plural, other {# 个异常}}。",
"xpack.ml.privilege.pleaseContactAdministratorTooltip": "{message}请联系您的管理员。",
@ -22883,7 +22882,6 @@
"xpack.ml.cases.anomalySwimLane.embeddableAddedEvent": "已添加异常泳道",
"xpack.ml.changePointDetection.pageHeader": "更改点检测",
"xpack.ml.chrome.help.appName": "Machine Learning",
"xpack.ml.common.learnMoreQuestion": "希望了解详情?",
"xpack.ml.common.readDocumentationLink": "阅读文档",
"xpack.ml.components.colorRangeLegend.blueColorRangeLabel": "蓝",
"xpack.ml.components.colorRangeLegend.greenRedColorRangeLabel": "绿 - 红",
@ -24510,7 +24508,6 @@
"xpack.ml.overview.anomalyDetection.noAnomaliesFoundMessage": "找不到异常",
"xpack.ml.overview.anomalyDetection.noResultsFoundMessage": "找不到结果",
"xpack.ml.overview.anomalyDetection.overallScore": "总分",
"xpack.ml.overview.anomalyDetection.panelTitle": "异常检测",
"xpack.ml.overview.anomalyDetection.resultActions.openInJobManagementText": "查看作业",
"xpack.ml.overview.anomalyDetection.resultActions.openJobsInAnomalyExplorerText": "在 Anomaly Explorer 中查看",
"xpack.ml.overview.anomalyDetection.tableActionLabel": "操作",
@ -24524,8 +24521,6 @@
"xpack.ml.overview.anomalyDetection.tableTypicalTooltip": "异常记录结果中的典型值。",
"xpack.ml.overview.anomalyDetection.viewJobsActionName": "查看作业",
"xpack.ml.overview.anomalyDetection.viewResultsActionName": "在 Anomaly Explorer 中查看",
"xpack.ml.overview.gettingStartedSectionSourceData": "实体中心型源数据集",
"xpack.ml.overview.gettingStartedSectionTransforms": "转换",
"xpack.ml.overview.notificationsLabel": "通知",
"xpack.ml.overview.overviewLabel": "概览",
"xpack.ml.overview.statsBar.failedAnalyticsLabel": "失败",