mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[ML] Data frames: Analytics jobs list. (#42598)
Introduces the data frame analytics jobs list.
This commit is contained in:
parent
492ade3d77
commit
81d7d6c2a3
52 changed files with 2720 additions and 11 deletions
|
@ -12,6 +12,10 @@ export interface AuditMessageBase {
|
|||
text?: string;
|
||||
}
|
||||
|
||||
export interface AnalyticsMessage extends AuditMessageBase {
|
||||
analytics_id: string;
|
||||
}
|
||||
|
||||
export interface TransformMessage extends AuditMessageBase {
|
||||
transform_id: string;
|
||||
}
|
||||
|
|
|
@ -28,12 +28,17 @@ export interface Privileges {
|
|||
canDeleteFilter: boolean;
|
||||
// File Data Visualizer
|
||||
canFindFileStructure: boolean;
|
||||
// Data Frames
|
||||
// Data Frame Transforms
|
||||
canGetDataFrame: boolean;
|
||||
canDeleteDataFrame: boolean;
|
||||
canPreviewDataFrame: boolean;
|
||||
canCreateDataFrame: boolean;
|
||||
canStartStopDataFrame: boolean;
|
||||
// Data Frame Analytics
|
||||
canGetDataFrameAnalytics: boolean;
|
||||
canDeleteDataFrameAnalytics: boolean;
|
||||
canCreateDataFrameAnalytics: boolean;
|
||||
canStartStopDataFrameAnalytics: boolean;
|
||||
}
|
||||
|
||||
export function getDefaultPrivileges(): Privileges {
|
||||
|
@ -60,12 +65,17 @@ export function getDefaultPrivileges(): Privileges {
|
|||
canDeleteFilter: false,
|
||||
// File Data Visualizer
|
||||
canFindFileStructure: false,
|
||||
// Data Frames
|
||||
// Data Frame Transforms
|
||||
canGetDataFrame: false,
|
||||
canDeleteDataFrame: false,
|
||||
canPreviewDataFrame: false,
|
||||
canCreateDataFrame: false,
|
||||
canStartStopDataFrame: false,
|
||||
// Data Frame Analytics
|
||||
canGetDataFrameAnalytics: false,
|
||||
canDeleteDataFrameAnalytics: false,
|
||||
canCreateDataFrameAnalytics: false,
|
||||
canStartStopDataFrameAnalytics: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import 'plugins/ml/jobs';
|
|||
import 'plugins/ml/services/calendar_service';
|
||||
import 'plugins/ml/components/messagebar';
|
||||
import 'plugins/ml/data_frame';
|
||||
import 'plugins/ml/data_frame_analytics';
|
||||
import 'plugins/ml/data_visualizer';
|
||||
import 'plugins/ml/datavisualizer';
|
||||
import 'plugins/ml/explorer';
|
||||
|
|
|
@ -17,6 +17,7 @@ const tabSupport = [
|
|||
'jobs',
|
||||
'settings',
|
||||
'data_frames',
|
||||
'data_frame_analytics',
|
||||
'datavisualizer',
|
||||
'filedatavisualizer',
|
||||
'timeseriesexplorer',
|
||||
|
|
|
@ -50,6 +50,13 @@ function getTabs(disableLinks: boolean): Tab[] {
|
|||
}),
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
id: 'data_frame_analytics',
|
||||
name: i18n.translate('xpack.ml.navMenu.dataFrameAnalyticsTabLinkText', {
|
||||
defaultMessage: 'Analytics',
|
||||
}),
|
||||
disabled: disableLinks,
|
||||
},
|
||||
{
|
||||
id: 'datavisualizer',
|
||||
name: i18n.translate('xpack.ml.navMenu.dataVisualizerTabLinkText', {
|
||||
|
@ -72,6 +79,7 @@ enum TAB_TEST_SUBJECT {
|
|||
explorer = 'mlTabAnomalyExplorer',
|
||||
timeseriesexplorer = 'mlTabSingleMetricViewer',
|
||||
data_frames = 'mlTabDataFrames', // eslint-disable-line
|
||||
data_frame_analytics = 'mlTabDataFrameAnalytics', // eslint-disable-line
|
||||
datavisualizer = 'mlTabDataVisualizer',
|
||||
settings = 'mlTabSettings',
|
||||
}
|
||||
|
|
|
@ -37,13 +37,17 @@ exports[`Data Frame: Job List <Page /> Minimal initialization 1`] = `
|
|||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<RefreshTransformListButton
|
||||
isLoading={false}
|
||||
onClick={[Function]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<CreateTransformButton />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -63,10 +63,12 @@ export const Page: FC = () => {
|
|||
</EuiPageContentHeaderSection>
|
||||
<EuiPageContentHeaderSection>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem>
|
||||
{/* grow={false} fixes IE11 issue with nested flex */}
|
||||
<EuiFlexItem grow={false}>
|
||||
<RefreshTransformListButton onClick={refresh} isLoading={isLoading} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{/* grow={false} fixes IE11 issue with nested flex */}
|
||||
<EuiFlexItem grow={false}>
|
||||
<CreateTransformButton />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
@import 'pages/analytics_management/components/analytics_list/index';
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 getDataFrameAnalyticsBreadcrumbs() {
|
||||
return [
|
||||
ML_BREADCRUMB,
|
||||
{
|
||||
text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameLabel', {
|
||||
defaultMessage: 'Analytics',
|
||||
}),
|
||||
href: '',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* 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 { useEffect } from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { filter, distinctUntilChanged } from 'rxjs/operators';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
// @ts-ignore
|
||||
import { isJobIdValid } from '../../../common/util/job_utils';
|
||||
|
||||
// TODO
|
||||
export const moveToAnalyticsWizard = () => {};
|
||||
|
||||
export const isAnalyticsIdValid = isJobIdValid;
|
||||
|
||||
export type IndexName = string;
|
||||
export type IndexPattern = string;
|
||||
export type DataFrameAnalyticsId = string;
|
||||
|
||||
export interface CreateRequestBody {
|
||||
// Description attribute is not supported yet
|
||||
// description?: string;
|
||||
dest: {
|
||||
index: IndexName;
|
||||
results_field: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DataFrameAnalyticsOutlierConfig extends CreateRequestBody {
|
||||
id: DataFrameAnalyticsId;
|
||||
analysis: {
|
||||
outlier_detection: {};
|
||||
};
|
||||
analyzed_fields: {
|
||||
includes: string[];
|
||||
excludes: string[];
|
||||
};
|
||||
model_memory_limit: string;
|
||||
create_time: number;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export enum REFRESH_ANALYTICS_LIST_STATE {
|
||||
ERROR = 'error',
|
||||
IDLE = 'idle',
|
||||
LOADING = 'loading',
|
||||
REFRESH = 'refresh',
|
||||
}
|
||||
export const refreshAnalyticsList$ = new BehaviorSubject<REFRESH_ANALYTICS_LIST_STATE>(
|
||||
REFRESH_ANALYTICS_LIST_STATE.IDLE
|
||||
);
|
||||
|
||||
export const useRefreshAnalyticsList = (
|
||||
callback: {
|
||||
isLoading?(d: boolean): void;
|
||||
onRefresh?(): void;
|
||||
} = {}
|
||||
) => {
|
||||
useEffect(() => {
|
||||
const distinct$ = refreshAnalyticsList$.pipe(distinctUntilChanged());
|
||||
|
||||
const subscriptions: Subscription[] = [];
|
||||
|
||||
if (typeof callback.onRefresh === 'function') {
|
||||
// initial call to refresh
|
||||
callback.onRefresh();
|
||||
|
||||
subscriptions.push(
|
||||
distinct$
|
||||
.pipe(filter(state => state === REFRESH_ANALYTICS_LIST_STATE.REFRESH))
|
||||
.subscribe(() => typeof callback.onRefresh === 'function' && callback.onRefresh())
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof callback.isLoading === 'function') {
|
||||
subscriptions.push(
|
||||
distinct$.subscribe(
|
||||
state =>
|
||||
typeof callback.isLoading === 'function' &&
|
||||
callback.isLoading(state === REFRESH_ANALYTICS_LIST_STATE.LOADING)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
subscriptions.map(sub => sub.unsubscribe());
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
refresh: () => {
|
||||
// A refresh is followed immediately by setting the state to loading
|
||||
// to trigger data fetching and loading indicators in one go.
|
||||
refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.REFRESH);
|
||||
refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.LOADING);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 {
|
||||
isAnalyticsIdValid,
|
||||
moveToAnalyticsWizard,
|
||||
refreshAnalyticsList$,
|
||||
useRefreshAnalyticsList,
|
||||
CreateRequestBody,
|
||||
DataFrameAnalyticsId,
|
||||
DataFrameAnalyticsOutlierConfig,
|
||||
IndexName,
|
||||
IndexPattern,
|
||||
REFRESH_ANALYTICS_LIST_STATE,
|
||||
} from './analytics';
|
|
@ -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 './pages/analytics_management/directive';
|
||||
import './pages/analytics_management/route';
|
|
@ -0,0 +1,26 @@
|
|||
.mlAnalyticsTable {
|
||||
// Using an override as a last resort because we cannot set custom classes on
|
||||
// nested upstream components. The opening animation limits the height
|
||||
// of the expanded row to 1000px which turned out to be not predictable.
|
||||
// The animation could also result in flickering with expanded rows
|
||||
// where the inner content would result in the DOM changing the height.
|
||||
.euiTableRow-isExpandedRow .euiTableCellContent {
|
||||
animation: none !important;
|
||||
.euiTableCellContent__text {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
// Another override: Because an update to the table replaces the DOM, the same
|
||||
// icon would still again fade in with an animation. If the table refreshes with
|
||||
// e.g. 1s this would result in a blinking icon effect.
|
||||
.euiIcon-isLoaded {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
.mlAnalyticsProgressBar {
|
||||
margin-bottom: $euiSizeM;
|
||||
}
|
||||
|
||||
.mlTaskStateBadge, .mlTaskModeBadge {
|
||||
max-width: 100px;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
@import 'analytics_table';
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiConfirmModal,
|
||||
EuiOverlayMask,
|
||||
EuiToolTip,
|
||||
EUI_MODAL_CONFIRM_BUTTON,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { deleteAnalytics } from '../../services/analytics_service';
|
||||
|
||||
import {
|
||||
checkPermission,
|
||||
createPermissionFailureMessage,
|
||||
} from '../../../../../privilege/check_privilege';
|
||||
|
||||
import { DataFrameAnalyticsListRow, DATA_FRAME_TASK_STATE } from './common';
|
||||
|
||||
interface DeleteActionProps {
|
||||
item: DataFrameAnalyticsListRow;
|
||||
}
|
||||
|
||||
export const DeleteAction: FC<DeleteActionProps> = ({ item }) => {
|
||||
const disabled = item.stats.state === DATA_FRAME_TASK_STATE.STARTED;
|
||||
|
||||
const canDeleteDataFrame: boolean = checkPermission('canDeleteDataFrame');
|
||||
|
||||
const [isModalVisible, setModalVisible] = useState(false);
|
||||
|
||||
const closeModal = () => setModalVisible(false);
|
||||
const deleteAndCloseModal = () => {
|
||||
setModalVisible(false);
|
||||
deleteAnalytics(item);
|
||||
};
|
||||
const openModal = () => setModalVisible(true);
|
||||
|
||||
const buttonDeleteText = i18n.translate('xpack.ml.dataframe.analyticsList.deleteActionName', {
|
||||
defaultMessage: 'Delete',
|
||||
});
|
||||
|
||||
let deleteButton = (
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
color="text"
|
||||
disabled={disabled || !canDeleteDataFrame}
|
||||
iconType="trash"
|
||||
onClick={openModal}
|
||||
aria-label={buttonDeleteText}
|
||||
>
|
||||
{buttonDeleteText}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
if (disabled || !canDeleteDataFrame) {
|
||||
deleteButton = (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={
|
||||
disabled
|
||||
? i18n.translate(
|
||||
'xpack.ml.dataframe.analyticsList.deleteActionDisabledToolTipContent',
|
||||
{
|
||||
defaultMessage: 'Stop the data frame analytics in order to delete it.',
|
||||
}
|
||||
)
|
||||
: createPermissionFailureMessage('canStartStopDataFrameAnalytics')
|
||||
}
|
||||
>
|
||||
{deleteButton}
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{deleteButton}
|
||||
{isModalVisible && (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
title={i18n.translate('xpack.ml.dataframe.analyticsList.deleteModalTitle', {
|
||||
defaultMessage: 'Delete {analyticsId}',
|
||||
values: { analyticsId: item.config.id },
|
||||
})}
|
||||
onCancel={closeModal}
|
||||
onConfirm={deleteAndCloseModal}
|
||||
cancelButtonText={i18n.translate(
|
||||
'xpack.ml.dataframe.analyticsList.deleteModalCancelButton',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
)}
|
||||
confirmButtonText={i18n.translate(
|
||||
'xpack.ml.dataframe.analyticsList.deleteModalDeleteButton',
|
||||
{
|
||||
defaultMessage: 'Delete',
|
||||
}
|
||||
)}
|
||||
defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON}
|
||||
buttonColor="danger"
|
||||
>
|
||||
<p>
|
||||
{i18n.translate('xpack.ml.dataframe.analyticsList.deleteModalBody', {
|
||||
defaultMessage: `Are you sure you want to delete this analytics job? The analytics job's destination index and optional Kibana index pattern will not be deleted.`,
|
||||
})}
|
||||
</p>
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* 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, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiConfirmModal,
|
||||
EuiOverlayMask,
|
||||
EuiToolTip,
|
||||
EUI_MODAL_CONFIRM_BUTTON,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { startAnalytics } from '../../services/analytics_service';
|
||||
|
||||
import {
|
||||
checkPermission,
|
||||
createPermissionFailureMessage,
|
||||
} from '../../../../../privilege/check_privilege';
|
||||
|
||||
import { DataFrameAnalyticsListRow, isCompletedBatchAnalytics } from './common';
|
||||
|
||||
interface StartActionProps {
|
||||
item: DataFrameAnalyticsListRow;
|
||||
}
|
||||
|
||||
export const StartAction: FC<StartActionProps> = ({ item }) => {
|
||||
const canStartStopDataFrameAnalytics: boolean = checkPermission('canStartStopDataFrameAnalytics');
|
||||
|
||||
const [isModalVisible, setModalVisible] = useState(false);
|
||||
|
||||
const closeModal = () => setModalVisible(false);
|
||||
const startAndCloseModal = () => {
|
||||
setModalVisible(false);
|
||||
startAnalytics(item);
|
||||
};
|
||||
const openModal = () => setModalVisible(true);
|
||||
|
||||
const buttonStartText = i18n.translate('xpack.ml.dataframe.analyticsList.startActionName', {
|
||||
defaultMessage: 'Start',
|
||||
});
|
||||
|
||||
// Disable start for batch analytics which have completed.
|
||||
const completedBatchAnalytics = isCompletedBatchAnalytics(item);
|
||||
|
||||
let startButton = (
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
color="text"
|
||||
disabled={!canStartStopDataFrameAnalytics || completedBatchAnalytics}
|
||||
iconType="play"
|
||||
onClick={openModal}
|
||||
aria-label={buttonStartText}
|
||||
>
|
||||
{buttonStartText}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
if (!canStartStopDataFrameAnalytics || completedBatchAnalytics) {
|
||||
startButton = (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={
|
||||
!canStartStopDataFrameAnalytics
|
||||
? createPermissionFailureMessage('canStartStopDataFrameAnalytics')
|
||||
: i18n.translate('xpack.ml.dataframe.analyticsList.completeBatchAnalyticsToolTip', {
|
||||
defaultMessage:
|
||||
'{analyticsId} is a completed batch analytics job and cannot be restarted.',
|
||||
values: { analyticsId: item.config.id },
|
||||
})
|
||||
}
|
||||
>
|
||||
{startButton}
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{startButton}
|
||||
{isModalVisible && (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
title={i18n.translate('xpack.ml.dataframe.analyticsList.startModalTitle', {
|
||||
defaultMessage: 'Start {analyticsId}',
|
||||
values: { analyticsId: item.config.id },
|
||||
})}
|
||||
onCancel={closeModal}
|
||||
onConfirm={startAndCloseModal}
|
||||
cancelButtonText={i18n.translate(
|
||||
'xpack.ml.dataframe.analyticsList.startModalCancelButton',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
)}
|
||||
confirmButtonText={i18n.translate(
|
||||
'xpack.ml.dataframe.analyticsList.startModalStartButton',
|
||||
{
|
||||
defaultMessage: 'Start',
|
||||
}
|
||||
)}
|
||||
defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON}
|
||||
buttonColor="primary"
|
||||
>
|
||||
<p>
|
||||
{i18n.translate('xpack.ml.dataframe.analyticsList.startModalBody', {
|
||||
defaultMessage:
|
||||
'A data frame analytics job will increase search and indexing load in your cluster. Please stop the analytics job if excessive load is experienced. Are you sure you want to start this analytics job?',
|
||||
})}
|
||||
</p>
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -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 from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
|
||||
|
||||
import {
|
||||
checkPermission,
|
||||
createPermissionFailureMessage,
|
||||
} from '../../../../../privilege/check_privilege';
|
||||
|
||||
import { DataFrameAnalyticsListRow, DATA_FRAME_TASK_STATE } from './common';
|
||||
import { stopAnalytics } from '../../services/analytics_service';
|
||||
|
||||
import { StartAction } from './action_start';
|
||||
import { DeleteAction } from './action_delete';
|
||||
|
||||
export const getActions = () => {
|
||||
const canStartStopDataFrameAnalytics: boolean = checkPermission('canStartStopDataFrameAnalytics');
|
||||
|
||||
return [
|
||||
{
|
||||
isPrimary: true,
|
||||
render: (item: DataFrameAnalyticsListRow) => {
|
||||
if (
|
||||
item.stats.state !== DATA_FRAME_TASK_STATE.STARTED &&
|
||||
item.stats.state !== DATA_FRAME_TASK_STATE.REINDEXING
|
||||
) {
|
||||
return <StartAction item={item} />;
|
||||
}
|
||||
|
||||
const buttonStopText = i18n.translate('xpack.ml.dataframe.analyticsList.stopActionName', {
|
||||
defaultMessage: 'Stop',
|
||||
});
|
||||
|
||||
const stopButton = (
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
color="text"
|
||||
disabled={!canStartStopDataFrameAnalytics}
|
||||
iconType="stop"
|
||||
onClick={() => stopAnalytics(item)}
|
||||
aria-label={buttonStopText}
|
||||
>
|
||||
{buttonStopText}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
if (!canStartStopDataFrameAnalytics) {
|
||||
return (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={createPermissionFailureMessage('canStartStopDataFrameAnalytics')}
|
||||
>
|
||||
{stopButton}
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
||||
return stopButton;
|
||||
},
|
||||
},
|
||||
{
|
||||
render: (item: DataFrameAnalyticsListRow) => {
|
||||
return <DeleteAction item={item} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
|
@ -0,0 +1,326 @@
|
|||
/*
|
||||
* 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, useState } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
// EuiBadge,
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiEmptyPrompt,
|
||||
SortDirection,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import {
|
||||
DataFrameAnalyticsId,
|
||||
moveToAnalyticsWizard,
|
||||
useRefreshAnalyticsList,
|
||||
} from '../../../../common';
|
||||
import { checkPermission } from '../../../../../privilege/check_privilege';
|
||||
import { getTaskStateBadge } from './columns';
|
||||
|
||||
import {
|
||||
DataFrameAnalyticsListColumn,
|
||||
DataFrameAnalyticsListRow,
|
||||
ItemIdToExpandedRowMap,
|
||||
DATA_FRAME_TASK_STATE,
|
||||
// DATA_FRAME_MODE,
|
||||
Query,
|
||||
Clause,
|
||||
} from './common';
|
||||
import { getAnalyticsFactory } from '../../services/analytics_service';
|
||||
import { getColumns } from './columns';
|
||||
import { ExpandedRow } from './expanded_row';
|
||||
import { ProgressBar, AnalyticsTable } from './analytics_table';
|
||||
import { useRefreshInterval } from './use_refresh_interval';
|
||||
|
||||
function getItemIdToExpandedRowMap(
|
||||
itemIds: DataFrameAnalyticsId[],
|
||||
dataFrameAnalytics: DataFrameAnalyticsListRow[]
|
||||
): ItemIdToExpandedRowMap {
|
||||
return itemIds.reduce(
|
||||
(m: ItemIdToExpandedRowMap, analyticsId: DataFrameAnalyticsId) => {
|
||||
const item = dataFrameAnalytics.find(analytics => analytics.config.id === analyticsId);
|
||||
if (item !== undefined) {
|
||||
m[analyticsId] = <ExpandedRow item={item} />;
|
||||
}
|
||||
return m;
|
||||
},
|
||||
{} as ItemIdToExpandedRowMap
|
||||
);
|
||||
}
|
||||
|
||||
function stringMatch(str: string | undefined, substr: string) {
|
||||
return (
|
||||
typeof str === 'string' &&
|
||||
typeof substr === 'string' &&
|
||||
(str.toLowerCase().match(substr.toLowerCase()) === null) === false
|
||||
);
|
||||
}
|
||||
|
||||
export const DataFrameAnalyticsList: FC = () => {
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [blockRefresh, setBlockRefresh] = useState(false);
|
||||
const [filterActive, setFilterActive] = useState(false);
|
||||
|
||||
const [analytics, setAnalytics] = useState<DataFrameAnalyticsListRow[]>([]);
|
||||
const [filteredAnalytics, setFilteredAnalytics] = useState<DataFrameAnalyticsListRow[]>([]);
|
||||
const [expandedRowItemIds, setExpandedRowItemIds] = useState<DataFrameAnalyticsId[]>([]);
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<any>(undefined);
|
||||
const [searchError, setSearchError] = useState<any>(undefined);
|
||||
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
const [sortField, setSortField] = useState<string>(DataFrameAnalyticsListColumn.id);
|
||||
const [sortDirection, setSortDirection] = useState<string>(SortDirection.ASC);
|
||||
|
||||
const disabled =
|
||||
!checkPermission('canCreateDataFrameAnalytics') ||
|
||||
!checkPermission('canStartStopDataFrameAnalytics');
|
||||
|
||||
const getAnalytics = getAnalyticsFactory(
|
||||
setAnalytics,
|
||||
setErrorMessage,
|
||||
setIsInitialized,
|
||||
blockRefresh
|
||||
);
|
||||
// Subscribe to the refresh observable to trigger reloading the analytics list.
|
||||
useRefreshAnalyticsList({
|
||||
isLoading: setIsLoading,
|
||||
onRefresh: () => getAnalytics(true),
|
||||
});
|
||||
// Call useRefreshInterval() after the subscription above is set up.
|
||||
useRefreshInterval(setBlockRefresh);
|
||||
|
||||
const onQueryChange = ({ query, error }: { query: Query; error: any }) => {
|
||||
if (error) {
|
||||
setSearchError(error.message);
|
||||
} else {
|
||||
let clauses: Clause[] = [];
|
||||
if (query && query.ast !== undefined && query.ast.clauses !== undefined) {
|
||||
clauses = query.ast.clauses;
|
||||
}
|
||||
if (clauses.length > 0) {
|
||||
setFilterActive(true);
|
||||
filterAnalytics(clauses);
|
||||
} else {
|
||||
setFilterActive(false);
|
||||
}
|
||||
setSearchError(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const filterAnalytics = (clauses: Clause[]) => {
|
||||
setIsLoading(true);
|
||||
// keep count of the number of matches we make as we're looping over the clauses
|
||||
// we only want to return analytics which match all clauses, i.e. each search term is ANDed
|
||||
// { analytics-one: { analytics: { id: analytics-one, config: {}, state: {}, ... }, count: 0 }, analytics-two: {...} }
|
||||
const matches: Record<string, any> = analytics.reduce((p: Record<string, any>, c) => {
|
||||
p[c.id] = {
|
||||
analytics: c,
|
||||
count: 0,
|
||||
};
|
||||
return p;
|
||||
}, {});
|
||||
|
||||
clauses.forEach(c => {
|
||||
// the search term could be negated with a minus, e.g. -bananas
|
||||
const bool = c.match === 'must';
|
||||
let ts = [];
|
||||
|
||||
if (c.type === 'term') {
|
||||
// filter term based clauses, e.g. bananas
|
||||
// match on id and description
|
||||
// if the term has been negated, AND the matches
|
||||
if (bool === true) {
|
||||
ts = analytics.filter(
|
||||
d => stringMatch(d.id, c.value) === bool // ||
|
||||
// stringMatch(d.config.description, c.value) === bool
|
||||
);
|
||||
} else {
|
||||
ts = analytics.filter(
|
||||
d => stringMatch(d.id, c.value) === bool // &&
|
||||
// stringMatch(d.config.description, c.value) === bool
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// filter other clauses, i.e. the mode and status filters
|
||||
if (Array.isArray(c.value)) {
|
||||
// the status value is an array of string(s) e.g. ['failed', 'stopped']
|
||||
ts = analytics.filter(d => c.value.includes(d.stats.state));
|
||||
} else {
|
||||
ts = analytics.filter(d => d.mode === c.value);
|
||||
}
|
||||
}
|
||||
|
||||
ts.forEach(t => matches[t.id].count++);
|
||||
});
|
||||
|
||||
// loop through the matches and return only analytics which have match all the clauses
|
||||
const filtered = Object.values(matches)
|
||||
.filter(m => (m && m.count) >= clauses.length)
|
||||
.map(m => m.analytics);
|
||||
|
||||
setFilteredAnalytics(filtered);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
// Before the analytics have been loaded for the first time, display the loading indicator only.
|
||||
// Otherwise a user would see 'No data frame analytics found' during the initial loading.
|
||||
if (!isInitialized) {
|
||||
return <ProgressBar isLoading={isLoading} />;
|
||||
}
|
||||
|
||||
if (typeof errorMessage !== 'undefined') {
|
||||
return (
|
||||
<Fragment>
|
||||
<ProgressBar isLoading={isLoading} />
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.ml.dataFrame.analyticsList.errorPromptTitle', {
|
||||
defaultMessage: 'An error occurred getting the data frame analytics list.',
|
||||
})}
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
>
|
||||
<pre>{JSON.stringify(errorMessage)}</pre>
|
||||
</EuiCallOut>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
if (analytics.length === 0) {
|
||||
return (
|
||||
<Fragment>
|
||||
<ProgressBar isLoading={isLoading} />
|
||||
<EuiEmptyPrompt
|
||||
title={
|
||||
<h2>
|
||||
{i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptTitle', {
|
||||
defaultMessage: 'No data frame analytics found',
|
||||
})}
|
||||
</h2>
|
||||
}
|
||||
actions={[
|
||||
<EuiButtonEmpty
|
||||
onClick={moveToAnalyticsWizard}
|
||||
isDisabled={disabled}
|
||||
style={{ display: 'none' }}
|
||||
>
|
||||
{i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptButtonText', {
|
||||
defaultMessage: 'Create your first data frame analytics',
|
||||
})}
|
||||
</EuiButtonEmpty>,
|
||||
]}
|
||||
data-test-subj="mlNoDataFrameAnalyticsFound"
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const columns = getColumns(expandedRowItemIds, setExpandedRowItemIds);
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: sortField,
|
||||
direction: sortDirection,
|
||||
},
|
||||
};
|
||||
|
||||
const itemIdToExpandedRowMap = getItemIdToExpandedRowMap(expandedRowItemIds, analytics);
|
||||
|
||||
const pagination = {
|
||||
initialPageIndex: pageIndex,
|
||||
initialPageSize: pageSize,
|
||||
totalItemCount: analytics.length,
|
||||
pageSizeOptions: [10, 20, 50],
|
||||
hidePerPageOptions: false,
|
||||
};
|
||||
|
||||
const search = {
|
||||
onChange: onQueryChange,
|
||||
box: {
|
||||
incremental: true,
|
||||
},
|
||||
filters: [
|
||||
{
|
||||
type: 'field_value_selection',
|
||||
field: 'state.state',
|
||||
name: i18n.translate('xpack.ml.dataframe.analyticsList.statusFilter', {
|
||||
defaultMessage: 'Status',
|
||||
}),
|
||||
multiSelect: 'or',
|
||||
options: Object.values(DATA_FRAME_TASK_STATE).map(val => ({
|
||||
value: val,
|
||||
name: val,
|
||||
view: getTaskStateBadge(val),
|
||||
})),
|
||||
},
|
||||
// For now analytics jobs are batch only
|
||||
/*
|
||||
{
|
||||
type: 'field_value_selection',
|
||||
field: 'mode',
|
||||
name: i18n.translate('xpack.ml.dataframe.analyticsList.modeFilter', {
|
||||
defaultMessage: 'Mode',
|
||||
}),
|
||||
multiSelect: false,
|
||||
options: Object.values(DATA_FRAME_MODE).map(val => ({
|
||||
value: val,
|
||||
name: val,
|
||||
view: (
|
||||
<EuiBadge className="mlTaskModeBadge" color="hollow">
|
||||
{val}
|
||||
</EuiBadge>
|
||||
),
|
||||
})),
|
||||
},
|
||||
*/
|
||||
],
|
||||
};
|
||||
|
||||
const onTableChange = ({
|
||||
page = { index: 0, size: 10 },
|
||||
sort = { field: DataFrameAnalyticsListColumn.id, direction: SortDirection.ASC },
|
||||
}: {
|
||||
page: { index: number; size: number };
|
||||
sort: { field: string; direction: string };
|
||||
}) => {
|
||||
const { index, size } = page;
|
||||
setPageIndex(index);
|
||||
setPageSize(size);
|
||||
|
||||
const { field, direction } = sort;
|
||||
setSortField(field);
|
||||
setSortDirection(direction);
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ProgressBar isLoading={isLoading} />
|
||||
<AnalyticsTable
|
||||
className="mlAnalyticsTable"
|
||||
columns={columns}
|
||||
error={searchError}
|
||||
hasActions={false}
|
||||
isExpandable={true}
|
||||
isSelectable={false}
|
||||
items={filterActive ? filteredAnalytics : analytics}
|
||||
itemId={DataFrameAnalyticsListColumn.id}
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
onChange={onTableChange}
|
||||
pagination={pagination}
|
||||
sorting={sorting}
|
||||
search={search}
|
||||
data-test-subj="mlDataFramesTableAnalytics"
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// This component extends EuiInMemoryTable with some
|
||||
// fixes and TS specs until the changes become available upstream.
|
||||
|
||||
import React, { Component, Fragment } from 'react';
|
||||
|
||||
import { EuiInMemoryTable, EuiInMemoryTableProps, EuiProgress } from '@elastic/eui';
|
||||
|
||||
import { ItemIdToExpandedRowMap } from './common';
|
||||
|
||||
// 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
|
||||
// our own progress bar on top of the table. `EuiProgress` after `isLoading` displays
|
||||
// the loading indicator. The variation after `!isLoading` displays an empty progress
|
||||
// bar fixed to 0%. Without it, the display would vertically jump when showing/hiding
|
||||
// the progress bar.
|
||||
export const ProgressBar = ({ isLoading = false }) => {
|
||||
return (
|
||||
<Fragment>
|
||||
{isLoading && <EuiProgress className="mlAnalyticsProgressBar" size="xs" color="primary" />}
|
||||
{!isLoading && (
|
||||
<EuiProgress className="mlAnalyticsProgressBar" value={0} max={100} size="xs" />
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
// copied from EUI to be available to the extended getDerivedStateFromProps()
|
||||
function findColumnByProp(columns: any, prop: any, value: any) {
|
||||
for (let i = 0; i < columns.length; i++) {
|
||||
const column = columns[i];
|
||||
if (column[prop] === value) {
|
||||
return column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// copied from EUI to be available to the extended getDerivedStateFromProps()
|
||||
const getInitialSorting = (columns: any, sorting: any) => {
|
||||
if (!sorting || !sorting.sort) {
|
||||
return {
|
||||
sortName: undefined,
|
||||
sortDirection: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const { field: sortable, direction: sortDirection } = sorting.sort;
|
||||
|
||||
// sortable could be a column's `field` or its `name`
|
||||
// for backwards compatibility `field` must be checked first
|
||||
let sortColumn = findColumnByProp(columns, 'field', sortable);
|
||||
if (sortColumn == null) {
|
||||
sortColumn = findColumnByProp(columns, 'name', sortable);
|
||||
}
|
||||
|
||||
if (sortColumn == null) {
|
||||
return {
|
||||
sortName: undefined,
|
||||
sortDirection: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const sortName = sortColumn.name;
|
||||
|
||||
return {
|
||||
sortName,
|
||||
sortDirection,
|
||||
};
|
||||
};
|
||||
|
||||
// TODO EUI's types for EuiInMemoryTable is missing these props
|
||||
interface ExpandableTableProps extends EuiInMemoryTableProps {
|
||||
itemIdToExpandedRowMap?: ItemIdToExpandedRowMap;
|
||||
isExpandable?: boolean;
|
||||
onChange({ page }: { page?: {} | undefined }): void;
|
||||
loading?: boolean;
|
||||
compressed?: boolean;
|
||||
error?: string;
|
||||
}
|
||||
interface ComponentWithConstructor<T> extends Component {
|
||||
new (): Component<T>;
|
||||
}
|
||||
const ExpandableTable = (EuiInMemoryTable as any) as ComponentWithConstructor<ExpandableTableProps>;
|
||||
|
||||
export class AnalyticsTable extends ExpandableTable {
|
||||
static getDerivedStateFromProps(nextProps: any, prevState: any) {
|
||||
const derivedState = {
|
||||
...prevState.prevProps,
|
||||
pageIndex: nextProps.pagination.initialPageIndex,
|
||||
pageSize: nextProps.pagination.initialPageSize,
|
||||
};
|
||||
|
||||
if (nextProps.items !== prevState.prevProps.items) {
|
||||
Object.assign(derivedState, {
|
||||
prevProps: {
|
||||
items: nextProps.items,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const { sortName, sortDirection } = getInitialSorting(nextProps.columns, nextProps.sorting);
|
||||
if (
|
||||
sortName !== prevState.prevProps.sortName ||
|
||||
sortDirection !== prevState.prevProps.sortDirection
|
||||
) {
|
||||
Object.assign(derivedState, {
|
||||
sortName,
|
||||
sortDirection,
|
||||
});
|
||||
}
|
||||
return derivedState;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,216 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
import { idx } from '@kbn/elastic-idx';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiProgress,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
RIGHT_ALIGNMENT,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { DataFrameAnalyticsId } from '../../../../common';
|
||||
import {
|
||||
DATA_FRAME_TASK_STATE,
|
||||
DataFrameAnalyticsListColumn,
|
||||
DataFrameAnalyticsListRow,
|
||||
DataFrameAnalyticsStats,
|
||||
} from './common';
|
||||
import { getActions } from './actions';
|
||||
|
||||
enum TASK_STATE_COLOR {
|
||||
failed = 'danger',
|
||||
reindexing = 'primary',
|
||||
started = 'primary',
|
||||
stopped = 'hollow',
|
||||
}
|
||||
|
||||
export const getTaskStateBadge = (
|
||||
state: DataFrameAnalyticsStats['state'],
|
||||
reason?: DataFrameAnalyticsStats['reason']
|
||||
) => {
|
||||
const color = TASK_STATE_COLOR[state];
|
||||
|
||||
if (state === DATA_FRAME_TASK_STATE.FAILED && reason !== undefined) {
|
||||
return (
|
||||
<EuiToolTip content={reason}>
|
||||
<EuiBadge className="mlTaskStateBadge" color={color}>
|
||||
{state}
|
||||
</EuiBadge>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiBadge className="mlTaskStateBadge" color={color}>
|
||||
{state}
|
||||
</EuiBadge>
|
||||
);
|
||||
};
|
||||
|
||||
export const getColumns = (
|
||||
expandedRowItemIds: DataFrameAnalyticsId[],
|
||||
setExpandedRowItemIds: React.Dispatch<React.SetStateAction<DataFrameAnalyticsId[]>>
|
||||
) => {
|
||||
const actions = getActions();
|
||||
|
||||
function toggleDetails(item: DataFrameAnalyticsListRow) {
|
||||
const index = expandedRowItemIds.indexOf(item.config.id);
|
||||
if (index !== -1) {
|
||||
expandedRowItemIds.splice(index, 1);
|
||||
setExpandedRowItemIds([...expandedRowItemIds]);
|
||||
} else {
|
||||
expandedRowItemIds.push(item.config.id);
|
||||
}
|
||||
|
||||
// spread to a new array otherwise the component wouldn't re-render
|
||||
setExpandedRowItemIds([...expandedRowItemIds]);
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
align: RIGHT_ALIGNMENT,
|
||||
width: '40px',
|
||||
isExpander: true,
|
||||
render: (item: DataFrameAnalyticsListRow) => (
|
||||
<EuiButtonIcon
|
||||
onClick={() => toggleDetails(item)}
|
||||
aria-label={
|
||||
expandedRowItemIds.includes(item.config.id)
|
||||
? i18n.translate('xpack.ml.dataframe.analyticsList.rowCollapse', {
|
||||
defaultMessage: 'Hide details for {analyticsId}',
|
||||
values: { analyticsId: item.config.id },
|
||||
})
|
||||
: i18n.translate('xpack.ml.dataframe.analyticsList.rowExpand', {
|
||||
defaultMessage: 'Show details for {analyticsId}',
|
||||
values: { analyticsId: item.config.id },
|
||||
})
|
||||
}
|
||||
iconType={expandedRowItemIds.includes(item.config.id) ? 'arrowUp' : 'arrowDown'}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: DataFrameAnalyticsListColumn.id,
|
||||
name: 'ID',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
// Description is not supported yet by API
|
||||
/*
|
||||
{
|
||||
field: DataFrameAnalyticsListColumn.description,
|
||||
name: i18n.translate('xpack.ml.dataframe.analyticsList.description', {
|
||||
defaultMessage: 'Description',
|
||||
}),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
*/
|
||||
{
|
||||
field: DataFrameAnalyticsListColumn.configSourceIndex,
|
||||
name: i18n.translate('xpack.ml.dataframe.analyticsList.sourceIndex', {
|
||||
defaultMessage: 'Source index',
|
||||
}),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
field: DataFrameAnalyticsListColumn.configDestIndex,
|
||||
name: i18n.translate('xpack.ml.dataframe.analyticsList.destinationIndex', {
|
||||
defaultMessage: 'Destination index',
|
||||
}),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.ml.dataframe.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',
|
||||
},
|
||||
// For now there is batch mode only so we hide this column for now.
|
||||
/*
|
||||
{
|
||||
name: i18n.translate('xpack.ml.dataframe.analyticsList.mode', { defaultMessage: 'Mode' }),
|
||||
sortable: (item: DataFrameAnalyticsListRow) => item.mode,
|
||||
truncateText: true,
|
||||
render(item: DataFrameAnalyticsListRow) {
|
||||
const mode = item.mode;
|
||||
const color = 'hollow';
|
||||
return <EuiBadge color={color}>{mode}</EuiBadge>;
|
||||
},
|
||||
width: '100px',
|
||||
},
|
||||
*/
|
||||
{
|
||||
name: i18n.translate('xpack.ml.dataframe.analyticsList.progress', {
|
||||
defaultMessage: 'Progress',
|
||||
}),
|
||||
sortable: (item: DataFrameAnalyticsListRow) => idx(item, _ => _.stats.progress_percent) || 0,
|
||||
truncateText: true,
|
||||
render(item: DataFrameAnalyticsListRow) {
|
||||
if (item.stats.progress_percent === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const progress = Math.round(item.stats.progress_percent);
|
||||
|
||||
// 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',
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.ml.dataframe.analyticsList.tableActionLabel', {
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
actions,
|
||||
width: '200px',
|
||||
},
|
||||
];
|
||||
};
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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 { DataFrameAnalyticsId, DataFrameAnalyticsOutlierConfig } from '../../../../common';
|
||||
|
||||
export enum DATA_FRAME_TASK_STATE {
|
||||
FAILED = 'failed',
|
||||
REINDEXING = 'reindexing',
|
||||
STARTED = 'started',
|
||||
STOPPED = 'stopped',
|
||||
}
|
||||
|
||||
export enum DATA_FRAME_MODE {
|
||||
BATCH = 'batch',
|
||||
CONTINUOUS = 'continuous',
|
||||
}
|
||||
|
||||
export interface Clause {
|
||||
type: string;
|
||||
value: string;
|
||||
match: string;
|
||||
}
|
||||
|
||||
export interface Query {
|
||||
ast: {
|
||||
clauses: Clause[];
|
||||
};
|
||||
text: string;
|
||||
syntax: any;
|
||||
}
|
||||
|
||||
export interface DataFrameAnalyticsStats {
|
||||
assignment_explanation?: string;
|
||||
id: DataFrameAnalyticsId;
|
||||
node?: {
|
||||
attributes: Record<string, any>;
|
||||
ephemeral_id: string;
|
||||
id: string;
|
||||
name: string;
|
||||
transport_address: string;
|
||||
};
|
||||
progress_percent?: number;
|
||||
reason?: string;
|
||||
state: DATA_FRAME_TASK_STATE;
|
||||
}
|
||||
|
||||
export function isDataFrameAnalyticsStats(arg: any): arg is DataFrameAnalyticsStats {
|
||||
return (
|
||||
typeof arg === 'object' &&
|
||||
arg !== null &&
|
||||
{}.hasOwnProperty.call(arg, 'state') &&
|
||||
Object.values(DATA_FRAME_TASK_STATE).includes(arg.state)
|
||||
);
|
||||
}
|
||||
|
||||
export interface DataFrameAnalyticsListRow {
|
||||
id: DataFrameAnalyticsId;
|
||||
checkpointing: object;
|
||||
config: DataFrameAnalyticsOutlierConfig;
|
||||
mode: string;
|
||||
stats: DataFrameAnalyticsStats;
|
||||
}
|
||||
|
||||
// Used to pass on attribute names to table columns
|
||||
export enum DataFrameAnalyticsListColumn {
|
||||
configDestIndex = 'config.dest.index',
|
||||
configSourceIndex = 'config.source.index',
|
||||
// Description attribute is not supported yet by API
|
||||
// description = 'config.description',
|
||||
id = 'id',
|
||||
}
|
||||
|
||||
export type ItemIdToExpandedRowMap = Record<string, JSX.Element>;
|
||||
|
||||
export function isCompletedBatchAnalytics(item: DataFrameAnalyticsListRow) {
|
||||
// For now all analytics jobs are batch jobs.
|
||||
return false;
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* 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 moment from 'moment-timezone';
|
||||
|
||||
import { EuiTabbedContent } from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { formatHumanReadableDateTimeSeconds } from '../../../../../util/date_utils';
|
||||
|
||||
import { DataFrameAnalyticsListRow } from './common';
|
||||
import { ExpandedRowDetailsPane, SectionConfig } from './expanded_row_details_pane';
|
||||
import { ExpandedRowJsonPane } from './expanded_row_json_pane';
|
||||
// import { ExpandedRowMessagesPane } from './expanded_row_messages_pane';
|
||||
|
||||
function getItemDescription(value: any) {
|
||||
if (typeof value === 'object') {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
interface Props {
|
||||
item: DataFrameAnalyticsListRow;
|
||||
}
|
||||
|
||||
export const ExpandedRow: FC<Props> = ({ item }) => {
|
||||
const stateValues = { ...item.stats };
|
||||
|
||||
const state: SectionConfig = {
|
||||
title: 'State',
|
||||
items: Object.entries(stateValues).map(s => {
|
||||
return { title: s[0].toString(), description: getItemDescription(s[1]) };
|
||||
}),
|
||||
position: 'left',
|
||||
};
|
||||
|
||||
const checkpointing: SectionConfig = {
|
||||
title: 'Checkpointing',
|
||||
items: Object.entries(item.checkpointing).map(s => {
|
||||
return { title: s[0].toString(), description: getItemDescription(s[1]) };
|
||||
}),
|
||||
position: 'left',
|
||||
};
|
||||
|
||||
const stats: SectionConfig = {
|
||||
title: 'Stats',
|
||||
items: [
|
||||
{
|
||||
title: 'create_time',
|
||||
description: formatHumanReadableDateTimeSeconds(
|
||||
moment(item.config.create_time).unix() * 1000
|
||||
),
|
||||
},
|
||||
{ title: 'model_memory_limit', description: item.config.model_memory_limit },
|
||||
{ title: 'version', description: item.config.version },
|
||||
],
|
||||
position: 'right',
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: 'analytics-details',
|
||||
name: i18n.translate(
|
||||
'xpack.ml.dataframe.analyticsList.analyticsDetails.tabs.analyticsSettingsLabel',
|
||||
{
|
||||
defaultMessage: 'Analytics details',
|
||||
}
|
||||
),
|
||||
content: <ExpandedRowDetailsPane sections={[state, checkpointing, stats]} />,
|
||||
},
|
||||
{
|
||||
id: 'analytics-json',
|
||||
name: 'JSON',
|
||||
content: <ExpandedRowJsonPane json={item.config} />,
|
||||
},
|
||||
// Audit messages are not yet supported by the analytics API.
|
||||
/*
|
||||
{
|
||||
id: 'analytics-messages',
|
||||
name: i18n.translate(
|
||||
'xpack.ml.dataframe.analyticsList.analyticsDetails.tabs.analyticsMessagesLabel',
|
||||
{
|
||||
defaultMessage: 'Messages',
|
||||
}
|
||||
),
|
||||
content: <ExpandedRowMessagesPane analyticsId={item.id} />,
|
||||
},
|
||||
*/
|
||||
];
|
||||
return (
|
||||
<EuiTabbedContent
|
||||
size="s"
|
||||
tabs={tabs}
|
||||
initialSelectedTab={tabs[0]}
|
||||
onTabClick={() => {}}
|
||||
expand={false}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiDescriptionList,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPanel,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
export interface SectionItem {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
export interface SectionConfig {
|
||||
title: string;
|
||||
position: 'left' | 'right';
|
||||
items: SectionItem[];
|
||||
}
|
||||
|
||||
interface SectionProps {
|
||||
section: SectionConfig;
|
||||
}
|
||||
|
||||
export const Section: FC<SectionProps> = ({ section }) => {
|
||||
if (section.items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPanel>
|
||||
<EuiTitle size="xs">
|
||||
<span>{section.title}</span>
|
||||
</EuiTitle>
|
||||
<EuiDescriptionList compressed type="column" listItems={section.items} />
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
interface ExpandedRowDetailsPaneProps {
|
||||
sections: SectionConfig[];
|
||||
}
|
||||
|
||||
export const ExpandedRowDetailsPane: FC<ExpandedRowDetailsPaneProps> = ({ sections }) => {
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem style={{ width: '50%' }}>
|
||||
{sections
|
||||
.filter(s => s.position === 'left')
|
||||
.map(s => (
|
||||
<Fragment key={s.title}>
|
||||
<EuiSpacer size="s" />
|
||||
<Section section={s} />
|
||||
</Fragment>
|
||||
))}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem style={{ width: '50%' }}>
|
||||
{sections
|
||||
.filter(s => s.position === 'right')
|
||||
.map(s => (
|
||||
<Fragment key={s.title}>
|
||||
<EuiSpacer size="s" />
|
||||
<Section section={s} />
|
||||
</Fragment>
|
||||
))}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 {
|
||||
// @ts-ignore
|
||||
EuiCodeEditor,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
json: object;
|
||||
}
|
||||
|
||||
export const ExpandedRowJsonPane: FC<Props> = ({ json }) => {
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiCodeEditor
|
||||
value={JSON.stringify(json, null, 2)}
|
||||
readOnly={true}
|
||||
mode="json"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}> </EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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, useState } from 'react';
|
||||
|
||||
import { EuiSpacer, EuiBasicTable } from '@elastic/eui';
|
||||
// @ts-ignore
|
||||
import { formatDate } from '@elastic/eui/lib/services/format';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import theme from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import { ml } from '../../../../../services/ml_api_service';
|
||||
// @ts-ignore
|
||||
import { JobIcon } from '../../../../../components/job_message_icon';
|
||||
import { AnalyticsMessage } from '../../../../../../common/types/audit_message';
|
||||
import { useRefreshAnalyticsList } from '../../../../common';
|
||||
|
||||
const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
interface Props {
|
||||
analyticsId: string;
|
||||
}
|
||||
|
||||
export const ExpandedRowMessagesPane: FC<Props> = ({ analyticsId }) => {
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
const getMessagesFactory = () => {
|
||||
let concurrentLoads = 0;
|
||||
|
||||
return async function getMessages() {
|
||||
try {
|
||||
concurrentLoads++;
|
||||
|
||||
if (concurrentLoads > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const messagesResp = await ml.dataFrameAnalytics.getAnalyticsAuditMessages(analyticsId);
|
||||
setIsLoading(false);
|
||||
setMessages(messagesResp);
|
||||
|
||||
concurrentLoads--;
|
||||
|
||||
if (concurrentLoads > 0) {
|
||||
concurrentLoads = 0;
|
||||
getMessages();
|
||||
}
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
setErrorMessage(
|
||||
i18n.translate('xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.errorMessage', {
|
||||
defaultMessage: 'Messages could not be loaded',
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
useRefreshAnalyticsList({ onRefresh: getMessagesFactory() });
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: '',
|
||||
render: (message: AnalyticsMessage) => <JobIcon message={message} />,
|
||||
width: `${theme.euiSizeXL}px`,
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.timeLabel', {
|
||||
defaultMessage: 'Time',
|
||||
}),
|
||||
render: (message: any) => formatDate(message.timestamp, TIME_FORMAT),
|
||||
},
|
||||
{
|
||||
field: 'node_name',
|
||||
name: i18n.translate('xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.nodeLabel', {
|
||||
defaultMessage: 'Node',
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'message',
|
||||
name: i18n.translate('xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.messageLabel', {
|
||||
defaultMessage: 'Message',
|
||||
}),
|
||||
width: '50%',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiBasicTable
|
||||
items={messages}
|
||||
columns={columns}
|
||||
compressed={true}
|
||||
loading={isLoading}
|
||||
error={errorMessage}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -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 { DataFrameAnalyticsList } from './analytics_list';
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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, { useEffect } from 'react';
|
||||
|
||||
import { timefilter } from 'ui/timefilter';
|
||||
|
||||
import {
|
||||
DEFAULT_REFRESH_INTERVAL_MS,
|
||||
MINIMUM_REFRESH_INTERVAL_MS,
|
||||
} from '../../../../../../common/constants/jobs_list';
|
||||
|
||||
import { useRefreshAnalyticsList } from '../../../../common';
|
||||
|
||||
export const useRefreshInterval = (
|
||||
setBlockRefresh: React.Dispatch<React.SetStateAction<boolean>>
|
||||
) => {
|
||||
const { refresh } = useRefreshAnalyticsList();
|
||||
useEffect(() => {
|
||||
let analyticsRefreshInterval: null | number = null;
|
||||
|
||||
timefilter.disableTimeRangeSelector();
|
||||
timefilter.enableAutoRefreshSelector();
|
||||
|
||||
initAutoRefresh();
|
||||
initAutoRefreshUpdate();
|
||||
|
||||
function initAutoRefresh() {
|
||||
const { value } = timefilter.getRefreshInterval();
|
||||
if (value === 0) {
|
||||
// the auto refresher starts in an off state
|
||||
// so switch it on and set the interval to 30s
|
||||
timefilter.setRefreshInterval({
|
||||
pause: false,
|
||||
value: DEFAULT_REFRESH_INTERVAL_MS,
|
||||
});
|
||||
}
|
||||
|
||||
setAutoRefresh();
|
||||
}
|
||||
|
||||
function initAutoRefreshUpdate() {
|
||||
// update the interval if it changes
|
||||
timefilter.on('refreshIntervalUpdate', () => {
|
||||
setAutoRefresh();
|
||||
});
|
||||
}
|
||||
|
||||
function setAutoRefresh() {
|
||||
const { value, pause } = timefilter.getRefreshInterval();
|
||||
if (pause) {
|
||||
clearRefreshInterval();
|
||||
} else {
|
||||
setRefreshInterval(value);
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
|
||||
function setRefreshInterval(interval: number) {
|
||||
clearRefreshInterval();
|
||||
if (interval >= MINIMUM_REFRESH_INTERVAL_MS) {
|
||||
setBlockRefresh(false);
|
||||
const intervalId = window.setInterval(() => {
|
||||
refresh();
|
||||
}, interval);
|
||||
analyticsRefreshInterval = intervalId;
|
||||
}
|
||||
}
|
||||
|
||||
function clearRefreshInterval() {
|
||||
setBlockRefresh(true);
|
||||
if (analyticsRefreshInterval !== null) {
|
||||
window.clearInterval(analyticsRefreshInterval);
|
||||
}
|
||||
}
|
||||
|
||||
// useEffect cleanup
|
||||
return () => {
|
||||
clearRefreshInterval();
|
||||
};
|
||||
}, []); // [] as comparator makes sure this only runs once
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { EuiButton, EuiToolTip } from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
checkPermission,
|
||||
createPermissionFailureMessage,
|
||||
} from '../../../../../privilege/check_privilege';
|
||||
|
||||
import { moveToAnalyticsWizard } from '../../../../common';
|
||||
|
||||
export const CreateAnalyticsButton: FC = () => {
|
||||
const disabled =
|
||||
!checkPermission('canCreateDataFrameAnalytics') ||
|
||||
!checkPermission('canStartStopDataFrameAnalytics');
|
||||
|
||||
const button = (
|
||||
<EuiButton
|
||||
disabled={true}
|
||||
fill
|
||||
onClick={moveToAnalyticsWizard}
|
||||
iconType="plusInCircle"
|
||||
size="s"
|
||||
data-test-subj="mlDataFrameAnalyticsButtonCreate"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.dataframe.analyticsList.createDataFrameAnalyticsButton"
|
||||
defaultMessage="Create data frame analytics job"
|
||||
/>
|
||||
</EuiButton>
|
||||
);
|
||||
|
||||
if (disabled) {
|
||||
return (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={createPermissionFailureMessage('canCreateDataFrameAnalytics')}
|
||||
>
|
||||
{button}
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
||||
return button;
|
||||
};
|
|
@ -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 { CreateAnalyticsButton } from './create_analytics_button';
|
|
@ -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 { RefreshAnalyticsListButton } from './refresh_analytics_list_button';
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
interface RefreshAnalyticsListButtonProps {
|
||||
isLoading: boolean;
|
||||
onClick(): void;
|
||||
}
|
||||
export const RefreshAnalyticsListButton: FC<RefreshAnalyticsListButtonProps> = ({
|
||||
onClick,
|
||||
isLoading,
|
||||
}) => (
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="mlRefreshAnalyticsListButton"
|
||||
onClick={onClick}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.dataframe.analyticsList.refreshButtonLabel"
|
||||
defaultMessage="Refresh"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
);
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
// @ts-ignore
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml', ['react']);
|
||||
|
||||
import { I18nContext } from 'ui/i18n';
|
||||
import { Page } from './page';
|
||||
|
||||
module.directive('mlDataFrameAnalyticsManagement', () => {
|
||||
return {
|
||||
scope: {},
|
||||
restrict: 'E',
|
||||
link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => {
|
||||
ReactDOM.render(
|
||||
<I18nContext>
|
||||
<Page />
|
||||
</I18nContext>,
|
||||
element[0]
|
||||
);
|
||||
|
||||
element.on('$destroy', () => {
|
||||
ReactDOM.unmountComponentAtNode(element[0]);
|
||||
scope.$destroy();
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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, useState } from 'react';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
EuiBetaBadge,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPageContentBody,
|
||||
EuiPageContentHeader,
|
||||
EuiPageContentHeaderSection,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { NavigationMenu } from '../../../components/navigation_menu/navigation_menu';
|
||||
import { useRefreshAnalyticsList } from '../../common';
|
||||
import { CreateAnalyticsButton } from './components/create_analytics_button';
|
||||
import { DataFrameAnalyticsList } from './components/analytics_list';
|
||||
import { RefreshAnalyticsListButton } from './components/refresh_analytics_list_button';
|
||||
|
||||
export const Page: FC = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { refresh } = useRefreshAnalyticsList({ isLoading: setIsLoading });
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<NavigationMenu tabId="data_frame_analytics" />
|
||||
<EuiPage data-test-subj="mlPageDataFrameAnalytics">
|
||||
<EuiPageBody>
|
||||
<EuiPageContentHeader>
|
||||
<EuiPageContentHeaderSection>
|
||||
<EuiTitle>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.dataframe.analyticsList.title"
|
||||
defaultMessage="Analytics jobs"
|
||||
/>
|
||||
<span> </span>
|
||||
<EuiBetaBadge
|
||||
label={i18n.translate(
|
||||
'xpack.ml.dataframe.analyticsList.experimentalBadgeLabel',
|
||||
{
|
||||
defaultMessage: 'Experimental',
|
||||
}
|
||||
)}
|
||||
tooltipContent={i18n.translate(
|
||||
'xpack.ml.dataframe.analyticsList.experimentalBadgeTooltipContent',
|
||||
{
|
||||
defaultMessage: `Data frame analytics are an experimental feature. We'd love to hear your feedback.`,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiPageContentHeaderSection>
|
||||
<EuiPageContentHeaderSection>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
{/* grow={false} fixes IE11 issue with nested flex */}
|
||||
<EuiFlexItem grow={false}>
|
||||
{<RefreshAnalyticsListButton onClick={refresh} isLoading={isLoading} />}
|
||||
</EuiFlexItem>
|
||||
{/* grow={false} fixes IE11 issue with nested flex */}
|
||||
<EuiFlexItem grow={false}>
|
||||
<CreateAnalyticsButton />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageContentHeaderSection>
|
||||
</EuiPageContentHeader>
|
||||
<EuiPageContentBody>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiPanel>
|
||||
<DataFrameAnalyticsList />
|
||||
</EuiPanel>
|
||||
</EuiPageContentBody>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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
|
||||
import { checkFullLicense } from '../../../license/check_license';
|
||||
// @ts-ignore
|
||||
import { checkGetJobsPrivilege } from '../../../privilege/check_privilege';
|
||||
// @ts-ignore
|
||||
import { loadIndexPatterns } from '../../../util/index_utils';
|
||||
// @ts-ignore
|
||||
import { getDataFrameAnalyticsBreadcrumbs } from '../../breadcrumbs';
|
||||
|
||||
const template = `<ml-data-frame-analytics-management />`;
|
||||
|
||||
uiRoutes.when('/data_frame_analytics/?', {
|
||||
template,
|
||||
k7Breadcrumbs: getDataFrameAnalyticsBreadcrumbs,
|
||||
resolve: {
|
||||
CheckLicense: checkFullLicense,
|
||||
privileges: checkGetJobsPrivilege,
|
||||
indexPatterns: loadIndexPatterns,
|
||||
},
|
||||
});
|
|
@ -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 { i18n } from '@kbn/i18n';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { ml } from '../../../../../services/ml_api_service';
|
||||
|
||||
import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../../common';
|
||||
|
||||
import {
|
||||
DATA_FRAME_TASK_STATE,
|
||||
DataFrameAnalyticsListRow,
|
||||
} from '../../components/analytics_list/common';
|
||||
|
||||
export const deleteAnalytics = async (d: DataFrameAnalyticsListRow) => {
|
||||
try {
|
||||
if (d.stats.state === DATA_FRAME_TASK_STATE.FAILED) {
|
||||
await ml.dataFrameAnalytics.stopDataFrameAnalytics(
|
||||
d.config.id,
|
||||
d.stats.state === DATA_FRAME_TASK_STATE.FAILED,
|
||||
true
|
||||
);
|
||||
}
|
||||
await ml.dataFrameAnalytics.deleteDataFrameAnalytics(d.config.id);
|
||||
toastNotifications.addSuccess(
|
||||
i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsSuccessMessage', {
|
||||
defaultMessage: 'Data frame analytics {analyticsId} delete request acknowledged.',
|
||||
values: { analyticsId: d.config.id },
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage', {
|
||||
defaultMessage:
|
||||
'An error occurred deleting the data frame analytics {analyticsId}: {error}',
|
||||
values: { analyticsId: d.config.id, error: JSON.stringify(e) },
|
||||
})
|
||||
);
|
||||
}
|
||||
refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.REFRESH);
|
||||
};
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* 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 { ml } from '../../../../../services/ml_api_service';
|
||||
import {
|
||||
DataFrameAnalyticsOutlierConfig,
|
||||
refreshAnalyticsList$,
|
||||
REFRESH_ANALYTICS_LIST_STATE,
|
||||
} from '../../../../common';
|
||||
|
||||
import {
|
||||
DataFrameAnalyticsListRow,
|
||||
DataFrameAnalyticsStats,
|
||||
DATA_FRAME_MODE,
|
||||
isDataFrameAnalyticsStats,
|
||||
} from '../../components/analytics_list/common';
|
||||
|
||||
interface GetDataFrameAnalyticsResponse {
|
||||
count: number;
|
||||
data_frame_analytics: DataFrameAnalyticsOutlierConfig[];
|
||||
}
|
||||
|
||||
interface GetDataFrameAnalyticsStatsResponseOk {
|
||||
node_failures?: object;
|
||||
count: number;
|
||||
data_frame_analytics: DataFrameAnalyticsStats[];
|
||||
}
|
||||
|
||||
const isGetDataFrameAnalyticsStatsResponseOk = (
|
||||
arg: any
|
||||
): arg is GetDataFrameAnalyticsStatsResponseOk => {
|
||||
return (
|
||||
{}.hasOwnProperty.call(arg, 'count') &&
|
||||
{}.hasOwnProperty.call(arg, 'data_frame_analytics') &&
|
||||
Array.isArray(arg.data_frame_analytics)
|
||||
);
|
||||
};
|
||||
|
||||
interface GetDataFrameAnalyticsStatsResponseError {
|
||||
statusCode: number;
|
||||
error: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
type GetDataFrameAnalyticsStatsResponse =
|
||||
| GetDataFrameAnalyticsStatsResponseOk
|
||||
| GetDataFrameAnalyticsStatsResponseError;
|
||||
|
||||
export type GetAnalytics = (forceRefresh?: boolean) => void;
|
||||
|
||||
export const getAnalyticsFactory = (
|
||||
setAnalytics: React.Dispatch<React.SetStateAction<DataFrameAnalyticsListRow[]>>,
|
||||
setErrorMessage: React.Dispatch<
|
||||
React.SetStateAction<GetDataFrameAnalyticsStatsResponseError | undefined>
|
||||
>,
|
||||
setIsInitialized: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
blockRefresh: boolean
|
||||
): GetAnalytics => {
|
||||
let concurrentLoads = 0;
|
||||
|
||||
const getAnalytics = async (forceRefresh = false) => {
|
||||
if (forceRefresh === true || blockRefresh === false) {
|
||||
refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.LOADING);
|
||||
concurrentLoads++;
|
||||
|
||||
if (concurrentLoads > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const analyticsConfigs: GetDataFrameAnalyticsResponse = await ml.dataFrameAnalytics.getDataFrameAnalytics();
|
||||
const analyticsStats: GetDataFrameAnalyticsStatsResponse = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats();
|
||||
|
||||
const tableRows = analyticsConfigs.data_frame_analytics.reduce(
|
||||
(reducedtableRows, config) => {
|
||||
const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats)
|
||||
? analyticsStats.data_frame_analytics.find(d => config.id === d.id)
|
||||
: undefined;
|
||||
|
||||
// A newly created analytics job might not have corresponding stats yet.
|
||||
// If that's the case we just skip the job and don't add it to the analytics jobs list yet.
|
||||
if (!isDataFrameAnalyticsStats(stats)) {
|
||||
return reducedtableRows;
|
||||
}
|
||||
|
||||
// Table with expandable rows requires `id` on the outer most level
|
||||
reducedtableRows.push({
|
||||
config,
|
||||
id: config.id,
|
||||
checkpointing: {},
|
||||
mode: DATA_FRAME_MODE.BATCH,
|
||||
stats,
|
||||
});
|
||||
return reducedtableRows;
|
||||
},
|
||||
[] as DataFrameAnalyticsListRow[]
|
||||
);
|
||||
|
||||
setAnalytics(tableRows);
|
||||
setErrorMessage(undefined);
|
||||
setIsInitialized(true);
|
||||
refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.IDLE);
|
||||
} catch (e) {
|
||||
// An error is followed immediately by setting the state to idle.
|
||||
// This way we're able to treat ERROR as a one-time-event like REFRESH.
|
||||
refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.ERROR);
|
||||
refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.IDLE);
|
||||
setAnalytics([]);
|
||||
setErrorMessage(e);
|
||||
setIsInitialized(true);
|
||||
}
|
||||
concurrentLoads--;
|
||||
|
||||
if (concurrentLoads > 0) {
|
||||
concurrentLoads = 0;
|
||||
getAnalytics(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return getAnalytics;
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 { getAnalyticsFactory } from './get_analytics';
|
||||
export { deleteAnalytics } from './delete_analytics';
|
||||
export { startAnalytics } from './start_analytics';
|
||||
export { stopAnalytics } from './stop_analytics';
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { toastNotifications } from 'ui/notify';
|
||||
import { ml } from '../../../../../services/ml_api_service';
|
||||
|
||||
import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../../common';
|
||||
|
||||
import { DataFrameAnalyticsListRow } from '../../components/analytics_list/common';
|
||||
|
||||
export const startAnalytics = async (d: DataFrameAnalyticsListRow) => {
|
||||
try {
|
||||
await ml.dataFrameAnalytics.startDataFrameAnalytics(d.config.id);
|
||||
toastNotifications.addSuccess(
|
||||
i18n.translate('xpack.ml.dataframe.analyticsList.startAnalyticsSuccessMessage', {
|
||||
defaultMessage: 'Data frame analytics {analyticsId} start request acknowledged.',
|
||||
values: { analyticsId: d.config.id },
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate('xpack.ml.dataframe.analyticsList.startAnalyticsErrorMessage', {
|
||||
defaultMessage:
|
||||
'An error occurred starting the data frame analytics {analyticsId}: {error}',
|
||||
values: { analyticsId: d.config.id, error: JSON.stringify(e) },
|
||||
})
|
||||
);
|
||||
}
|
||||
refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.REFRESH);
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { toastNotifications } from 'ui/notify';
|
||||
import { ml } from '../../../../../services/ml_api_service';
|
||||
|
||||
import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../../common';
|
||||
|
||||
import {
|
||||
DATA_FRAME_TASK_STATE,
|
||||
DataFrameAnalyticsListRow,
|
||||
} from '../../components/analytics_list/common';
|
||||
|
||||
export const stopAnalytics = async (d: DataFrameAnalyticsListRow) => {
|
||||
try {
|
||||
await ml.dataFrameAnalytics.stopDataFrameAnalytics(
|
||||
d.config.id,
|
||||
d.stats.state === DATA_FRAME_TASK_STATE.FAILED,
|
||||
true
|
||||
);
|
||||
toastNotifications.addSuccess(
|
||||
i18n.translate('xpack.ml.dataframe.analyticsList.stopAnalyticsSuccessMessage', {
|
||||
defaultMessage: 'Data frame analytics {analyticsId} stop request acknowledged.',
|
||||
values: { analyticsId: d.config.id },
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate('xpack.ml.dataframe.analyticsList.stopAnalyticsErrorMessage', {
|
||||
defaultMessage:
|
||||
'An error occurred stopping the data frame analytics {analyticsId}: {error}',
|
||||
values: { analyticsId: d.config.id, error: JSON.stringify(e) },
|
||||
})
|
||||
);
|
||||
}
|
||||
refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.REFRESH);
|
||||
};
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
// Sub applications
|
||||
@import 'data_frame/index';
|
||||
@import 'data_frame_analytics/index';
|
||||
@import 'data_visualizer/index';
|
||||
@import 'datavisualizer/index';
|
||||
@import 'explorer/index'; // SASSTODO: This file needs to be rewritten
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 chrome from 'ui/chrome';
|
||||
|
||||
import { http } from '../../services/http_service';
|
||||
|
||||
const basePath = chrome.addBasePath('/api/ml');
|
||||
|
||||
export const dataFrameAnalytics = {
|
||||
getDataFrameAnalytics(analyticsId) {
|
||||
const analyticsIdString = analyticsId !== undefined ? `/${analyticsId}` : '';
|
||||
return http({
|
||||
url: `${basePath}/data_frame/analytics${analyticsIdString}`,
|
||||
method: 'GET'
|
||||
});
|
||||
},
|
||||
getDataFrameAnalyticsStats(analyticsId) {
|
||||
if (analyticsId !== undefined) {
|
||||
return http({
|
||||
url: `${basePath}/data_frame/analytics/${analyticsId}/_stats`,
|
||||
method: 'GET'
|
||||
});
|
||||
}
|
||||
|
||||
return http({
|
||||
url: `${basePath}/data_frame/analytics/_stats`,
|
||||
method: 'GET'
|
||||
});
|
||||
},
|
||||
createDataFrameAnalytics(analyticsId, analyticsConfig) {
|
||||
return http({
|
||||
url: `${basePath}/data_frame/analytics/${analyticsId}`,
|
||||
method: 'PUT',
|
||||
data: analyticsConfig
|
||||
});
|
||||
},
|
||||
deleteDataFrameAnalytics(analyticsId) {
|
||||
return http({
|
||||
url: `${basePath}/data_frame/analytics/${analyticsId}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
},
|
||||
startDataFrameAnalytics(analyticsId) {
|
||||
return http({
|
||||
url: `${basePath}/data_frame/analytics/${analyticsId}/_start`,
|
||||
method: 'POST',
|
||||
});
|
||||
},
|
||||
stopDataFrameAnalytics(analyticsId, force = false) {
|
||||
return http({
|
||||
url: `${basePath}/data_frame/analytics/${analyticsId}/_stop?force=${force}`,
|
||||
method: 'POST',
|
||||
});
|
||||
},
|
||||
getAnalyticsAuditMessages(analyticsId) {
|
||||
return http({
|
||||
url: `${basePath}/data_frame/analytics/${analyticsId}/messages`,
|
||||
method: 'GET',
|
||||
});
|
||||
},
|
||||
};
|
|
@ -29,6 +29,20 @@ declare interface Ml {
|
|||
indexAnnotation(annotation: Annotation): Promise<object>;
|
||||
};
|
||||
|
||||
dataFrameAnalytics: {
|
||||
getDataFrameAnalytics(analyticsId?: string): Promise<any>;
|
||||
getDataFrameAnalyticsStats(analyticsId?: string): Promise<any>;
|
||||
createDataFrameAnalytics(analyticsId: string, analyticsConfig: any): Promise<any>;
|
||||
deleteDataFrameAnalytics(analyticsId: string): Promise<any>;
|
||||
startDataFrameAnalytics(analyticsId: string): Promise<any>;
|
||||
stopDataFrameAnalytics(
|
||||
analyticsId: string,
|
||||
force?: boolean,
|
||||
waitForCompletion?: boolean
|
||||
): Promise<any>;
|
||||
getAnalyticsAuditMessages(analyticsId: string): Promise<any>;
|
||||
};
|
||||
|
||||
dataFrame: {
|
||||
getDataFrameTransforms(jobId?: string): Promise<any>;
|
||||
getDataFrameTransformsStats(jobId?: string): Promise<any>;
|
||||
|
|
|
@ -13,6 +13,7 @@ import { http } from '../../services/http_service';
|
|||
|
||||
import { annotations } from './annotations';
|
||||
import { dataFrame } from './data_frame';
|
||||
import { dataFrameAnalytics } from './data_frame_analytics';
|
||||
import { filters } from './filters';
|
||||
import { results } from './results';
|
||||
import { jobs } from './jobs';
|
||||
|
@ -443,6 +444,7 @@ export const ml = {
|
|||
|
||||
annotations,
|
||||
dataFrame,
|
||||
dataFrameAnalytics,
|
||||
filters,
|
||||
results,
|
||||
jobs,
|
||||
|
|
|
@ -105,6 +105,106 @@ export const elasticsearchJsPlugin = (Client, config, components) => {
|
|||
method: 'POST'
|
||||
});
|
||||
|
||||
// Currently the endpoint uses a default size of 100 unless a size is supplied.
|
||||
// So until paging is supported in the UI, explicitly supply a size of 1000
|
||||
// to match the max number of docs that the endpoint can return.
|
||||
ml.getDataFrameAnalytics = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_ml/data_frame/analytics/<%=analyticsId%>',
|
||||
req: {
|
||||
analyticsId: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
fmt: '/_ml/data_frame/analytics/_all?size=1000',
|
||||
}
|
||||
],
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
ml.getDataFrameAnalyticsStats = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_ml/data_frame/analytics/<%=analyticsId%>/_stats',
|
||||
req: {
|
||||
analyticsId: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
// Currently the endpoint uses a default size of 100 unless a size is supplied.
|
||||
// So until paging is supported in the UI, explicitly supply a size of 1000
|
||||
// to match the max number of docs that the endpoint can return.
|
||||
fmt: '/_ml/data_frame/analytics/_all/_stats?size=1000',
|
||||
}
|
||||
],
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
ml.createDataFrameAnalytics = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_ml/data_frame/analytics/<%=analyticsId%>',
|
||||
req: {
|
||||
analyticsId: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
needBody: true,
|
||||
method: 'PUT'
|
||||
});
|
||||
|
||||
ml.deleteDataFrameAnalytics = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_ml/data_frame/analytics/<%=analyticsId%>',
|
||||
req: {
|
||||
analyticsId: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
ml.startDataFrameAnalytics = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_ml/data_frame/analytics/<%=analyticsId%>/_start',
|
||||
req: {
|
||||
analyticsId: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
ml.stopDataFrameAnalytics = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_ml/data_frame/analytics/<%=analyticsId%>/_stop?&force=<%=force%>',
|
||||
req: {
|
||||
analyticsId: {
|
||||
type: 'string'
|
||||
},
|
||||
force: {
|
||||
type: 'boolean'
|
||||
},
|
||||
}
|
||||
}
|
||||
],
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
// Currently the endpoint uses a default size of 100 unless a size is supplied.
|
||||
// So until paging is supported in the UI, explicitly supply a size of 1000
|
||||
// to match the max number of docs that the endpoint can return.
|
||||
|
|
|
@ -104,7 +104,7 @@ describe('check_privileges', () => {
|
|||
);
|
||||
const { capabilities } = await getPrivileges();
|
||||
const count = Object.keys(capabilities).length;
|
||||
expect(count).toBe(23);
|
||||
expect(count).toBe(27);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@ -143,6 +143,10 @@ describe('check_privileges', () => {
|
|||
expect(capabilities.canPreviewDataFrame).toBe(false);
|
||||
expect(capabilities.canCreateDataFrame).toBe(false);
|
||||
expect(capabilities.canStartStopDataFrame).toBe(false);
|
||||
expect(capabilities.canGetDataFrameAnalytics).toBe(true);
|
||||
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canStartStopDataFrameAnalytics).toBe(false);
|
||||
done();
|
||||
});
|
||||
|
||||
|
@ -179,6 +183,10 @@ describe('check_privileges', () => {
|
|||
expect(capabilities.canPreviewDataFrame).toBe(true);
|
||||
expect(capabilities.canCreateDataFrame).toBe(true);
|
||||
expect(capabilities.canStartStopDataFrame).toBe(true);
|
||||
expect(capabilities.canGetDataFrameAnalytics).toBe(true);
|
||||
expect(capabilities.canDeleteDataFrameAnalytics).toBe(true);
|
||||
expect(capabilities.canCreateDataFrameAnalytics).toBe(true);
|
||||
expect(capabilities.canStartStopDataFrameAnalytics).toBe(true);
|
||||
done();
|
||||
});
|
||||
|
||||
|
@ -215,6 +223,10 @@ describe('check_privileges', () => {
|
|||
expect(capabilities.canPreviewDataFrame).toBe(false);
|
||||
expect(capabilities.canCreateDataFrame).toBe(false);
|
||||
expect(capabilities.canStartStopDataFrame).toBe(false);
|
||||
expect(capabilities.canGetDataFrameAnalytics).toBe(true);
|
||||
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canStartStopDataFrameAnalytics).toBe(false);
|
||||
done();
|
||||
});
|
||||
|
||||
|
@ -251,6 +263,10 @@ describe('check_privileges', () => {
|
|||
expect(capabilities.canPreviewDataFrame).toBe(false);
|
||||
expect(capabilities.canCreateDataFrame).toBe(false);
|
||||
expect(capabilities.canStartStopDataFrame).toBe(false);
|
||||
expect(capabilities.canGetDataFrameAnalytics).toBe(true);
|
||||
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canStartStopDataFrameAnalytics).toBe(false);
|
||||
done();
|
||||
});
|
||||
|
||||
|
@ -287,6 +303,10 @@ describe('check_privileges', () => {
|
|||
expect(capabilities.canPreviewDataFrame).toBe(false);
|
||||
expect(capabilities.canCreateDataFrame).toBe(false);
|
||||
expect(capabilities.canStartStopDataFrame).toBe(false);
|
||||
expect(capabilities.canGetDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canStartStopDataFrameAnalytics).toBe(false);
|
||||
done();
|
||||
});
|
||||
|
||||
|
@ -323,6 +343,10 @@ describe('check_privileges', () => {
|
|||
expect(capabilities.canPreviewDataFrame).toBe(true);
|
||||
expect(capabilities.canCreateDataFrame).toBe(true);
|
||||
expect(capabilities.canStartStopDataFrame).toBe(true);
|
||||
expect(capabilities.canGetDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canStartStopDataFrameAnalytics).toBe(false);
|
||||
done();
|
||||
});
|
||||
|
||||
|
@ -359,6 +383,10 @@ describe('check_privileges', () => {
|
|||
expect(capabilities.canPreviewDataFrame).toBe(false);
|
||||
expect(capabilities.canCreateDataFrame).toBe(false);
|
||||
expect(capabilities.canStartStopDataFrame).toBe(false);
|
||||
expect(capabilities.canGetDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canStartStopDataFrameAnalytics).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@ -397,6 +425,10 @@ describe('check_privileges', () => {
|
|||
expect(capabilities.canPreviewDataFrame).toBe(true);
|
||||
expect(capabilities.canCreateDataFrame).toBe(true);
|
||||
expect(capabilities.canStartStopDataFrame).toBe(true);
|
||||
expect(capabilities.canGetDataFrameAnalytics).toBe(true);
|
||||
expect(capabilities.canDeleteDataFrameAnalytics).toBe(true);
|
||||
expect(capabilities.canCreateDataFrameAnalytics).toBe(true);
|
||||
expect(capabilities.canStartStopDataFrameAnalytics).toBe(true);
|
||||
done();
|
||||
});
|
||||
|
||||
|
@ -433,6 +465,10 @@ describe('check_privileges', () => {
|
|||
expect(capabilities.canPreviewDataFrame).toBe(false);
|
||||
expect(capabilities.canCreateDataFrame).toBe(false);
|
||||
expect(capabilities.canStartStopDataFrame).toBe(false);
|
||||
expect(capabilities.canGetDataFrameAnalytics).toBe(true);
|
||||
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canStartStopDataFrameAnalytics).toBe(false);
|
||||
done();
|
||||
});
|
||||
|
||||
|
@ -469,6 +505,10 @@ describe('check_privileges', () => {
|
|||
expect(capabilities.canPreviewDataFrame).toBe(false);
|
||||
expect(capabilities.canCreateDataFrame).toBe(false);
|
||||
expect(capabilities.canStartStopDataFrame).toBe(false);
|
||||
expect(capabilities.canGetDataFrameAnalytics).toBe(true);
|
||||
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canStartStopDataFrameAnalytics).toBe(false);
|
||||
done();
|
||||
});
|
||||
|
||||
|
@ -505,6 +545,10 @@ describe('check_privileges', () => {
|
|||
expect(capabilities.canPreviewDataFrame).toBe(true);
|
||||
expect(capabilities.canCreateDataFrame).toBe(true);
|
||||
expect(capabilities.canStartStopDataFrame).toBe(true);
|
||||
expect(capabilities.canGetDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canStartStopDataFrameAnalytics).toBe(false);
|
||||
done();
|
||||
});
|
||||
|
||||
|
@ -541,6 +585,10 @@ describe('check_privileges', () => {
|
|||
expect(capabilities.canPreviewDataFrame).toBe(true);
|
||||
expect(capabilities.canCreateDataFrame).toBe(true);
|
||||
expect(capabilities.canStartStopDataFrame).toBe(true);
|
||||
expect(capabilities.canGetDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canStartStopDataFrameAnalytics).toBe(false);
|
||||
done();
|
||||
});
|
||||
|
||||
|
@ -577,6 +625,10 @@ describe('check_privileges', () => {
|
|||
expect(capabilities.canPreviewDataFrame).toBe(false);
|
||||
expect(capabilities.canCreateDataFrame).toBe(false);
|
||||
expect(capabilities.canStartStopDataFrame).toBe(false);
|
||||
expect(capabilities.canGetDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
|
||||
expect(capabilities.canStartStopDataFrameAnalytics).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -128,13 +128,22 @@ function setFullGettingPrivileges(
|
|||
privileges.canFindFileStructure = true;
|
||||
}
|
||||
|
||||
// Data Frames
|
||||
// Data Frame Transforms
|
||||
if (
|
||||
forceTrue ||
|
||||
(cluster['cluster:monitor/data_frame/get'] && cluster['cluster:monitor/data_frame/stats/get'])
|
||||
) {
|
||||
privileges.canGetDataFrame = true;
|
||||
}
|
||||
|
||||
// Data Frame Analytics
|
||||
if (
|
||||
forceTrue ||
|
||||
(cluster['cluster:monitor/xpack/ml/job/get'] &&
|
||||
cluster['cluster:monitor/xpack/ml/job/stats/get'])
|
||||
) {
|
||||
privileges.canGetDataFrameAnalytics = true;
|
||||
}
|
||||
}
|
||||
|
||||
function setFullActionPrivileges(
|
||||
|
@ -224,7 +233,7 @@ function setFullActionPrivileges(
|
|||
privileges.canDeleteFilter = true;
|
||||
}
|
||||
|
||||
// Data Frames
|
||||
// Data Frame Transforms
|
||||
if (forceTrue || cluster['cluster:admin/data_frame/put']) {
|
||||
privileges.canCreateDataFrame = true;
|
||||
}
|
||||
|
@ -245,6 +254,33 @@ function setFullActionPrivileges(
|
|||
) {
|
||||
privileges.canStartStopDataFrame = true;
|
||||
}
|
||||
|
||||
// Data Frame Analytics
|
||||
if (
|
||||
forceTrue ||
|
||||
(cluster['cluster:admin/xpack/ml/job/put'] &&
|
||||
cluster['cluster:admin/xpack/ml/job/open'] &&
|
||||
cluster['cluster:admin/xpack/ml/datafeeds/put'])
|
||||
) {
|
||||
privileges.canCreateDataFrameAnalytics = true;
|
||||
}
|
||||
|
||||
if (
|
||||
forceTrue ||
|
||||
(cluster['cluster:admin/xpack/ml/job/delete'] &&
|
||||
cluster['cluster:admin/xpack/ml/datafeeds/delete'])
|
||||
) {
|
||||
privileges.canDeleteDataFrameAnalytics = true;
|
||||
}
|
||||
|
||||
if (
|
||||
forceTrue ||
|
||||
(cluster['cluster:admin/xpack/ml/job/open'] &&
|
||||
cluster['cluster:admin/xpack/ml/datafeeds/start'] &&
|
||||
cluster['cluster:admin/xpack/ml/datafeeds/stop'])
|
||||
) {
|
||||
privileges.canStartStopDataFrameAnalytics = true;
|
||||
}
|
||||
}
|
||||
|
||||
function setBasicGettingPrivileges(
|
||||
|
@ -257,7 +293,7 @@ function setBasicGettingPrivileges(
|
|||
privileges.canFindFileStructure = true;
|
||||
}
|
||||
|
||||
// Data Frames
|
||||
// Data Frame Transforms
|
||||
if (
|
||||
forceTrue ||
|
||||
(cluster['cluster:monitor/data_frame/get'] && cluster['cluster:monitor/data_frame/stats/get'])
|
||||
|
@ -271,7 +307,7 @@ function setBasicActionPrivileges(
|
|||
privileges: Privileges,
|
||||
forceTrue = false
|
||||
) {
|
||||
// Data Frames
|
||||
// Data Frame Transforms
|
||||
if (forceTrue || cluster['cluster:admin/data_frame/put']) {
|
||||
privileges.canCreateDataFrame = true;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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 { ML_DF_NOTIFICATION_INDEX_PATTERN } from '../../../common/constants/index_patterns';
|
||||
import { callWithRequestType } from '../../../common/types/kibana';
|
||||
import { AnalyticsMessage } from '../../../common/types/audit_message';
|
||||
|
||||
const SIZE = 50;
|
||||
|
||||
interface Message {
|
||||
_index: string;
|
||||
_type: string;
|
||||
_id: string;
|
||||
_score: null | number;
|
||||
_source: AnalyticsMessage;
|
||||
sort?: any;
|
||||
}
|
||||
|
||||
interface BoolQuery {
|
||||
bool: { [key: string]: any };
|
||||
}
|
||||
|
||||
export function analyticsAuditMessagesProvider(callWithRequest: callWithRequestType) {
|
||||
// search for audit messages,
|
||||
// analyticsId is optional. without it, all analytics will be listed.
|
||||
async function getAnalyticsAuditMessages(analyticsId: string) {
|
||||
const query: BoolQuery = {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
term: {
|
||||
level: 'activity',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// if no analyticsId specified, load all of the messages
|
||||
if (analyticsId !== undefined) {
|
||||
query.bool.filter.push({
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
term: {
|
||||
analytics_id: '', // catch system messages
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
analytics_id: analyticsId, // messages for specified analyticsId
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await callWithRequest('search', {
|
||||
index: ML_DF_NOTIFICATION_INDEX_PATTERN,
|
||||
ignore_unavailable: true,
|
||||
rest_total_hits_as_int: true,
|
||||
size: SIZE,
|
||||
body: {
|
||||
sort: [{ timestamp: { order: 'desc' } }, { analytics_id: { order: 'asc' } }],
|
||||
query,
|
||||
},
|
||||
});
|
||||
|
||||
let messages = [];
|
||||
if (resp.hits.total !== 0) {
|
||||
messages = resp.hits.hits.map((hit: Message) => hit._source);
|
||||
messages.reverse();
|
||||
}
|
||||
return messages;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getAnalyticsAuditMessages,
|
||||
};
|
||||
}
|
|
@ -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 { analyticsAuditMessagesProvider } from './analytics_audit_messages';
|
|
@ -39,6 +39,8 @@ import { systemRoutes } from '../routes/system';
|
|||
// @ts-ignore: could not find declaration file for module
|
||||
import { dataFrameRoutes } from '../routes/data_frame';
|
||||
// @ts-ignore: could not find declaration file for module
|
||||
import { dataFrameAnalyticsRoutes } from '../routes/data_frame_analytics';
|
||||
// @ts-ignore: could not find declaration file for module
|
||||
import { dataRecognizer } from '../routes/modules';
|
||||
// @ts-ignore: could not find declaration file for module
|
||||
import { dataVisualizerRoutes } from '../routes/data_visualizer';
|
||||
|
@ -220,6 +222,7 @@ export class Plugin {
|
|||
jobRoutes(routeInitializationDeps);
|
||||
dataFeedRoutes(routeInitializationDeps);
|
||||
dataFrameRoutes(routeInitializationDeps);
|
||||
dataFrameAnalyticsRoutes(routeInitializationDeps);
|
||||
indicesRoutes(routeInitializationDeps);
|
||||
jobValidationRoutes(extendedRouteInitializationDeps);
|
||||
notificationRoutes(routeInitializationDeps);
|
||||
|
|
148
x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.js
Normal file
148
x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.js
Normal file
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* 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 { callWithRequestFactory } from '../client/call_with_request_factory';
|
||||
import { wrapError } from '../client/errors';
|
||||
import { analyticsAuditMessagesProvider } from '../models/data_frame_analytics/analytics_audit_messages';
|
||||
|
||||
export function dataFrameAnalyticsRoutes({ commonRouteConfig, elasticsearchPlugin, route }) {
|
||||
|
||||
route({
|
||||
method: 'GET',
|
||||
path: '/api/ml/data_frame/analytics',
|
||||
handler(request) {
|
||||
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
|
||||
return callWithRequest('ml.getDataFrameAnalytics')
|
||||
.catch(resp => wrapError(resp));
|
||||
},
|
||||
config: {
|
||||
...commonRouteConfig
|
||||
}
|
||||
});
|
||||
|
||||
route({
|
||||
method: 'GET',
|
||||
path: '/api/ml/data_frame/analytics/{analyticsId}',
|
||||
handler(request) {
|
||||
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
|
||||
const { analyticsId } = request.params;
|
||||
return callWithRequest('ml.getDataFrameAnalytics', { analyticsId })
|
||||
.catch(resp => wrapError(resp));
|
||||
},
|
||||
config: {
|
||||
...commonRouteConfig
|
||||
}
|
||||
});
|
||||
|
||||
route({
|
||||
method: 'GET',
|
||||
path: '/api/ml/data_frame/analytics/_stats',
|
||||
handler(request) {
|
||||
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
|
||||
return callWithRequest('ml.getDataFrameAnalyticsStats')
|
||||
.catch(resp => wrapError(resp));
|
||||
},
|
||||
config: {
|
||||
...commonRouteConfig
|
||||
}
|
||||
});
|
||||
|
||||
route({
|
||||
method: 'GET',
|
||||
path: '/api/ml/data_frame/analytics/{analyticsId}/_stats',
|
||||
handler(request) {
|
||||
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
|
||||
const { analyticsId } = request.params;
|
||||
return callWithRequest('ml.getDataFrameAnalyticsStats', { analyticsId })
|
||||
.catch(resp => wrapError(resp));
|
||||
},
|
||||
config: {
|
||||
...commonRouteConfig
|
||||
}
|
||||
});
|
||||
|
||||
route({
|
||||
method: 'PUT',
|
||||
path: '/api/ml/data_frame/analytics/{analyticsId}',
|
||||
handler(request) {
|
||||
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
|
||||
const { analyticsId } = request.params;
|
||||
return callWithRequest('ml.createDataFrameAnalytics', { body: request.payload, analyticsId })
|
||||
.catch(resp => wrapError(resp));
|
||||
},
|
||||
config: {
|
||||
...commonRouteConfig
|
||||
}
|
||||
});
|
||||
|
||||
route({
|
||||
method: 'DELETE',
|
||||
path: '/api/ml/data_frame/analytics/{analyticsId}',
|
||||
handler(request) {
|
||||
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
|
||||
const { analyticsId } = request.params;
|
||||
return callWithRequest('ml.deleteDataFrameAnalytics', { analyticsId })
|
||||
.catch(resp => wrapError(resp));
|
||||
},
|
||||
config: {
|
||||
...commonRouteConfig
|
||||
}
|
||||
});
|
||||
|
||||
route({
|
||||
method: 'POST',
|
||||
path: '/api/ml/data_frame/analytics/{analyticsId}/_start',
|
||||
handler(request) {
|
||||
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
|
||||
const options = {
|
||||
analyticsId: request.params.analyticsId
|
||||
};
|
||||
|
||||
return callWithRequest('ml.startDataFrameAnalytics', options)
|
||||
.catch(resp => wrapError(resp));
|
||||
},
|
||||
config: {
|
||||
...commonRouteConfig
|
||||
}
|
||||
});
|
||||
|
||||
route({
|
||||
method: 'POST',
|
||||
path: '/api/ml/data_frame/analytics/{analyticsId}/_stop',
|
||||
handler(request) {
|
||||
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
|
||||
const options = {
|
||||
analyticsId: request.params.analyticsId
|
||||
};
|
||||
|
||||
if (request.query.force !== undefined) {
|
||||
options.force = request.query.force;
|
||||
}
|
||||
|
||||
return callWithRequest('ml.stopDataFrameAnalytics', options)
|
||||
.catch(resp => wrapError(resp));
|
||||
},
|
||||
config: {
|
||||
...commonRouteConfig
|
||||
}
|
||||
});
|
||||
|
||||
route({
|
||||
method: 'GET',
|
||||
path: '/api/ml/data_frame/analytics/{analyticsId}/messages',
|
||||
handler(request) {
|
||||
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
|
||||
const { getAnalyticsAuditMessages } = analyticsAuditMessagesProvider(callWithRequest);
|
||||
const { analyticsId } = request.params;
|
||||
return getAnalyticsAuditMessages(analyticsId)
|
||||
.catch(resp => wrapError(resp));
|
||||
},
|
||||
config: {
|
||||
...commonRouteConfig
|
||||
}
|
||||
});
|
||||
|
||||
}
|
|
@ -31,6 +31,10 @@ export const emptyMlCapabilities: MlCapabilities = {
|
|||
canPreviewDataFrame: false,
|
||||
canCreateDataFrame: false,
|
||||
canStartStopDataFrame: false,
|
||||
canGetDataFrameAnalytics: false,
|
||||
canDeleteDataFrameAnalytics: false,
|
||||
canCreateDataFrameAnalytics: false,
|
||||
canStartStopDataFrameAnalytics: false,
|
||||
},
|
||||
isPlatinumOrTrialLicense: false,
|
||||
mlFeatureEnabledInSpace: false,
|
||||
|
|
|
@ -125,6 +125,10 @@ export interface MlCapabilities {
|
|||
canPreviewDataFrame: boolean;
|
||||
canCreateDataFrame: boolean;
|
||||
canStartStopDataFrame: boolean;
|
||||
canGetDataFrameAnalytics: boolean;
|
||||
canDeleteDataFrameAnalytics: boolean;
|
||||
canCreateDataFrameAnalytics: boolean;
|
||||
canStartStopDataFrameAnalytics: boolean;
|
||||
};
|
||||
isPlatinumOrTrialLicense: boolean;
|
||||
mlFeatureEnabledInSpace: boolean;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue