mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* show overview tab with main tabs * wip:add overview dir with initial components * convert and move AnalyticsTable and types to MlInMemoryTable components dir * create analytics table for overview * add stats bar to analytics panel * wip: adds anomaly detection table * adds actions column to anomaly detection table * add stats bar to anomaly detection panel * add max anomaly score to table * update score display. * add refresh button to panels * create scss files for styles * update functional nav tests * fix functional test failure * fix anomalyDetection for when there are no jobs * add translations and update jobList types * add maxAnomalyScore endpoint * fix types and update tab testSub * fix transforms use of inMemoryTable
This commit is contained in:
parent
13e30097cb
commit
90bf444703
52 changed files with 1502 additions and 123 deletions
|
@ -39,6 +39,30 @@ export interface MlJob {
|
|||
state: string;
|
||||
}
|
||||
|
||||
export interface MlSummaryJob {
|
||||
id: string;
|
||||
description: string;
|
||||
groups: string[];
|
||||
processed_record_count: number;
|
||||
memory_status?: string;
|
||||
jobState: string;
|
||||
hasDatafeed: boolean;
|
||||
datafeedId?: string;
|
||||
datafeedIndices: any[];
|
||||
datafeedState?: string;
|
||||
latestTimestampMs: number;
|
||||
earliestTimestampMs?: number;
|
||||
latestResultsTimestampMs: number;
|
||||
isSingleMetricViewerJob: boolean;
|
||||
nodeName?: string;
|
||||
deleting?: boolean;
|
||||
fullJob?: any;
|
||||
auditMessage?: any;
|
||||
latestTimestampSortValue?: number;
|
||||
}
|
||||
|
||||
export type MlSummaryJobs = MlSummaryJob[];
|
||||
|
||||
export function isMlJob(arg: any): arg is MlJob {
|
||||
return typeof arg.job_id === 'string';
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import 'plugins/ml/components/transition/transition';
|
|||
import 'plugins/ml/components/modal/modal';
|
||||
import 'plugins/ml/access_denied';
|
||||
import 'plugins/ml/jobs';
|
||||
import 'plugins/ml/overview';
|
||||
import 'plugins/ml/services/calendar_service';
|
||||
import 'plugins/ml/components/messagebar';
|
||||
import 'plugins/ml/data_frame';
|
||||
|
@ -43,5 +44,5 @@ if (typeof uiRoutes.enable === 'function') {
|
|||
|
||||
uiRoutes
|
||||
.otherwise({
|
||||
redirectTo: '/jobs'
|
||||
redirectTo: '/overview'
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { ProgressBar, MlInMemoryTable } from './ml_in_memory_table';
|
||||
export * from './types';
|
|
@ -71,9 +71,9 @@ const getInitialSorting = (columns: any, sorting: any) => {
|
|||
};
|
||||
};
|
||||
|
||||
import { MlInMemoryTable } from '../../../../../../common/types/eui/in_memory_table';
|
||||
import { MlInMemoryTableBasic } from './types';
|
||||
|
||||
export class AnalyticsTable extends MlInMemoryTable {
|
||||
export class MlInMemoryTable extends MlInMemoryTableBasic {
|
||||
static getDerivedStateFromProps(nextProps: any, prevState: any) {
|
||||
const derivedState = {
|
||||
...prevState.prevProps,
|
|
@ -30,6 +30,7 @@ export interface FieldDataColumnType {
|
|||
truncateText?: boolean;
|
||||
render?: RenderFunc;
|
||||
footer?: string | ReactElement | FooterFunc;
|
||||
textOnly?: boolean;
|
||||
}
|
||||
|
||||
export interface ComputedColumnType {
|
||||
|
@ -191,6 +192,6 @@ interface ComponentWithConstructor<T> extends Component {
|
|||
new (): Component<T>;
|
||||
}
|
||||
|
||||
export const MlInMemoryTable = (EuiInMemoryTable as any) as ComponentWithConstructor<
|
||||
export const MlInMemoryTableBasic = (EuiInMemoryTable as any) as ComponentWithConstructor<
|
||||
EuiInMemoryTableProps
|
||||
>;
|
|
@ -23,13 +23,13 @@ interface Props {
|
|||
|
||||
function getTabs(disableLinks: boolean): Tab[] {
|
||||
return [
|
||||
// {
|
||||
// id: 'overview',
|
||||
// name: i18n.translate('xpack.ml.navMenu.overviewTabLinkText', {
|
||||
// defaultMessage: 'Overview',
|
||||
// }),
|
||||
// disabled: disableLinks,
|
||||
// },
|
||||
{
|
||||
id: 'overview',
|
||||
name: i18n.translate('xpack.ml.navMenu.overviewTabLinkText', {
|
||||
defaultMessage: 'Overview',
|
||||
}),
|
||||
disabled: disableLinks,
|
||||
},
|
||||
{
|
||||
id: 'anomaly_detection',
|
||||
name: i18n.translate('xpack.ml.navMenu.anomalyDetectionTabLinkText', {
|
||||
|
@ -66,7 +66,7 @@ interface TabData {
|
|||
}
|
||||
|
||||
const TAB_DATA: Record<TabId, TabData> = {
|
||||
// overview: { testSubject: 'mlTabOverview', pathId: 'overview' },
|
||||
overview: { testSubject: 'mlMainTab overview', pathId: 'overview' },
|
||||
anomaly_detection: { testSubject: 'mlMainTab anomalyDetection', pathId: 'jobs' },
|
||||
data_frames: { testSubject: 'mlMainTab dataFrames' },
|
||||
data_frame_analytics: { testSubject: 'mlMainTab dataFrameAnalytics' },
|
||||
|
|
|
@ -18,7 +18,7 @@ export type TabId = string;
|
|||
type TabSupport = Record<TabId, string | null>;
|
||||
|
||||
const tabSupport: TabSupport = {
|
||||
// overview: null,
|
||||
overview: null,
|
||||
jobs: 'anomaly_detection',
|
||||
settings: 'anomaly_detection',
|
||||
data_frames: null,
|
||||
|
|
|
@ -11,8 +11,6 @@ import ReactDOM from 'react-dom';
|
|||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml');
|
||||
|
||||
import 'ui/directives/kbn_href';
|
||||
|
||||
import { NavigationMenu } from './navigation_menu';
|
||||
|
||||
module.directive('mlNavMenu', function () {
|
||||
|
|
|
@ -19,7 +19,7 @@ interface Props {
|
|||
|
||||
export function getTabs(tabId: TabId, disableLinks: boolean): Tab[] {
|
||||
const TAB_MAP: Partial<Record<TabId, Tab[]>> = {
|
||||
// overview: [],
|
||||
overview: [],
|
||||
datavisualizer: [],
|
||||
data_frames: [],
|
||||
data_frame_analytics: [],
|
||||
|
@ -59,7 +59,7 @@ export function getTabs(tabId: TabId, disableLinks: boolean): Tab[] {
|
|||
}
|
||||
|
||||
enum TAB_TEST_SUBJECT {
|
||||
// overview = 'mlOverview',
|
||||
overview = 'mlOverview',
|
||||
jobs = 'mlSubTab jobManagement',
|
||||
explorer = 'mlSubTab anomalyExplorer',
|
||||
timeseriesexplorer = 'mlSubTab singleMetricViewer',
|
||||
|
|
|
@ -4,4 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { StatsBar, TransformStatsBarStats } from './stats_bar';
|
||||
export {
|
||||
StatsBar,
|
||||
TransformStatsBarStats,
|
||||
AnalyticStatsBarStats,
|
||||
JobStatsBarStats,
|
||||
} from './stats_bar';
|
||||
|
|
|
@ -7,24 +7,29 @@
|
|||
import React, { FC } from 'react';
|
||||
import { Stat, StatsBarStat } from './stat';
|
||||
|
||||
interface JobStatsBarStats {
|
||||
activeNodes: StatsBarStat;
|
||||
interface Stats {
|
||||
total: StatsBarStat;
|
||||
open: StatsBarStat;
|
||||
failed: StatsBarStat;
|
||||
}
|
||||
export interface JobStatsBarStats extends Stats {
|
||||
activeNodes: StatsBarStat;
|
||||
open: StatsBarStat;
|
||||
closed: StatsBarStat;
|
||||
activeDatafeeds: StatsBarStat;
|
||||
}
|
||||
|
||||
export interface TransformStatsBarStats {
|
||||
total: StatsBarStat;
|
||||
export interface TransformStatsBarStats extends Stats {
|
||||
batch: StatsBarStat;
|
||||
continuous: StatsBarStat;
|
||||
failed: StatsBarStat;
|
||||
started: StatsBarStat;
|
||||
}
|
||||
|
||||
type StatsBarStats = TransformStatsBarStats | JobStatsBarStats;
|
||||
export interface AnalyticStatsBarStats extends Stats {
|
||||
started: StatsBarStat;
|
||||
stopped: StatsBarStat;
|
||||
}
|
||||
|
||||
type StatsBarStats = TransformStatsBarStats | JobStatsBarStats | AnalyticStatsBarStats;
|
||||
type StatsKey = keyof StatsBarStats;
|
||||
|
||||
interface StatsBarProps {
|
||||
|
|
|
@ -30,10 +30,10 @@ import {
|
|||
|
||||
import {
|
||||
ColumnType,
|
||||
MlInMemoryTable,
|
||||
MlInMemoryTableBasic,
|
||||
SortingPropType,
|
||||
SORT_DIRECTION,
|
||||
} from '../../../../../../common/types/eui/in_memory_table';
|
||||
} from '../../../../../components/ml_in_memory_table';
|
||||
|
||||
import { KBN_FIELD_TYPES } from '../../../../../../common/constants/field_types';
|
||||
import { Dictionary } from '../../../../../../common/types/common';
|
||||
|
@ -405,7 +405,7 @@ export const SourceIndexPreview: React.SFC<Props> = React.memo(({ cellClick, que
|
|||
<EuiProgress size="xs" color="accent" max={1} value={0} />
|
||||
)}
|
||||
{clearTable === false && columns.length > 0 && sorting !== false && (
|
||||
<MlInMemoryTable
|
||||
<MlInMemoryTableBasic
|
||||
allowNeutralSort={false}
|
||||
compressed
|
||||
items={tableItems}
|
||||
|
|
|
@ -23,9 +23,9 @@ import {
|
|||
|
||||
import {
|
||||
ColumnType,
|
||||
MlInMemoryTable,
|
||||
MlInMemoryTableBasic,
|
||||
SORT_DIRECTION,
|
||||
} from '../../../../../../common/types/eui/in_memory_table';
|
||||
} from '../../../../../components/ml_in_memory_table';
|
||||
import { dictionaryToArray } from '../../../../../../common/types/common';
|
||||
import { ES_FIELD_TYPES } from '../../../../../../common/constants/field_types';
|
||||
import { formatHumanReadableDateTimeSeconds } from '../../../../../util/date_utils';
|
||||
|
@ -284,7 +284,7 @@ export const PivotPreview: SFC<PivotPreviewProps> = React.memo(({ aggs, groupBy,
|
|||
<EuiProgress size="xs" color="accent" max={1} value={0} />
|
||||
)}
|
||||
{dataFramePreviewData.length > 0 && clearTable === false && columns.length > 0 && (
|
||||
<MlInMemoryTable
|
||||
<MlInMemoryTableBasic
|
||||
allowNeutralSort={false}
|
||||
compressed
|
||||
items={dataFramePreviewData}
|
||||
|
|
|
@ -22,7 +22,7 @@ import {
|
|||
ComputedColumnType,
|
||||
ExpanderColumnType,
|
||||
FieldDataColumnType,
|
||||
} from '../../../../../../common/types/eui/in_memory_table';
|
||||
} from '../../../../../components/ml_in_memory_table';
|
||||
|
||||
import {
|
||||
getTransformProgress,
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
SortDirection,
|
||||
SORT_DIRECTION,
|
||||
FieldDataColumnType,
|
||||
} from '../../../../../../common/types/eui/in_memory_table';
|
||||
} from '../../../../../components/ml_in_memory_table';
|
||||
|
||||
import { ml } from '../../../../../services/ml_api_service';
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ import {
|
|||
OnTableChangeArg,
|
||||
SortDirection,
|
||||
SORT_DIRECTION,
|
||||
} from '../../../../../../common/types/eui/in_memory_table';
|
||||
} from '../../../../../components/ml_in_memory_table';
|
||||
|
||||
import {
|
||||
DataFrameTransformId,
|
||||
|
|
|
@ -11,7 +11,7 @@ import React, { Fragment } from 'react';
|
|||
|
||||
import { EuiProgress } from '@elastic/eui';
|
||||
|
||||
import { MlInMemoryTable } from '../../../../../../common/types/eui/in_memory_table';
|
||||
import { MlInMemoryTableBasic } from '../../../../../components/ml_in_memory_table';
|
||||
|
||||
// The built in loading progress bar of EuiInMemoryTable causes a full DOM replacement
|
||||
// of the table and doesn't play well with auto-refreshing. That's why we're displaying
|
||||
|
@ -73,7 +73,7 @@ const getInitialSorting = (columns: any, sorting: any) => {
|
|||
};
|
||||
};
|
||||
|
||||
export class TransformTable extends MlInMemoryTable {
|
||||
export class TransformTable extends MlInMemoryTableBasic {
|
||||
static getDerivedStateFromProps(nextProps: any, prevState: any) {
|
||||
const derivedState = {
|
||||
...prevState.prevProps,
|
||||
|
|
|
@ -32,11 +32,11 @@ import euiThemeDark from '@elastic/eui/dist/eui_theme_dark.json';
|
|||
|
||||
import {
|
||||
ColumnType,
|
||||
MlInMemoryTable,
|
||||
MlInMemoryTableBasic,
|
||||
OnTableChangeArg,
|
||||
SortingPropType,
|
||||
SORT_DIRECTION,
|
||||
} from '../../../../../../common/types/eui/in_memory_table';
|
||||
} from '../../../../../components/ml_in_memory_table';
|
||||
|
||||
import { useUiChromeContext } from '../../../../../contexts/ui/use_ui_chrome_context';
|
||||
|
||||
|
@ -466,7 +466,7 @@ export const Exploration: FC<Props> = React.memo(({ jobId }) => {
|
|||
<EuiProgress size="xs" color="accent" max={1} value={0} />
|
||||
)}
|
||||
{clearTable === false && columns.length > 0 && sortField !== '' && (
|
||||
<MlInMemoryTable
|
||||
<MlInMemoryTableBasic
|
||||
allowNeutralSort={false}
|
||||
className="mlDataFrameAnalyticsExploration"
|
||||
columns={columns}
|
||||
|
|
|
@ -8,7 +8,7 @@ import React, { useEffect, useState } from 'react';
|
|||
|
||||
import { SearchResponse } from 'elasticsearch';
|
||||
|
||||
import { SortDirection, SORT_DIRECTION } from '../../../../../../common/types/eui/in_memory_table';
|
||||
import { SortDirection, SORT_DIRECTION } from '../../../../../components/ml_in_memory_table';
|
||||
|
||||
import { ml } from '../../../../../services/ml_api_service';
|
||||
import { getNestedProperty } from '../../../../../util/object_utils';
|
||||
|
|
|
@ -21,31 +21,33 @@ import { stopAnalytics } from '../../services/analytics_service';
|
|||
import { StartAction } from './action_start';
|
||||
import { DeleteAction } from './action_delete';
|
||||
|
||||
export const AnalyticsViewAction = {
|
||||
isPrimary: true,
|
||||
render: (item: DataFrameAnalyticsListRow) => {
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
disabled={!isOutlierAnalysis(item.config.analysis)}
|
||||
onClick={() => (window.location.href = getResultsUrl(item.id))}
|
||||
size="xs"
|
||||
color="text"
|
||||
iconType="visTable"
|
||||
aria-label={i18n.translate('xpack.ml.dataframe.analyticsList.viewAriaLabel', {
|
||||
defaultMessage: 'View',
|
||||
})}
|
||||
>
|
||||
{i18n.translate('xpack.ml.dataframe.analyticsList.viewActionName', {
|
||||
defaultMessage: 'View',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const getActions = () => {
|
||||
const canStartStopDataFrameAnalytics: boolean = checkPermission('canStartStopDataFrameAnalytics');
|
||||
|
||||
return [
|
||||
{
|
||||
isPrimary: true,
|
||||
render: (item: DataFrameAnalyticsListRow) => {
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
disabled={!isOutlierAnalysis(item.config.analysis)}
|
||||
onClick={() => (window.location.href = getResultsUrl(item.id))}
|
||||
size="xs"
|
||||
color="text"
|
||||
iconType="visTable"
|
||||
aria-label={i18n.translate('xpack.ml.dataframe.analyticsList.viewAriaLabel', {
|
||||
defaultMessage: 'View',
|
||||
})}
|
||||
>
|
||||
{i18n.translate('xpack.ml.dataframe.analyticsList.viewActionName', {
|
||||
defaultMessage: 'View',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
},
|
||||
},
|
||||
AnalyticsViewAction,
|
||||
{
|
||||
render: (item: DataFrameAnalyticsListRow) => {
|
||||
if (!isDataFrameAnalyticsRunning(item.stats)) {
|
||||
|
|
|
@ -15,12 +15,6 @@ import {
|
|||
EuiEmptyPrompt,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import {
|
||||
OnTableChangeArg,
|
||||
SortDirection,
|
||||
SORT_DIRECTION,
|
||||
} from '../../../../../../common/types/eui/in_memory_table';
|
||||
|
||||
import { DataFrameAnalyticsId, useRefreshAnalyticsList } from '../../../../common';
|
||||
import { checkPermission } from '../../../../../privilege/check_privilege';
|
||||
import { getTaskStateBadge } from './columns';
|
||||
|
@ -38,7 +32,13 @@ import { ActionDispatchers } from '../../hooks/use_create_analytics_form/actions
|
|||
import { getAnalyticsFactory } from '../../services/analytics_service';
|
||||
import { getColumns } from './columns';
|
||||
import { ExpandedRow } from './expanded_row';
|
||||
import { ProgressBar, AnalyticsTable } from './analytics_table';
|
||||
import {
|
||||
ProgressBar,
|
||||
MlInMemoryTable,
|
||||
OnTableChangeArg,
|
||||
SortDirection,
|
||||
SORT_DIRECTION,
|
||||
} from '../../../../../components/ml_in_memory_table';
|
||||
|
||||
function getItemIdToExpandedRowMap(
|
||||
itemIds: DataFrameAnalyticsId[],
|
||||
|
@ -310,7 +310,7 @@ export const DataFrameAnalyticsList: FC<Props> = ({
|
|||
return (
|
||||
<Fragment>
|
||||
<ProgressBar isLoading={isLoading} />
|
||||
<AnalyticsTable
|
||||
<MlInMemoryTable
|
||||
allowNeutralSort={false}
|
||||
className="mlAnalyticsTable"
|
||||
columns={columns}
|
||||
|
|
|
@ -58,6 +58,57 @@ export const getTaskStateBadge = (
|
|||
);
|
||||
};
|
||||
|
||||
export const progressColumn = {
|
||||
name: i18n.translate('xpack.ml.dataframe.analyticsList.progress', {
|
||||
defaultMessage: 'Progress',
|
||||
}),
|
||||
sortable: (item: DataFrameAnalyticsListRow) => getDataFrameAnalyticsProgress(item.stats),
|
||||
truncateText: true,
|
||||
render(item: DataFrameAnalyticsListRow) {
|
||||
const progress = getDataFrameAnalyticsProgress(item.stats);
|
||||
|
||||
if (progress === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For now all analytics jobs are batch jobs.
|
||||
const isBatchTransform = true;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs">
|
||||
{isBatchTransform && (
|
||||
<Fragment>
|
||||
<EuiFlexItem style={{ width: '40px' }} grow={false}>
|
||||
<EuiProgress value={progress} max={100} color="primary" size="m">
|
||||
{progress}%
|
||||
</EuiProgress>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem style={{ width: '35px' }} grow={false}>
|
||||
<EuiText size="xs">{`${progress}%`}</EuiText>
|
||||
</EuiFlexItem>
|
||||
</Fragment>
|
||||
)}
|
||||
{!isBatchTransform && (
|
||||
<Fragment>
|
||||
<EuiFlexItem style={{ width: '40px' }} grow={false}>
|
||||
{item.stats.state === DATA_FRAME_TASK_STATE.STARTED && (
|
||||
<EuiProgress color="primary" size="m" />
|
||||
)}
|
||||
{item.stats.state === DATA_FRAME_TASK_STATE.STOPPED && (
|
||||
<EuiProgress value={0} max={100} color="primary" size="m" />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem style={{ width: '35px' }} grow={false}>
|
||||
|
||||
</EuiFlexItem>
|
||||
</Fragment>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
},
|
||||
width: '100px',
|
||||
};
|
||||
|
||||
export const getColumns = (
|
||||
expandedRowItemIds: DataFrameAnalyticsId[],
|
||||
setExpandedRowItemIds: React.Dispatch<React.SetStateAction<DataFrameAnalyticsId[]>>,
|
||||
|
@ -166,56 +217,7 @@ export const getColumns = (
|
|||
width: '100px',
|
||||
},
|
||||
*/
|
||||
{
|
||||
name: i18n.translate('xpack.ml.dataframe.analyticsList.progress', {
|
||||
defaultMessage: 'Progress',
|
||||
}),
|
||||
sortable: (item: DataFrameAnalyticsListRow) => getDataFrameAnalyticsProgress(item.stats),
|
||||
truncateText: true,
|
||||
render(item: DataFrameAnalyticsListRow) {
|
||||
const progress = getDataFrameAnalyticsProgress(item.stats);
|
||||
|
||||
if (progress === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For now all analytics jobs are batch jobs.
|
||||
const isBatchTransform = true;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs">
|
||||
{isBatchTransform && (
|
||||
<Fragment>
|
||||
<EuiFlexItem style={{ width: '40px' }} grow={false}>
|
||||
<EuiProgress value={progress} max={100} color="primary" size="m">
|
||||
{progress}%
|
||||
</EuiProgress>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem style={{ width: '35px' }} grow={false}>
|
||||
<EuiText size="xs">{`${progress}%`}</EuiText>
|
||||
</EuiFlexItem>
|
||||
</Fragment>
|
||||
)}
|
||||
{!isBatchTransform && (
|
||||
<Fragment>
|
||||
<EuiFlexItem style={{ width: '40px' }} grow={false}>
|
||||
{item.stats.state === DATA_FRAME_TASK_STATE.STARTED && (
|
||||
<EuiProgress color="primary" size="m" />
|
||||
)}
|
||||
{item.stats.state === DATA_FRAME_TASK_STATE.STOPPED && (
|
||||
<EuiProgress value={0} max={100} color="primary" size="m" />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem style={{ width: '35px' }} grow={false}>
|
||||
|
||||
</EuiFlexItem>
|
||||
</Fragment>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
},
|
||||
width: '100px',
|
||||
},
|
||||
progressColumn,
|
||||
];
|
||||
|
||||
if (isManagementTable === true) {
|
||||
|
|
|
@ -94,6 +94,7 @@ export interface DataFrameAnalyticsListRow {
|
|||
export enum DataFrameAnalyticsListColumn {
|
||||
configDestIndex = 'config.dest.index',
|
||||
configSourceIndex = 'config.source.index',
|
||||
configCreateTime = 'config.create_time',
|
||||
// Description attribute is not supported yet by API
|
||||
// description = 'config.description',
|
||||
id = 'id',
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
@import 'datavisualizer/index';
|
||||
@import 'explorer/index'; // SASSTODO: This file needs to be rewritten
|
||||
@import 'jobs/index'; // SASSTODO: This collection of sass files has multiple problems
|
||||
@import 'overview/index';
|
||||
@import 'settings/index';
|
||||
@import 'timeseriesexplorer/index';
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ import chrome from 'ui/chrome';
|
|||
import { mlJobService } from '../../../../services/job_service';
|
||||
import { injectI18n } from '@kbn/i18n/react';
|
||||
|
||||
function getLink(location, jobs) {
|
||||
export function getLink(location, jobs) {
|
||||
const resultsPageUrl = mlJobService.createResultsUrlForJobs(jobs, location);
|
||||
return `${chrome.getBasePath()}/app/${resultsPageUrl}`;
|
||||
}
|
||||
|
|
1
x-pack/legacy/plugins/ml/public/overview/_index.scss
Normal file
1
x-pack/legacy/plugins/ml/public/overview/_index.scss
Normal file
|
@ -0,0 +1 @@
|
|||
@import './components/index';
|
23
x-pack/legacy/plugins/ml/public/overview/breadcrumbs.ts
Normal file
23
x-pack/legacy/plugins/ml/public/overview/breadcrumbs.ts
Normal 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 { i18n } from '@kbn/i18n';
|
||||
// @ts-ignore
|
||||
import { ML_BREADCRUMB } from '../breadcrumbs';
|
||||
|
||||
export function getOverviewBreadcrumbs() {
|
||||
// Whilst top level nav menu with tabs remains,
|
||||
// use root ML breadcrumb.
|
||||
return [
|
||||
ML_BREADCRUMB,
|
||||
{
|
||||
text: i18n.translate('xpack.ml.overviewBreadcrumbs.overviewLabel', {
|
||||
defaultMessage: 'Overview',
|
||||
}),
|
||||
href: '',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
.mlOverviewPanel {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.mlOverviewPanel__buttons {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.mlOverviewPanel__statsBar {
|
||||
margin-top: 0;
|
||||
margin-right: 0
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* 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 React, { FC, Fragment, useState, useEffect } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiEmptyPrompt,
|
||||
EuiLoadingSpinner,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
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';
|
||||
|
||||
export const AnalyticsPanel: FC = () => {
|
||||
const [analytics, setAnalytics] = useState<DataFrameAnalyticsListRow[]>([]);
|
||||
const [errorMessage, setErrorMessage] = useState<any>(undefined);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
const getAnalytics = getAnalyticsFactory(setAnalytics, setErrorMessage, setIsInitialized, false);
|
||||
|
||||
useEffect(() => {
|
||||
getAnalytics(true);
|
||||
}, []);
|
||||
|
||||
const onRefresh = () => {
|
||||
getAnalytics(true);
|
||||
};
|
||||
|
||||
const errorDisplay = (
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.ml.overview.analyticsList.errorPromptTitle', {
|
||||
defaultMessage: 'An error occurred getting the data frame analytics list.',
|
||||
})}
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
>
|
||||
<pre>
|
||||
{errorMessage && errorMessage.message !== undefined
|
||||
? errorMessage.message
|
||||
: JSON.stringify(errorMessage)}
|
||||
</pre>
|
||||
</EuiCallOut>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPanel className="mlOverviewPanel">
|
||||
{typeof errorMessage !== 'undefined' && errorDisplay}
|
||||
{isInitialized === false && <EuiLoadingSpinner />}
|
||||
{isInitialized === true && analytics.length === 0 && (
|
||||
<EuiEmptyPrompt
|
||||
iconType="createAdvancedJob"
|
||||
title={
|
||||
<h2>
|
||||
{i18n.translate('xpack.ml.overview.analyticsList.createFirstJobMessage', {
|
||||
defaultMessage: 'Create your first analytics job.',
|
||||
})}
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<Fragment>
|
||||
<p>
|
||||
{i18n.translate('xpack.ml.overview.analyticsList.emptyPromptText', {
|
||||
defaultMessage: `Data frame analytics enable you to perform different analyses of your data and annotate it with the results. The analytics job stores the annotated data, as well as a copy of the source data, in a new index.`,
|
||||
})}
|
||||
</p>
|
||||
</Fragment>
|
||||
}
|
||||
actions={
|
||||
<EuiButton href="#/data_frame_analytics?" color="primary" fill>
|
||||
{i18n.translate('xpack.ml.overview.analyticsList.createJobButtonText', {
|
||||
defaultMessage: 'Create job.',
|
||||
})}
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isInitialized === true && analytics.length > 0 && (
|
||||
<Fragment>
|
||||
<AnalyticsTable items={analytics} />
|
||||
<EuiSpacer size="m" />
|
||||
<div className="mlOverviewPanel__buttons">
|
||||
<EuiButtonEmpty size="s" onClick={onRefresh}>
|
||||
{i18n.translate('xpack.ml.overview.analyticsList.refreshJobsButtonText', {
|
||||
defaultMessage: 'Refresh',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
<EuiButton size="s" fill href="#/data_frame_analytics?">
|
||||
{i18n.translate('xpack.ml.overview.analyticsList.manageJobsButtonText', {
|
||||
defaultMessage: 'Manage jobs',
|
||||
})}
|
||||
</EuiButton>
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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 React, { FC } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { StatsBar, AnalyticStatsBarStats } from '../../../components/stats_bar';
|
||||
import {
|
||||
DataFrameAnalyticsListRow,
|
||||
DATA_FRAME_TASK_STATE,
|
||||
} from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common';
|
||||
|
||||
function getAnalyticsStats(analyticsList: any[]) {
|
||||
const analyticsStats = {
|
||||
total: {
|
||||
label: i18n.translate('xpack.ml.overview.statsBar.totalAnalyticsLabel', {
|
||||
defaultMessage: 'Total analytics jobs',
|
||||
}),
|
||||
value: 0,
|
||||
show: true,
|
||||
},
|
||||
started: {
|
||||
label: i18n.translate('xpack.ml.overview.statsBar.startedAnalyticsLabel', {
|
||||
defaultMessage: 'Started',
|
||||
}),
|
||||
value: 0,
|
||||
show: true,
|
||||
},
|
||||
stopped: {
|
||||
label: i18n.translate('xpack.ml.overview.statsBar.stoppedAnalyticsLabel', {
|
||||
defaultMessage: 'Stopped',
|
||||
}),
|
||||
value: 0,
|
||||
show: true,
|
||||
},
|
||||
failed: {
|
||||
label: i18n.translate('xpack.ml.overview.statsBar.failedAnalyticsLabel', {
|
||||
defaultMessage: 'Failed',
|
||||
}),
|
||||
value: 0,
|
||||
show: false,
|
||||
},
|
||||
};
|
||||
|
||||
if (analyticsList === undefined) {
|
||||
return analyticsStats;
|
||||
}
|
||||
|
||||
let failedJobs = 0;
|
||||
let startedJobs = 0;
|
||||
let stoppedJobs = 0;
|
||||
|
||||
analyticsList.forEach(job => {
|
||||
if (job.stats.state === DATA_FRAME_TASK_STATE.FAILED) {
|
||||
failedJobs++;
|
||||
} else if (
|
||||
job.stats.state === DATA_FRAME_TASK_STATE.STARTED ||
|
||||
job.stats.state === DATA_FRAME_TASK_STATE.ANALYZING ||
|
||||
job.stats.state === DATA_FRAME_TASK_STATE.REINDEXING
|
||||
) {
|
||||
startedJobs++;
|
||||
} else if (job.stats.state === DATA_FRAME_TASK_STATE.STOPPED) {
|
||||
stoppedJobs++;
|
||||
}
|
||||
});
|
||||
|
||||
analyticsStats.total.value = analyticsList.length;
|
||||
analyticsStats.started.value = startedJobs;
|
||||
analyticsStats.stopped.value = stoppedJobs;
|
||||
|
||||
if (failedJobs !== 0) {
|
||||
analyticsStats.failed.value = failedJobs;
|
||||
analyticsStats.failed.show = true;
|
||||
} else {
|
||||
analyticsStats.failed.show = false;
|
||||
}
|
||||
|
||||
return analyticsStats;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
analyticsList: DataFrameAnalyticsListRow[];
|
||||
}
|
||||
|
||||
export const AnalyticsStatsBar: FC<Props> = ({ analyticsList }) => {
|
||||
const analyticsStats: AnalyticStatsBarStats = getAnalyticsStats(analyticsList);
|
||||
|
||||
return <StatsBar stats={analyticsStats} dataTestSub={'mlOverviewAnalyticsStatsBar'} />;
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { AnalyticsPanel } from './analytics_panel';
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* 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 React, { FC, Fragment, useState } from 'react';
|
||||
import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
MlInMemoryTable,
|
||||
SortDirection,
|
||||
SORT_DIRECTION,
|
||||
OnTableChangeArg,
|
||||
ColumnType,
|
||||
} from '../../../components/ml_in_memory_table';
|
||||
import { getAnalysisType } from '../../../data_frame_analytics/common/analytics';
|
||||
import {
|
||||
DataFrameAnalyticsListColumn,
|
||||
DataFrameAnalyticsListRow,
|
||||
} from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common';
|
||||
import {
|
||||
getTaskStateBadge,
|
||||
progressColumn,
|
||||
} from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/columns';
|
||||
import { AnalyticsViewAction } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/actions';
|
||||
import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils';
|
||||
import { AnalyticsStatsBar } from './analytics_stats_bar';
|
||||
|
||||
interface Props {
|
||||
items: any[];
|
||||
}
|
||||
export const AnalyticsTable: FC<Props> = ({ items }) => {
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
const [sortField, setSortField] = useState<string>(DataFrameAnalyticsListColumn.id);
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection>(SORT_DIRECTION.ASC);
|
||||
|
||||
// id, type, status, progress, created time, view icon
|
||||
const columns: ColumnType[] = [
|
||||
{
|
||||
field: DataFrameAnalyticsListColumn.id,
|
||||
name: i18n.translate('xpack.ml.overview.analyticsList.id', { defaultMessage: 'ID' }),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.ml.overview.analyticsList.type', { defaultMessage: 'Type' }),
|
||||
sortable: (item: DataFrameAnalyticsListRow) => getAnalysisType(item.config.analysis),
|
||||
truncateText: true,
|
||||
render(item: DataFrameAnalyticsListRow) {
|
||||
return <EuiBadge color="hollow">{getAnalysisType(item.config.analysis)}</EuiBadge>;
|
||||
},
|
||||
width: '150px',
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.ml.overview.analyticsList.status', { defaultMessage: 'Status' }),
|
||||
sortable: (item: DataFrameAnalyticsListRow) => item.stats.state,
|
||||
truncateText: true,
|
||||
render(item: DataFrameAnalyticsListRow) {
|
||||
return getTaskStateBadge(item.stats.state, item.stats.reason);
|
||||
},
|
||||
width: '100px',
|
||||
},
|
||||
progressColumn,
|
||||
{
|
||||
field: DataFrameAnalyticsListColumn.configCreateTime,
|
||||
name: i18n.translate('xpack.ml.overview.analyticsList.reatedTimeColumnName', {
|
||||
defaultMessage: 'Creation time',
|
||||
}),
|
||||
dataType: 'date',
|
||||
render: (time: number) => formatHumanReadableDateTimeSeconds(time),
|
||||
textOnly: true,
|
||||
sortable: true,
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.ml.overview.analyticsList.tableActionLabel', {
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
actions: [AnalyticsViewAction],
|
||||
width: '100px',
|
||||
},
|
||||
];
|
||||
|
||||
const onTableChange = ({
|
||||
page = { index: 0, size: 10 },
|
||||
sort = { field: DataFrameAnalyticsListColumn.id, direction: SORT_DIRECTION.ASC },
|
||||
}: OnTableChangeArg) => {
|
||||
const { index, size } = page;
|
||||
setPageIndex(index);
|
||||
setPageSize(size);
|
||||
|
||||
const { field, direction } = sort;
|
||||
setSortField(field);
|
||||
setSortDirection(direction);
|
||||
};
|
||||
|
||||
const pagination = {
|
||||
initialPageIndex: pageIndex,
|
||||
initialPageSize: pageSize,
|
||||
totalItemCount: items.length,
|
||||
pageSizeOptions: [10, 20, 50],
|
||||
hidePerPageOptions: false,
|
||||
};
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: sortField,
|
||||
direction: sortDirection,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<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} className="mlOverviewPanel__statsBar">
|
||||
<AnalyticsStatsBar analyticsList={items} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
<MlInMemoryTable
|
||||
allowNeutralSort={false}
|
||||
className="mlAnalyticsTable"
|
||||
columns={columns}
|
||||
hasActions={false}
|
||||
isExpandable={false}
|
||||
isSelectable={false}
|
||||
items={items}
|
||||
itemId={DataFrameAnalyticsListColumn.id}
|
||||
onTableChange={onTableChange}
|
||||
pagination={pagination}
|
||||
sorting={sorting}
|
||||
data-test-subj="mlOverviewTableAnalytics"
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 React, { FC } from 'react';
|
||||
import { EuiToolTip, EuiButtonEmpty } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
// @ts-ignore no module file
|
||||
import { getLink } from '../../../jobs/jobs_list/components/job_actions/results';
|
||||
import { MlSummaryJobs } from '../../../../common/types/jobs';
|
||||
|
||||
interface Props {
|
||||
jobsList: MlSummaryJobs;
|
||||
}
|
||||
|
||||
export const ExplorerLink: FC<Props> = ({ jobsList }) => {
|
||||
const openJobsInAnomalyExplorerText = i18n.translate(
|
||||
'xpack.ml.overview.anomalyDetection.resultActions.openJobsInAnomalyExplorerText',
|
||||
{
|
||||
defaultMessage: 'Open {jobsCount, plural, one {{jobId}} other {# jobs}} in Anomaly Explorer',
|
||||
values: { jobsCount: jobsList.length, jobId: jobsList[0] && jobsList[0].id },
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiToolTip position="bottom" content={openJobsInAnomalyExplorerText}>
|
||||
<EuiButtonEmpty
|
||||
color="text"
|
||||
size="xs"
|
||||
href={getLink('explorer', jobsList)}
|
||||
iconType="tableOfContents"
|
||||
aria-label={openJobsInAnomalyExplorerText}
|
||||
className="results-button"
|
||||
data-test-subj={`openOverviewJobsInAnomalyExplorer`}
|
||||
>
|
||||
{i18n.translate('xpack.ml.overview.anomalyDetection.exploreActionName', {
|
||||
defaultMessage: 'Explore',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
* 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 React, { FC, Fragment, useState, useEffect } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiEmptyPrompt,
|
||||
EuiLoadingSpinner,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import moment from 'moment';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { AnomalyDetectionTable } from './table';
|
||||
import { ml } from '../../../services/ml_api_service';
|
||||
import { getGroupsFromJobs, getStatsBarData, getJobsWithTimerange } from './utils';
|
||||
import { Dictionary } from '../../../../common/types/common';
|
||||
import { MlSummaryJobs, MlSummaryJob } from '../../../../common/types/jobs';
|
||||
|
||||
export type GroupsDictionary = Dictionary<Group>;
|
||||
|
||||
export interface Group {
|
||||
id: string;
|
||||
jobIds: string[];
|
||||
docs_processed: number;
|
||||
earliest_timestamp: number;
|
||||
latest_timestamp: number;
|
||||
max_anomaly_score: number | null;
|
||||
}
|
||||
|
||||
type MaxScoresByGroup = Dictionary<{
|
||||
maxScore: number;
|
||||
index?: number;
|
||||
}>;
|
||||
|
||||
const createJobLink = '#/jobs/new_job/step/index_or_search';
|
||||
|
||||
function getDefaultAnomalyScores(groups: Group[]): MaxScoresByGroup {
|
||||
const anomalyScores: MaxScoresByGroup = {};
|
||||
groups.forEach(group => {
|
||||
anomalyScores[group.id] = { maxScore: 0 };
|
||||
});
|
||||
|
||||
return anomalyScores;
|
||||
}
|
||||
|
||||
export const AnomalyDetectionPanel: FC = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [groups, setGroups] = useState<GroupsDictionary>({});
|
||||
const [groupsCount, setGroupsCount] = useState<number>(0);
|
||||
const [jobsList, setJobsList] = useState<MlSummaryJobs>([]);
|
||||
const [statsBarData, setStatsBarData] = useState<any>(undefined);
|
||||
const [errorMessage, setErrorMessage] = useState<any>(undefined);
|
||||
|
||||
const loadJobs = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const jobsResult: MlSummaryJobs = await ml.jobs.jobsSummary([]);
|
||||
const jobsSummaryList = jobsResult.map((job: MlSummaryJob) => {
|
||||
job.latestTimestampSortValue = job.latestTimestampMs || 0;
|
||||
return job;
|
||||
});
|
||||
const { groups: jobsGroups, count } = getGroupsFromJobs(jobsSummaryList);
|
||||
const jobsWithTimerange = getJobsWithTimerange(jobsSummaryList);
|
||||
const stats = getStatsBarData(jobsSummaryList);
|
||||
setIsLoading(false);
|
||||
setErrorMessage(undefined);
|
||||
setStatsBarData(stats);
|
||||
setGroupsCount(count);
|
||||
setGroups(jobsGroups);
|
||||
setJobsList(jobsWithTimerange);
|
||||
loadMaxAnomalyScores(jobsGroups);
|
||||
} catch (e) {
|
||||
setErrorMessage(e.message !== undefined ? e.message : JSON.stringify(e));
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadMaxAnomalyScores = async (groupsObject: GroupsDictionary) => {
|
||||
const groupsList: Group[] = Object.values(groupsObject);
|
||||
const scores = getDefaultAnomalyScores(groupsList);
|
||||
|
||||
try {
|
||||
const promises = groupsList
|
||||
.filter(group => group.jobIds.length > 0)
|
||||
.map((group, i) => {
|
||||
scores[group.id].index = i;
|
||||
const latestTimestamp = group.latest_timestamp;
|
||||
const startMoment = moment(latestTimestamp);
|
||||
const twentyFourHoursAgo = startMoment.subtract(24, 'hours').valueOf();
|
||||
return ml.results.getMaxAnomalyScore(group.jobIds, twentyFourHoursAgo, latestTimestamp);
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
const tempGroups = { ...groupsObject };
|
||||
// Check results for each group's promise index and update state
|
||||
Object.keys(scores).forEach(groupId => {
|
||||
const resultsIndex = scores[groupId] && scores[groupId].index;
|
||||
scores[groupId] = resultsIndex !== undefined && results[resultsIndex];
|
||||
tempGroups[groupId].max_anomaly_score = resultsIndex !== undefined && results[resultsIndex];
|
||||
});
|
||||
|
||||
setGroups(tempGroups);
|
||||
} catch (e) {
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate(
|
||||
'xpack.ml.overview.anomalyDetection.errorWithFetchingAnomalyScoreNotificationErrorMessage',
|
||||
{
|
||||
defaultMessage: 'An error occurred fetching anomaly scores: {error}',
|
||||
values: { error: e.message !== undefined ? e.message : JSON.stringify(e) },
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadJobs();
|
||||
}, []);
|
||||
|
||||
const onRefresh = () => {
|
||||
loadJobs();
|
||||
};
|
||||
|
||||
const errorDisplay = (
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.ml.overview.anomalyDetection.errorPromptTitle', {
|
||||
defaultMessage: 'An error occurred getting the anomaly detection jobs list.',
|
||||
})}
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
>
|
||||
<pre>{errorMessage}</pre>
|
||||
</EuiCallOut>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPanel className="mlOverviewPanel">
|
||||
{typeof errorMessage !== 'undefined' && errorDisplay}
|
||||
{isLoading && <EuiLoadingSpinner />}
|
||||
{isLoading === false && typeof errorMessage === 'undefined' && groupsCount === 0 && (
|
||||
<EuiEmptyPrompt
|
||||
iconType="createSingleMetricJob"
|
||||
title={
|
||||
<h2>
|
||||
{i18n.translate('xpack.ml.overview.anomalyDetection.createFirstJobMessage', {
|
||||
defaultMessage: 'Create your first anomaly detection job.',
|
||||
})}
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<Fragment>
|
||||
<p>
|
||||
{i18n.translate('xpack.ml.overview.anomalyDetection.emptyPromptText', {
|
||||
defaultMessage: `Machine learning makes it easy to detect anomalies in time series data stored in Elasticsearch. Track one metric from a single machine or hundreds of metrics across thousands of machines. Start automatically spotting the anomalies hiding in your data and resolve issues faster.`,
|
||||
})}
|
||||
</p>
|
||||
</Fragment>
|
||||
}
|
||||
actions={
|
||||
<EuiButton color="primary" href={createJobLink} fill>
|
||||
{i18n.translate('xpack.ml.overview.anomalyDetection.createJobButtonText', {
|
||||
defaultMessage: 'Create job.',
|
||||
})}
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isLoading === false && typeof errorMessage === 'undefined' && groupsCount > 0 && (
|
||||
<Fragment>
|
||||
<AnomalyDetectionTable items={groups} jobsList={jobsList} statsBarData={statsBarData} />
|
||||
<EuiSpacer size="m" />
|
||||
<div className="mlOverviewPanel__buttons">
|
||||
<EuiButtonEmpty size="s" onClick={onRefresh}>
|
||||
{i18n.translate('xpack.ml.overview.anomalyDetection.refreshJobsButtonText', {
|
||||
defaultMessage: 'Refresh',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
<EuiButton size="s" fill href="#/jobs?">
|
||||
{i18n.translate('xpack.ml.overview.anomalyDetection.manageJobsButtonText', {
|
||||
defaultMessage: 'Manage jobs',
|
||||
})}
|
||||
</EuiButton>
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { AnomalyDetectionPanel } from './anomaly_detection_panel';
|
|
@ -0,0 +1,194 @@
|
|||
/*
|
||||
* 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 React, { FC, Fragment, useState } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHealth,
|
||||
EuiLoadingSpinner,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
MlInMemoryTable,
|
||||
SortDirection,
|
||||
SORT_DIRECTION,
|
||||
OnTableChangeArg,
|
||||
ColumnType,
|
||||
} from '../../../components/ml_in_memory_table';
|
||||
import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils';
|
||||
import { ExplorerLink } from './actions';
|
||||
import { getJobsFromGroup } from './utils';
|
||||
import { GroupsDictionary, Group } from './anomaly_detection_panel';
|
||||
import { MlSummaryJobs } from '../../../../common/types/jobs';
|
||||
import { StatsBar, JobStatsBarStats } from '../../../components/stats_bar';
|
||||
// @ts-ignore
|
||||
import { JobSelectorBadge } from '../../../components/job_selector/job_selector_badge';
|
||||
// @ts-ignore
|
||||
import { toLocaleString } from '../../../util/string_utils';
|
||||
import { getSeverityColor } from '../../../../common/util/anomaly_utils';
|
||||
|
||||
// Used to pass on attribute names to table columns
|
||||
export enum AnomalyDetectionListColumns {
|
||||
id = 'id',
|
||||
maxAnomalyScore = 'max_anomaly_score',
|
||||
jobIds = 'jobIds',
|
||||
latestTimestamp = 'latest_timestamp',
|
||||
docsProcessed = 'docs_processed',
|
||||
}
|
||||
|
||||
interface Props {
|
||||
items: GroupsDictionary;
|
||||
statsBarData: JobStatsBarStats;
|
||||
jobsList: MlSummaryJobs;
|
||||
}
|
||||
|
||||
export const AnomalyDetectionTable: FC<Props> = ({ items, jobsList, statsBarData }) => {
|
||||
const groupsList = Object.values(items);
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
const [sortField, setSortField] = useState<string>(AnomalyDetectionListColumns.id);
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection>(SORT_DIRECTION.ASC);
|
||||
|
||||
// columns: group, max anomaly, jobs in group, latest timestamp, docs processed, action to explorer
|
||||
const columns: ColumnType[] = [
|
||||
{
|
||||
field: AnomalyDetectionListColumns.id,
|
||||
name: i18n.translate('xpack.ml.overview.anomalyDetection.tableId', {
|
||||
defaultMessage: 'Group ID',
|
||||
}),
|
||||
render: (id: Group['id']) => <JobSelectorBadge id={id} isGroup={id !== 'ungrouped'} />,
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
field: AnomalyDetectionListColumns.maxAnomalyScore,
|
||||
name: i18n.translate('xpack.ml.overview.anomalyDetection.tableMaxScore', {
|
||||
defaultMessage: 'Max anomaly score',
|
||||
}),
|
||||
sortable: true,
|
||||
render: (score: Group['max_anomaly_score']) => {
|
||||
if (score === null) {
|
||||
return <EuiLoadingSpinner />;
|
||||
} else {
|
||||
const color: string = getSeverityColor(score);
|
||||
return (
|
||||
// @ts-ignore
|
||||
<EuiHealth color={color} compressed="true">
|
||||
{score >= 1 ? Math.floor(score) : '< 1'}
|
||||
</EuiHealth>
|
||||
);
|
||||
}
|
||||
},
|
||||
truncateText: true,
|
||||
width: '150px',
|
||||
},
|
||||
{
|
||||
field: AnomalyDetectionListColumns.jobIds,
|
||||
name: i18n.translate('xpack.ml.overview.anomalyDetection.tableNumJobs', {
|
||||
defaultMessage: 'Jobs in group',
|
||||
}),
|
||||
render: (jobIds: Group['jobIds']) => jobIds.length,
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
width: '100px',
|
||||
},
|
||||
{
|
||||
field: AnomalyDetectionListColumns.latestTimestamp,
|
||||
name: i18n.translate('xpack.ml.overview.anomalyDetection.tableLatestTimestamp', {
|
||||
defaultMessage: 'Latest timestamp',
|
||||
}),
|
||||
dataType: 'date',
|
||||
render: (time: number) => formatHumanReadableDateTimeSeconds(time),
|
||||
textOnly: true,
|
||||
sortable: true,
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
field: AnomalyDetectionListColumns.docsProcessed,
|
||||
name: i18n.translate('xpack.ml.overview.anomalyDetection.tableDocsProcessed', {
|
||||
defaultMessage: 'Docs processed',
|
||||
}),
|
||||
render: (docs: number) => toLocaleString(docs),
|
||||
textOnly: true,
|
||||
sortable: true,
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.ml.overview.anomalyDetection.tableActionLabel', {
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
render: (group: Group) => <ExplorerLink jobsList={getJobsFromGroup(group, jobsList)} />,
|
||||
width: '100px',
|
||||
},
|
||||
];
|
||||
|
||||
const onTableChange = ({
|
||||
page = { index: 0, size: 10 },
|
||||
sort = { field: AnomalyDetectionListColumns.id, direction: SORT_DIRECTION.ASC },
|
||||
}: OnTableChangeArg) => {
|
||||
const { index, size } = page;
|
||||
setPageIndex(index);
|
||||
setPageSize(size);
|
||||
|
||||
const { field, direction } = sort;
|
||||
setSortField(field);
|
||||
setSortDirection(direction);
|
||||
};
|
||||
|
||||
const pagination = {
|
||||
initialPageIndex: pageIndex,
|
||||
initialPageSize: pageSize,
|
||||
totalItemCount: groupsList.length,
|
||||
pageSizeOptions: [10, 20, 50],
|
||||
hidePerPageOptions: false,
|
||||
};
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: sortField,
|
||||
direction: sortDirection,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="m">
|
||||
<h3>
|
||||
{i18n.translate('xpack.ml.overview.anomalyDetection.panelTitle', {
|
||||
defaultMessage: 'Anomaly Detection',
|
||||
})}
|
||||
</h3>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} className="mlOverviewPanel__statsBar">
|
||||
<StatsBar stats={statsBarData} dataTestSub={'mlOverviewJobStatsBar'} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
<MlInMemoryTable
|
||||
allowNeutralSort={false}
|
||||
className="mlAnomalyDetectionTable"
|
||||
columns={columns}
|
||||
hasActions={false}
|
||||
isExpandable={false}
|
||||
isSelectable={false}
|
||||
items={groupsList}
|
||||
itemId={AnomalyDetectionListColumns.id}
|
||||
onTableChange={onTableChange}
|
||||
pagination={pagination}
|
||||
sorting={sorting}
|
||||
data-test-subj="mlOverviewTableAnomalyDetection"
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
* 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';
|
||||
import { JOB_STATE, DATAFEED_STATE } from '../../../../common/constants/states';
|
||||
import { Group, GroupsDictionary } from './anomaly_detection_panel';
|
||||
import { MlSummaryJobs, MlSummaryJob } from '../../../../common/types/jobs';
|
||||
|
||||
export function getGroupsFromJobs(
|
||||
jobs: MlSummaryJobs
|
||||
): { groups: GroupsDictionary; count: number } {
|
||||
const groups: any = {
|
||||
ungrouped: {
|
||||
id: 'ungrouped',
|
||||
jobIds: [],
|
||||
docs_processed: 0,
|
||||
latest_timestamp: 0,
|
||||
max_anomaly_score: null,
|
||||
},
|
||||
};
|
||||
|
||||
jobs.forEach((job: MlSummaryJob) => {
|
||||
// Organize job by group
|
||||
if (job.groups.length > 0) {
|
||||
job.groups.forEach((g: any) => {
|
||||
if (groups[g] === undefined) {
|
||||
groups[g] = {
|
||||
id: g,
|
||||
jobIds: [job.id],
|
||||
docs_processed: job.processed_record_count,
|
||||
latest_timestamp: job.latestTimestampMs,
|
||||
max_anomaly_score: null,
|
||||
};
|
||||
} else {
|
||||
groups[g].jobIds.push(job.id);
|
||||
groups[g].docs_processed += job.processed_record_count;
|
||||
// if incoming job latest timestamp is greater than the last saved one, replace it
|
||||
if (groups[g].latest_timestamp === undefined) {
|
||||
groups[g].latest_timestamp = job.latestTimestampMs;
|
||||
} else if (job.latestTimestampMs > groups[g].latest_timestamp) {
|
||||
groups[g].latest_timestamp = job.latestTimestampMs;
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
groups.ungrouped.jobIds.push(job.id);
|
||||
groups.ungrouped.docs_processed += job.processed_record_count;
|
||||
// if incoming job latest timestamp is greater than the last saved one, replace it
|
||||
if (job.latestTimestampMs > groups.ungrouped.latest_timestamp) {
|
||||
groups.ungrouped.latest_timestamp = job.latestTimestampMs;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (groups.ungrouped.jobIds.length === 0) {
|
||||
delete groups.ungrouped;
|
||||
}
|
||||
|
||||
const count = Object.values(groups).length;
|
||||
|
||||
return { groups, count };
|
||||
}
|
||||
|
||||
export function getStatsBarData(jobsList: any) {
|
||||
const jobStats = {
|
||||
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,
|
||||
},
|
||||
activeDatafeeds: {
|
||||
label: i18n.translate('xpack.ml.jobsList.statsBar.activeDatafeedsLabel', {
|
||||
defaultMessage: 'Active datafeeds',
|
||||
}),
|
||||
value: 0,
|
||||
show: true,
|
||||
},
|
||||
};
|
||||
|
||||
if (jobsList === undefined) {
|
||||
return jobStats;
|
||||
}
|
||||
|
||||
// object to keep track of nodes being used by jobs
|
||||
const mlNodes: any = {};
|
||||
let failedJobs = 0;
|
||||
|
||||
jobsList.forEach((job: MlSummaryJob) => {
|
||||
if (job.jobState === JOB_STATE.OPENED) {
|
||||
jobStats.open.value++;
|
||||
} else if (job.jobState === JOB_STATE.CLOSED) {
|
||||
jobStats.closed.value++;
|
||||
} else if (job.jobState === JOB_STATE.FAILED) {
|
||||
failedJobs++;
|
||||
}
|
||||
|
||||
if (job.hasDatafeed && job.datafeedState === DATAFEED_STATE.STARTED) {
|
||||
jobStats.activeDatafeeds.value++;
|
||||
}
|
||||
|
||||
if (job.nodeName !== undefined) {
|
||||
mlNodes[job.nodeName] = {};
|
||||
}
|
||||
});
|
||||
|
||||
jobStats.total.value = jobsList.length;
|
||||
|
||||
// Only show failed jobs if it is non-zero
|
||||
if (failedJobs) {
|
||||
jobStats.failed.value = failedJobs;
|
||||
jobStats.failed.show = true;
|
||||
} else {
|
||||
jobStats.failed.show = false;
|
||||
}
|
||||
|
||||
jobStats.activeNodes.value = Object.keys(mlNodes).length;
|
||||
|
||||
return jobStats;
|
||||
}
|
||||
|
||||
export function getJobsFromGroup(group: Group, jobs: any) {
|
||||
return group.jobIds.map(jobId => jobs[jobId]).filter(id => id !== undefined);
|
||||
}
|
||||
|
||||
export function getJobsWithTimerange(jobsList: any) {
|
||||
const jobs: any = {};
|
||||
jobsList.forEach((job: any) => {
|
||||
if (jobs[job.id] === undefined) {
|
||||
// create the job in the object with the times you need
|
||||
if (job.earliestTimestampMs !== undefined) {
|
||||
const { earliestTimestampMs, latestResultsTimestampMs } = job;
|
||||
jobs[job.id] = {
|
||||
id: job.id,
|
||||
earliestTimestampMs,
|
||||
latestResultsTimestampMs,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return jobs;
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 React, { FC } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { AnomalyDetectionPanel } from './anomaly_detection_panel';
|
||||
import { AnalyticsPanel } from './analytics_panel/';
|
||||
|
||||
// Fetch jobs and determine what to show
|
||||
export const OverviewContent: FC = () => (
|
||||
<EuiFlexItem grow={3}>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<AnomalyDetectionPanel />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AnalyticsPanel />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 React, { FC } from 'react';
|
||||
import { EuiFlexItem, EuiLink, EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { metadata } from 'ui/metadata';
|
||||
|
||||
const createJobLink = '#/jobs/new_job/step/index_or_search';
|
||||
// metadata.branch corresponds to the version used in documentation links.
|
||||
const docsLink = `https://www.elastic.co/guide/en/kibana/${metadata.branch}/xpack-ml.html`;
|
||||
const feedbackLink = 'https://www.elastic.co/community/';
|
||||
|
||||
export const OverviewSideBar: FC = () => (
|
||||
<EuiFlexItem grow={1}>
|
||||
<EuiText className="mlOverview__sidebar">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.overview.gettingStartedSectionTitle"
|
||||
defaultMessage="Getting Started"
|
||||
/>
|
||||
</h2>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.overview.gettingStartedSectionText"
|
||||
defaultMessage="Welcome to Machine Learning. Get started by reviewing our {docs} or {createJob}.
|
||||
For information about upcoming features and tutorials be sure to check out our solutions page."
|
||||
values={{
|
||||
docs: (
|
||||
<EuiLink href={docsLink} target="blank">
|
||||
<FormattedMessage
|
||||
id="xpack.ml.overview.gettingStartedSectionDocs"
|
||||
defaultMessage="documentation"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
createJob: (
|
||||
<EuiLink href={createJobLink} target="blank">
|
||||
<FormattedMessage
|
||||
id="xpack.ml.overview.gettingStartedSectionCreateJob"
|
||||
defaultMessage="creating a new job"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<h2>
|
||||
<FormattedMessage id="xpack.ml.overview.feedbackSectionTitle" defaultMessage="Feedback" />
|
||||
</h2>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.overview.feedbackSectionText"
|
||||
defaultMessage="If you have input or suggestions regarding your experience with Machine Learning please feel free to submit {feedbackLink}."
|
||||
values={{
|
||||
feedbackLink: (
|
||||
<EuiLink href={feedbackLink} target="blank">
|
||||
<FormattedMessage
|
||||
id="xpack.ml.overview.feedbackSectionLink"
|
||||
defaultMessage="feedback online"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
);
|
38
x-pack/legacy/plugins/ml/public/overview/directive.tsx
Normal file
38
x-pack/legacy/plugins/ml/public/overview/directive.tsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 ReactDOM from 'react-dom';
|
||||
import React from 'react';
|
||||
import { I18nContext } from 'ui/i18n';
|
||||
import { timefilter } from 'ui/timefilter';
|
||||
// @ts-ignore
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml', ['react']);
|
||||
|
||||
import { OverviewPage } from './overview_page';
|
||||
|
||||
module.directive('mlOverview', function() {
|
||||
return {
|
||||
scope: {},
|
||||
restrict: 'E',
|
||||
link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => {
|
||||
timefilter.disableTimeRangeSelector();
|
||||
timefilter.disableAutoRefreshSelector();
|
||||
|
||||
ReactDOM.render(
|
||||
<I18nContext>
|
||||
<OverviewPage />
|
||||
</I18nContext>,
|
||||
element[0]
|
||||
);
|
||||
|
||||
element.on('$destroy', () => {
|
||||
ReactDOM.unmountComponentAtNode(element[0]);
|
||||
scope.$destroy();
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
8
x-pack/legacy/plugins/ml/public/overview/index.ts
Normal file
8
x-pack/legacy/plugins/ml/public/overview/index.ts
Normal 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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import './route';
|
||||
import './directive';
|
27
x-pack/legacy/plugins/ml/public/overview/overview_page.tsx
Normal file
27
x-pack/legacy/plugins/ml/public/overview/overview_page.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 React, { Fragment, FC } from 'react';
|
||||
import { EuiFlexGroup, EuiPage, EuiPageBody } from '@elastic/eui';
|
||||
import { NavigationMenu } from '../components/navigation_menu/navigation_menu';
|
||||
import { OverviewSideBar } from './components/sidebar';
|
||||
import { OverviewContent } from './components/content';
|
||||
|
||||
export const OverviewPage: FC = () => {
|
||||
return (
|
||||
<Fragment>
|
||||
<NavigationMenu tabId="overview" />
|
||||
<EuiPage data-test-subj="mlPageOverview">
|
||||
<EuiPageBody>
|
||||
<EuiFlexGroup>
|
||||
<OverviewSideBar />
|
||||
<OverviewContent />
|
||||
</EuiFlexGroup>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
23
x-pack/legacy/plugins/ml/public/overview/route.ts
Normal file
23
x-pack/legacy/plugins/ml/public/overview/route.ts
Normal 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 uiRoutes from 'ui/routes';
|
||||
// @ts-ignore no declaration module
|
||||
import { checkFullLicense } from '../license/check_license';
|
||||
import { checkGetJobsPrivilege } from '../privilege/check_privilege';
|
||||
import { getOverviewBreadcrumbs } from './breadcrumbs';
|
||||
import './directive';
|
||||
|
||||
const template = `<ml-overview />`;
|
||||
|
||||
uiRoutes.when('/overview/?', {
|
||||
template,
|
||||
k7Breadcrumbs: getOverviewBreadcrumbs,
|
||||
resolve: {
|
||||
CheckLicense: checkFullLicense,
|
||||
privileges: checkGetJobsPrivilege,
|
||||
},
|
||||
});
|
|
@ -11,6 +11,7 @@ export interface ExistingJobsAndGroups {
|
|||
|
||||
declare interface JobService {
|
||||
currentJob: any;
|
||||
createResultsUrlForJobs: () => string;
|
||||
tempJobCloningObjects: {
|
||||
job: any;
|
||||
skipTimeRangeStep: boolean;
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
DataFrameTransformEndpointRequest,
|
||||
DataFrameTransformEndpointResult,
|
||||
} from '../../data_frame/pages/transform_management/components/transform_list/common';
|
||||
import { MlSummaryJobs } from '../../../common/types/jobs';
|
||||
|
||||
// TODO This is not a complete representation of all methods of `ml.*`.
|
||||
// It just satisfies needs for other parts of the code area which use
|
||||
|
@ -87,8 +88,12 @@ declare interface Ml {
|
|||
getVisualizerFieldStats(obj: object): Promise<any>;
|
||||
getVisualizerOverallStats(obj: object): Promise<any>;
|
||||
|
||||
results: {
|
||||
getMaxAnomalyScore: (jobIds: string[], earliestMs: number, latestMs: number) => Promise<any>; // THIS ONE IS RIGHT
|
||||
};
|
||||
|
||||
jobs: {
|
||||
jobsSummary(jobIds: string[]): Promise<object>;
|
||||
jobsSummary(jobIds: string[]): Promise<MlSummaryJobs>;
|
||||
jobs(jobIds: string[]): Promise<object>;
|
||||
groups(): Promise<object>;
|
||||
updateGroups(updatedJobs: string[]): Promise<object>;
|
||||
|
|
|
@ -45,6 +45,23 @@ export const results = {
|
|||
});
|
||||
},
|
||||
|
||||
getMaxAnomalyScore(
|
||||
jobIds,
|
||||
earliestMs,
|
||||
latestMs
|
||||
) {
|
||||
|
||||
return http({
|
||||
url: `${basePath}/results/max_anomaly_score`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
jobIds,
|
||||
earliestMs,
|
||||
latestMs
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getCategoryDefinition(jobId, categoryId) {
|
||||
return http({
|
||||
url: `${basePath}/results/category_definition`,
|
||||
|
|
|
@ -20,7 +20,13 @@ declare interface MlResultsService {
|
|||
getScheduledEventsByBucket: () => Promise<any>;
|
||||
getTopInfluencers: () => Promise<any>;
|
||||
getTopInfluencerValues: () => Promise<any>;
|
||||
getOverallBucketScores: () => Promise<any>;
|
||||
getOverallBucketScores: (
|
||||
jobIds: any,
|
||||
topN: any,
|
||||
earliestMs: any,
|
||||
latestMs: any,
|
||||
interval?: any
|
||||
) => Promise<any>;
|
||||
getInfluencerValueMaxScoreByTime: () => Promise<any>;
|
||||
getRecordInfluencers: () => Promise<any>;
|
||||
getRecordsForInfluencer: () => Promise<any>;
|
||||
|
|
|
@ -200,6 +200,73 @@ export function resultsServiceProvider(callWithRequest) {
|
|||
|
||||
}
|
||||
|
||||
// Returns the maximum anomaly_score for result_type:bucket over jobIds for the interval passed in
|
||||
async function getMaxAnomalyScore(jobIds = [], earliestMs, latestMs) {
|
||||
// Build the criteria to use in the bool filter part of the request.
|
||||
// Adds criteria for the time range plus any specified job IDs.
|
||||
const boolCriteria = [
|
||||
{
|
||||
range: {
|
||||
timestamp: {
|
||||
gte: earliestMs,
|
||||
lte: latestMs,
|
||||
format: 'epoch_millis'
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
if (jobIds.length > 0) {
|
||||
let jobIdFilterStr = '';
|
||||
jobIds.forEach((jobId, i) => {
|
||||
if (i > 0) {
|
||||
jobIdFilterStr += ' OR ';
|
||||
}
|
||||
jobIdFilterStr += 'job_id:';
|
||||
jobIdFilterStr += jobId;
|
||||
});
|
||||
boolCriteria.push({
|
||||
query_string: {
|
||||
analyze_wildcard: false,
|
||||
query: jobIdFilterStr
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const query = {
|
||||
size: 0,
|
||||
index: ML_RESULTS_INDEX_PATTERN,
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
filter: [{
|
||||
query_string: {
|
||||
query: 'result_type:bucket',
|
||||
analyze_wildcard: false
|
||||
}
|
||||
}, {
|
||||
bool: {
|
||||
must: boolCriteria
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
aggs: {
|
||||
max_score: {
|
||||
max: {
|
||||
field: 'anomaly_score'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resp = await callWithRequest('search', query);
|
||||
const maxScore = _.get(resp, ['aggregations', 'max_score', 'value'], null);
|
||||
|
||||
return maxScore;
|
||||
}
|
||||
|
||||
// Obtains the latest bucket result timestamp by job ID.
|
||||
// Returns data over all jobs unless an optional list of job IDs of interest is supplied.
|
||||
// Returned response consists of latest bucket timestamps (ms since Jan 1 1970) against job ID
|
||||
|
@ -342,6 +409,7 @@ export function resultsServiceProvider(callWithRequest) {
|
|||
getCategoryDefinition,
|
||||
getCategoryExamples,
|
||||
getLatestBucketTimestampByJob,
|
||||
getMaxAnomalyScore
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -58,6 +58,19 @@ function getCategoryExamples(callWithRequest, payload) {
|
|||
maxExamples);
|
||||
}
|
||||
|
||||
|
||||
function getMaxAnomalyScore(callWithRequest, payload) {
|
||||
const rs = resultsServiceProvider(callWithRequest);
|
||||
const {
|
||||
jobIds,
|
||||
earliestMs,
|
||||
latestMs } = payload;
|
||||
return rs.getMaxAnomalyScore(
|
||||
jobIds,
|
||||
earliestMs,
|
||||
latestMs);
|
||||
}
|
||||
|
||||
export function resultsServiceRoutes({ commonRouteConfig, elasticsearchPlugin, route }) {
|
||||
|
||||
route({
|
||||
|
@ -86,6 +99,19 @@ export function resultsServiceRoutes({ commonRouteConfig, elasticsearchPlugin, r
|
|||
}
|
||||
});
|
||||
|
||||
route({
|
||||
method: 'POST',
|
||||
path: '/api/ml/results/max_anomaly_score',
|
||||
handler(request) {
|
||||
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
|
||||
return getMaxAnomalyScore(callWithRequest, request.payload)
|
||||
.catch(resp => wrapError(resp));
|
||||
},
|
||||
config: {
|
||||
...commonRouteConfig
|
||||
}
|
||||
});
|
||||
|
||||
route({
|
||||
method: 'POST',
|
||||
path: '/api/ml/results/category_examples',
|
||||
|
|
|
@ -51,7 +51,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
|
|||
basePath: '/s/custom_space',
|
||||
});
|
||||
|
||||
await testSubjects.existOrFail('ml-jobs-list');
|
||||
await testSubjects.existOrFail('mlPageOverview');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -24,6 +24,10 @@ export default function({ getService }: FtrProviderContext) {
|
|||
await ml.navigation.navigateToMl();
|
||||
});
|
||||
|
||||
it('loads the overview page', async () => {
|
||||
await ml.navigation.navigateToOverview();
|
||||
});
|
||||
|
||||
it('loads the anomaly detection area', async () => {
|
||||
await ml.navigation.navigateToAnomalyDetection();
|
||||
});
|
||||
|
|
|
@ -50,6 +50,10 @@ export function MachineLearningNavigationProvider({
|
|||
]);
|
||||
},
|
||||
|
||||
async navigateToOverview() {
|
||||
await this.navigateToArea('mlMainTab overview', 'mlPageOverview');
|
||||
},
|
||||
|
||||
async navigateToAnomalyDetection() {
|
||||
await this.navigateToArea('mlMainTab anomalyDetection', 'mlPageJobManagement');
|
||||
await this.assertTabsExist('mlSubTab', [
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue