[ML] DataFrame bulk actions (#43331) (#43791)

* wip: add selection and actions icon

* add bulk delete functionality. use existing delete action component

* add start bulk action

* add stop bulk action

* add label for number of transforms selected

* Action components only accept items array. Update endpoint calls for array param

* update tests

* fix translation error

* update start modal translation

* transformDelete to server side for synchronous looping through ids

* transformsStart to server side for synchronous looping through ids

* transformsStop to server side for synchronous looping through ids

* change request method for delete.

* update deprecated functional component type

* ensure bulk actions disabled when appropriate

* handle timeouts for start,stop,delete actions

* rename DataFrameTransformEndpointRequest type

* disable all row actions when selected items

* fix localization error
This commit is contained in:
Melissa Alvarez 2019-08-22 15:41:18 -04:00 committed by GitHub
parent 2778539171
commit cf6d86c295
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 762 additions and 226 deletions

View file

@ -111,7 +111,7 @@ export const StepCreateForm: SFC<Props> = React.memo(
setStarted(true);
try {
await ml.dataFrame.startDataFrameTransform(transformId);
await ml.dataFrame.startDataFrameTransforms([{ id: transformId }]);
toastNotifications.addSuccess(
i18n.translate('xpack.ml.dataframe.stepCreateForm.startTransformSuccessMessage', {
defaultMessage: 'Data frame transform {transformId} started successfully.',

View file

@ -0,0 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Data Frame: Transform List Actions <StopAction /> Minimal initialization 1`] = `
<EuiToolTip
content="Your license has expired. Please contact your administrator."
delay="regular"
position="top"
>
<EuiButtonEmpty
aria-label="Stop"
color="text"
disabled={true}
iconSide="left"
iconType="stop"
onClick={[Function]}
size="xs"
type="button"
>
Stop
</EuiButtonEmpty>
</EuiToolTip>
`;

View file

@ -17,6 +17,24 @@
animation: none !important;
}
}
.mlTransformBulkActionItem {
display: block;
padding: $euiSizeS;
width: 100%;
text-align: left;
}
.mlTransformBulkActionsBorder {
height: 20px;
border-right: $euiBorderThin;
width: 1px;
display: inline-block;
vertical-align: middle;
height: 35px;
margin: 0px 5px;
margin-top: -5px;
}
.mlTransformProgressBar {
margin-bottom: $euiSizeM;
}

View file

@ -17,7 +17,7 @@ describe('Data Frame: Transform List Actions <DeleteAction />', () => {
const item: DataFrameTransformListRow = dataFrameTransformListRow;
const props = {
disabled: false,
item,
items: [item],
deleteTransform(d: DataFrameTransformListRow) {},
};

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, SFC, useState } from 'react';
import React, { Fragment, FC, useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiButtonEmpty,
@ -14,7 +14,7 @@ import {
EUI_MODAL_CONFIRM_BUTTON,
} from '@elastic/eui';
import { deleteTransform } from '../../services/transform_service';
import { deleteTransforms } from '../../services/transform_service';
import {
checkPermission,
@ -24,11 +24,16 @@ import {
import { DataFrameTransformListRow, DATA_FRAME_TRANSFORM_STATE } from './common';
interface DeleteActionProps {
item: DataFrameTransformListRow;
items: DataFrameTransformListRow[];
forceDisable?: boolean;
}
export const DeleteAction: SFC<DeleteActionProps> = ({ item }) => {
const disabled = item.stats.state !== DATA_FRAME_TRANSFORM_STATE.STOPPED;
export const DeleteAction: FC<DeleteActionProps> = ({ items, forceDisable }) => {
const isBulkAction = items.length > 1;
const disabled = items.some(
(i: DataFrameTransformListRow) => i.stats.state !== DATA_FRAME_TRANSFORM_STATE.STOPPED
);
const canDeleteDataFrame: boolean = checkPermission('canDeleteDataFrame');
@ -37,19 +42,54 @@ export const DeleteAction: SFC<DeleteActionProps> = ({ item }) => {
const closeModal = () => setModalVisible(false);
const deleteAndCloseModal = () => {
setModalVisible(false);
deleteTransform(item);
deleteTransforms(items);
};
const openModal = () => setModalVisible(true);
const buttonDeleteText = i18n.translate('xpack.ml.dataframe.transformList.deleteActionName', {
defaultMessage: 'Delete',
});
const bulkDeleteButtonDisabledText = i18n.translate(
'xpack.ml.dataframe.transformList.deleteBulkActionDisabledToolTipContent',
{
defaultMessage:
'One or more selected data frame transforms must be stopped in order to be deleted.',
}
);
const deleteButtonDisabledText = i18n.translate(
'xpack.ml.dataframe.transformList.deleteActionDisabledToolTipContent',
{
defaultMessage: 'Stop the data frame transform in order to delete it.',
}
);
const bulkDeleteModalTitle = i18n.translate(
'xpack.ml.dataframe.transformList.bulkDeleteModalTitle',
{
defaultMessage: 'Delete {count} {count, plural, one {transform} other {transforms}}?',
values: { count: items.length },
}
);
const deleteModalTitle = i18n.translate('xpack.ml.dataframe.transformList.deleteModalTitle', {
defaultMessage: 'Delete {transformId}',
values: { transformId: items[0] && items[0].config.id },
});
const bulkDeleteModalMessage = i18n.translate(
'xpack.ml.dataframe.transformList.bulkDeleteModalBody',
{
defaultMessage:
"Are you sure you want to delete {count, plural, one {this} other {these}} {count} {count, plural, one {transform} other {transforms}}? The transform's destination index and optional Kibana index pattern will not be deleted.",
values: { count: items.length },
}
);
const deleteModalMessage = i18n.translate('xpack.ml.dataframe.transformList.deleteModalBody', {
defaultMessage: `Are you sure you want to delete this transform? The transform's destination index and optional Kibana index pattern will not be deleted.`,
});
let deleteButton = (
<EuiButtonEmpty
size="xs"
color="text"
disabled={disabled || !canDeleteDataFrame}
disabled={forceDisable === true || disabled || !canDeleteDataFrame}
iconType="trash"
onClick={openModal}
aria-label={buttonDeleteText}
@ -59,20 +99,15 @@ export const DeleteAction: SFC<DeleteActionProps> = ({ item }) => {
);
if (disabled || !canDeleteDataFrame) {
let content;
if (disabled) {
content = isBulkAction === true ? bulkDeleteButtonDisabledText : deleteButtonDisabledText;
} else {
content = createPermissionFailureMessage('canStartStopDataFrame');
}
deleteButton = (
<EuiToolTip
position="top"
content={
disabled
? i18n.translate(
'xpack.ml.dataframe.transformList.deleteActionDisabledToolTipContent',
{
defaultMessage: 'Stop the data frame transform in order to delete it.',
}
)
: createPermissionFailureMessage('canStartStopDataFrame')
}
>
<EuiToolTip position="top" content={content}>
{deleteButton}
</EuiToolTip>
);
@ -84,10 +119,7 @@ export const DeleteAction: SFC<DeleteActionProps> = ({ item }) => {
{isModalVisible && (
<EuiOverlayMask>
<EuiConfirmModal
title={i18n.translate('xpack.ml.dataframe.transformList.deleteModalTitle', {
defaultMessage: 'Delete {transformId}',
values: { transformId: item.config.id },
})}
title={isBulkAction === true ? bulkDeleteModalTitle : deleteModalTitle}
onCancel={closeModal}
onConfirm={deleteAndCloseModal}
cancelButtonText={i18n.translate(
@ -105,11 +137,7 @@ export const DeleteAction: SFC<DeleteActionProps> = ({ item }) => {
defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON}
buttonColor="danger"
>
<p>
{i18n.translate('xpack.ml.dataframe.transformList.deleteModalBody', {
defaultMessage: `Are you sure you want to delete this transform? The transform's destination index and optional Kibana index pattern will not be deleted.`,
})}
</p>
<p>{isBulkAction === true ? bulkDeleteModalMessage : deleteModalMessage}</p>
</EuiConfirmModal>
</EuiOverlayMask>
)}

View file

@ -17,7 +17,7 @@ describe('Data Frame: Transform List Actions <StartAction />', () => {
const item: DataFrameTransformListRow = dataFrameTransformListRow;
const props = {
disabled: false,
item,
items: [item],
startTransform(d: DataFrameTransformListRow) {},
};

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, SFC, useState } from 'react';
import React, { Fragment, FC, useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiButtonEmpty,
@ -14,20 +14,26 @@ import {
EUI_MODAL_CONFIRM_BUTTON,
} from '@elastic/eui';
import { startTransform } from '../../services/transform_service';
import { startTransforms } from '../../services/transform_service';
import {
checkPermission,
createPermissionFailureMessage,
} from '../../../../../privilege/check_privilege';
import { DataFrameTransformListRow, isCompletedBatchTransform } from './common';
import {
DataFrameTransformListRow,
isCompletedBatchTransform,
DATA_FRAME_TRANSFORM_STATE,
} from './common';
interface StartActionProps {
item: DataFrameTransformListRow;
items: DataFrameTransformListRow[];
forceDisable?: boolean;
}
export const StartAction: SFC<StartActionProps> = ({ item }) => {
export const StartAction: FC<StartActionProps> = ({ items, forceDisable }) => {
const isBulkAction = items.length > 1;
const canStartStopDataFrameTransform: boolean = checkPermission('canStartStopDataFrame');
const [isModalVisible, setModalVisible] = useState(false);
@ -35,7 +41,7 @@ export const StartAction: SFC<StartActionProps> = ({ item }) => {
const closeModal = () => setModalVisible(false);
const startAndCloseModal = () => {
setModalVisible(false);
startTransform(item);
startTransforms(items);
};
const openModal = () => setModalVisible(true);
@ -44,13 +50,56 @@ export const StartAction: SFC<StartActionProps> = ({ item }) => {
});
// Disable start for batch transforms which have completed.
const completedBatchTransform = isCompletedBatchTransform(item);
const completedBatchTransform = items.some((i: DataFrameTransformListRow) =>
isCompletedBatchTransform(i)
);
// Disable start action if one of the transforms is already started or trying to restart will throw error
const startedTransform = items.some(
(i: DataFrameTransformListRow) => i.stats.state === DATA_FRAME_TRANSFORM_STATE.STARTED
);
let startedTransformMessage;
let completedBatchTransformMessage;
if (isBulkAction === true) {
startedTransformMessage = i18n.translate(
'xpack.ml.dataframe.transformList.startedTransformBulkToolTip',
{
defaultMessage: 'One or more selected data frame transforms is already started.',
}
);
completedBatchTransformMessage = i18n.translate(
'xpack.ml.dataframe.transformList.completeBatchTransformBulkActionToolTip',
{
defaultMessage:
'One or more selected data frame transforms is a completed batch transform and cannot be restarted.',
}
);
} else {
startedTransformMessage = i18n.translate(
'xpack.ml.dataframe.transformList.startedTransformToolTip',
{
defaultMessage: '{transformId} is already started.',
values: { transformId: items[0] && items[0].config.id },
}
);
completedBatchTransformMessage = i18n.translate(
'xpack.ml.dataframe.transformList.completeBatchTransformToolTip',
{
defaultMessage: '{transformId} is a completed batch transform and cannot be restarted.',
values: { transformId: items[0] && items[0].config.id },
}
);
}
const actionIsDisabled =
!canStartStopDataFrameTransform || completedBatchTransform || startedTransform;
let startButton = (
<EuiButtonEmpty
size="xs"
color="text"
disabled={!canStartStopDataFrameTransform || completedBatchTransform}
disabled={forceDisable === true || actionIsDisabled}
iconType="play"
onClick={openModal}
aria-label={buttonStartText}
@ -59,35 +108,42 @@ export const StartAction: SFC<StartActionProps> = ({ item }) => {
</EuiButtonEmpty>
);
if (!canStartStopDataFrameTransform || completedBatchTransform) {
if (actionIsDisabled) {
let content;
if (!canStartStopDataFrameTransform) {
content = createPermissionFailureMessage('canStartStopDataFrame');
} else if (completedBatchTransform) {
content = completedBatchTransformMessage;
} else if (startedTransform) {
content = startedTransformMessage;
}
startButton = (
<EuiToolTip
position="top"
content={
!canStartStopDataFrameTransform
? createPermissionFailureMessage('canStartStopDataFrame')
: i18n.translate('xpack.ml.dataframe.transformList.completeBatchTransformToolTip', {
defaultMessage:
'{transformId} is a completed batch transform and cannot be restarted.',
values: { transformId: item.config.id },
})
}
>
<EuiToolTip position="top" content={content}>
{startButton}
</EuiToolTip>
);
}
const bulkStartModalTitle = i18n.translate(
'xpack.ml.dataframe.transformList.bulkStartModalTitle',
{
defaultMessage: 'Start {count} {count, plural, one {transform} other {transforms}}?',
values: { count: items && items.length },
}
);
const startModalTitle = i18n.translate('xpack.ml.dataframe.transformList.startModalTitle', {
defaultMessage: 'Start {transformId}',
values: { transformId: items[0] && items[0].config.id },
});
return (
<Fragment>
{startButton}
{isModalVisible && (
<EuiOverlayMask>
<EuiConfirmModal
title={i18n.translate('xpack.ml.dataframe.transformList.startModalTitle', {
defaultMessage: 'Start {transformId}',
values: { transformId: item.config.id },
})}
title={isBulkAction === true ? bulkStartModalTitle : startModalTitle}
onCancel={closeModal}
onConfirm={startAndCloseModal}
cancelButtonText={i18n.translate(
@ -108,7 +164,8 @@ export const StartAction: SFC<StartActionProps> = ({ item }) => {
<p>
{i18n.translate('xpack.ml.dataframe.transformList.startModalBody', {
defaultMessage:
'A data frame transform will increase search and indexing load in your cluster. Please stop the transform if excessive load is experienced. Are you sure you want to start this transform?',
'A data frame transform will increase search and indexing load in your cluster. Please stop the transform if excessive load is experienced. Are you sure you want to start {count, plural, one {this} other {these}} {count} {count, plural, one {transform} other {transforms}}?',
values: { count: items.length },
})}
</p>
</EuiConfirmModal>

View file

@ -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 { shallow } from 'enzyme';
import React from 'react';
import { DataFrameTransformListRow } from './common';
import { StopAction } from './action_stop';
import dataFrameTransformListRow from './__mocks__/data_frame_transform_list_row.json';
describe('Data Frame: Transform List Actions <StopAction />', () => {
test('Minimal initialization', () => {
const item: DataFrameTransformListRow = dataFrameTransformListRow;
const props = {
disabled: false,
items: [item],
stopTransform(d: DataFrameTransformListRow) {},
};
const wrapper = shallow(<StopAction {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -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, { FC } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
import { DataFrameTransformListRow, DATA_FRAME_TRANSFORM_STATE } from './common';
import {
checkPermission,
createPermissionFailureMessage,
} from '../../../../../privilege/check_privilege';
import { stopTransforms } from '../../services/transform_service';
interface StopActionProps {
items: DataFrameTransformListRow[];
forceDisable?: boolean;
}
export const StopAction: FC<StopActionProps> = ({ items, forceDisable }) => {
const isBulkAction = items.length > 1;
const canStartStopDataFrame: boolean = checkPermission('canStartStopDataFrame');
const buttonStopText = i18n.translate('xpack.ml.dataframe.transformList.stopActionName', {
defaultMessage: 'Stop',
});
// Disable stop action if one of the transforms is stopped already
const stoppedTransform = items.some(
(i: DataFrameTransformListRow) => i.stats.state === DATA_FRAME_TRANSFORM_STATE.STOPPED
);
let stoppedTransformMessage;
if (isBulkAction === true) {
stoppedTransformMessage = i18n.translate(
'xpack.ml.dataframe.transformList.stoppedTransformBulkToolTip',
{
defaultMessage: 'One or more selected data frame transforms is already stopped.',
}
);
} else {
stoppedTransformMessage = i18n.translate(
'xpack.ml.dataframe.transformList.stoppedTransformToolTip',
{
defaultMessage: '{transformId} is already stopped.',
values: { transformId: items[0] && items[0].config.id },
}
);
}
const handleStop = () => {
stopTransforms(items);
};
const stopButton = (
<EuiButtonEmpty
size="xs"
color="text"
disabled={forceDisable === true || !canStartStopDataFrame || stoppedTransform === true}
iconType="stop"
onClick={handleStop}
aria-label={buttonStopText}
>
{buttonStopText}
</EuiButtonEmpty>
);
if (!canStartStopDataFrame || stoppedTransform) {
return (
<EuiToolTip
position="top"
content={
!canStartStopDataFrame
? createPermissionFailureMessage('canStartStopDataFrame')
: stoppedTransformMessage
}
>
{stopButton}
</EuiToolTip>
);
}
return stopButton;
};

View file

@ -8,7 +8,7 @@ import { getActions } from './actions';
describe('Data Frame: Transform List Actions', () => {
test('getActions()', () => {
const actions = getActions();
const actions = getActions({ forceDisable: false });
expect(actions).toHaveLength(2);
expect(actions[0].isPrimary).toBeTruthy();

View file

@ -5,64 +5,25 @@
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
import {
checkPermission,
createPermissionFailureMessage,
} from '../../../../../privilege/check_privilege';
import { DataFrameTransformListRow, DATA_FRAME_TRANSFORM_STATE } from './common';
import { stopTransform } from '../../services/transform_service';
import { StartAction } from './action_start';
import { StopAction } from './action_stop';
import { DeleteAction } from './action_delete';
export const getActions = () => {
const canStartStopDataFrame: boolean = checkPermission('canStartStopDataFrame');
export const getActions = ({ forceDisable }: { forceDisable: boolean }) => {
return [
{
isPrimary: true,
render: (item: DataFrameTransformListRow) => {
if (item.stats.state === DATA_FRAME_TRANSFORM_STATE.STOPPED) {
return <StartAction item={item} />;
return <StartAction items={[item]} forceDisable={forceDisable} />;
}
const buttonStopText = i18n.translate('xpack.ml.dataframe.transformList.stopActionName', {
defaultMessage: 'Stop',
});
const stopButton = (
<EuiButtonEmpty
size="xs"
color="text"
disabled={!canStartStopDataFrame}
iconType="stop"
onClick={() => stopTransform(item)}
aria-label={buttonStopText}
>
{buttonStopText}
</EuiButtonEmpty>
);
if (!canStartStopDataFrame) {
return (
<EuiToolTip
position="top"
content={createPermissionFailureMessage('canStartStopDataFrame')}
>
{stopButton}
</EuiToolTip>
);
}
return stopButton;
return <StopAction items={[item]} forceDisable={forceDisable} />;
},
},
{
render: (item: DataFrameTransformListRow) => {
return <DeleteAction item={item} />;
return <DeleteAction items={[item]} forceDisable={forceDisable} />;
},
},
];

View file

@ -8,7 +8,7 @@ import { getColumns } from './columns';
describe('Data Frame: Job List Columns', () => {
test('getColumns()', () => {
const columns = getColumns([], () => {});
const columns = getColumns([], () => {}, []);
expect(columns).toHaveLength(9);
expect(columns[0].isExpander).toBeTruthy();

View file

@ -61,9 +61,10 @@ export const getTaskStateBadge = (
export const getColumns = (
expandedRowItemIds: DataFrameTransformId[],
setExpandedRowItemIds: React.Dispatch<React.SetStateAction<DataFrameTransformId[]>>
setExpandedRowItemIds: React.Dispatch<React.SetStateAction<DataFrameTransformId[]>>,
transformSelection: DataFrameTransformListRow[]
) => {
const actions = getActions();
const actions = getActions({ forceDisable: transformSelection.length > 0 });
function toggleDetails(item: DataFrameTransformListRow) {
const index = expandedRowItemIds.indexOf(item.config.id);

View file

@ -105,6 +105,20 @@ export interface DataFrameTransformListRow {
stats: DataFrameTransformStats;
}
export interface DataFrameTransformEndpointRequest {
id: DataFrameTransformId;
state?: DATA_FRAME_TRANSFORM_STATE;
}
export interface ResultData {
success: boolean;
error?: any;
}
export interface DataFrameTransformEndpointResult {
[key: string]: ResultData;
}
// Used to pass on attribute names to table columns
export enum DataFrameTransformListColumn {
configDestIndex = 'config.dest.index',

View file

@ -8,7 +8,16 @@ import React, { Fragment, SFC, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiBadge, EuiButtonEmpty, EuiCallOut, EuiEmptyPrompt, SortDirection } from '@elastic/eui';
import {
EuiBadge,
EuiButtonEmpty,
EuiButtonIcon,
EuiCallOut,
EuiEmptyPrompt,
EuiPopover,
EuiTitle,
SortDirection,
} from '@elastic/eui';
import {
DataFrameTransformId,
@ -17,6 +26,9 @@ import {
} from '../../../../common';
import { checkPermission } from '../../../../../privilege/check_privilege';
import { getTaskStateBadge } from './columns';
import { DeleteAction } from './action_delete';
import { StartAction } from './action_start';
import { StopAction } from './action_stop';
import {
DataFrameTransformListColumn,
@ -67,6 +79,9 @@ export const DataFrameTransformList: SFC = () => {
const [filteredTransforms, setFilteredTransforms] = useState<DataFrameTransformListRow[]>([]);
const [expandedRowItemIds, setExpandedRowItemIds] = useState<DataFrameTransformId[]>([]);
const [transformSelection, setTransformSelection] = useState<DataFrameTransformListRow[]>([]);
const [isActionsMenuOpen, setIsActionsMenuOpen] = useState(false);
const [errorMessage, setErrorMessage] = useState<any>(undefined);
const [searchError, setSearchError] = useState<any>(undefined);
@ -218,7 +233,7 @@ export const DataFrameTransformList: SFC = () => {
);
}
const columns = getColumns(expandedRowItemIds, setExpandedRowItemIds);
const columns = getColumns(expandedRowItemIds, setExpandedRowItemIds, transformSelection);
const sorting = {
sort: {
@ -237,7 +252,66 @@ export const DataFrameTransformList: SFC = () => {
hidePerPageOptions: false,
};
const bulkActionMenuItems = [
<div key="startAction" className="mlTransformBulkActionItem">
<StartAction items={transformSelection} />
</div>,
<div key="stopAction" className="mlTransformBulkActionItem">
<StopAction items={transformSelection} />
</div>,
<div key="deleteAction" className="mlTransformBulkActionItem">
<DeleteAction items={transformSelection} />
</div>,
];
const renderToolsLeft = () => {
const buttonIcon = (
<EuiButtonIcon
size="s"
iconType="gear"
color="text"
onClick={() => {
setIsActionsMenuOpen(true);
}}
aria-label={i18n.translate(
'xpack.ml.dataframe.multiTransformActionsMenu.managementActionsAriaLabel',
{
defaultMessage: 'Management actions',
}
)}
/>
);
const bulkActionIcon = (
<EuiPopover
key="bulkActionIcon"
id="transformBulkActionsMenu"
button={buttonIcon}
isOpen={isActionsMenuOpen}
closePopover={() => setIsActionsMenuOpen(false)}
panelPaddingSize="none"
anchorPosition="rightUp"
>
{bulkActionMenuItems}
</EuiPopover>
);
return [
<EuiTitle key="selectedText" size="s">
<h3>
{i18n.translate('xpack.ml.dataframe.multiTransformActionsMenu.transformsCount', {
defaultMessage: '{count} {count, plural, one {transform} other {transforms}} selected',
values: { count: transformSelection.length },
})}
</h3>
</EuiTitle>,
<div key="bulkActionsBorder" className="mlTransformBulkActionsBorder" />,
bulkActionIcon,
];
};
const search = {
toolsLeft: transformSelection.length > 0 ? renderToolsLeft() : undefined,
onChange: onQueryChange,
box: {
incremental: true,
@ -288,6 +362,10 @@ export const DataFrameTransformList: SFC = () => {
setSortDirection(direction);
};
const selection = {
onSelectionChange: (selected: DataFrameTransformListRow[]) => setTransformSelection(selected),
};
return (
<Fragment>
<ProgressBar isLoading={isLoading} />
@ -303,6 +381,7 @@ export const DataFrameTransformList: SFC = () => {
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
onChange={onTableChange}
pagination={pagination}
selection={selection}
sorting={sorting}
search={search}
data-test-subj="mlDataFramesTableTransforms"

View file

@ -7,34 +7,45 @@
import { i18n } from '@kbn/i18n';
import { toastNotifications } from 'ui/notify';
import { ml } from '../../../../../services/ml_api_service';
import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../../../../common';
import {
DATA_FRAME_TRANSFORM_STATE,
DataFrameTransformListRow,
DataFrameTransformEndpointRequest,
DataFrameTransformEndpointResult,
} from '../../components/transform_list/common';
// @ts-ignore no declaration file
import { mlMessageBarService } from '../../../../../../public/components/messagebar/messagebar_service';
export const deleteTransform = async (d: DataFrameTransformListRow) => {
try {
if (d.stats.state === DATA_FRAME_TRANSFORM_STATE.FAILED) {
await ml.dataFrame.stopDataFrameTransform(d.config.id, true, true);
export const deleteTransforms = async (dataFrames: DataFrameTransformListRow[]) => {
const dataFramesInfo: DataFrameTransformEndpointRequest[] = dataFrames.map(df => ({
id: df.config.id,
state: df.stats.state,
}));
const results: DataFrameTransformEndpointResult = await ml.dataFrame.deleteDataFrameTransforms(
dataFramesInfo
);
for (const transformId in results) {
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
if (results.hasOwnProperty(transformId)) {
if (results[transformId].success === true) {
toastNotifications.addSuccess(
i18n.translate('xpack.ml.dataframe.transformList.deleteTransformSuccessMessage', {
defaultMessage: 'Data frame transform {transformId} deleted successfully.',
values: { transformId },
})
);
} else {
toastNotifications.addDanger(
i18n.translate('xpack.ml.dataframe.transformList.deleteTransformErrorMessage', {
defaultMessage: 'An error occurred deleting the data frame transform {transformId}',
values: { transformId },
})
);
mlMessageBarService.notify.error(results[transformId].error);
}
}
await ml.dataFrame.deleteDataFrameTransform(d.config.id);
toastNotifications.addSuccess(
i18n.translate('xpack.ml.dataframe.transformList.deleteTransformSuccessMessage', {
defaultMessage: 'Data frame transform {transformId} deleted successfully.',
values: { transformId: d.config.id },
})
);
} catch (e) {
toastNotifications.addDanger(
i18n.translate('xpack.ml.dataframe.transformList.deleteTransformErrorMessage', {
defaultMessage:
'An error occurred deleting the data frame transform {transformId}: {error}',
values: { transformId: d.config.id, error: JSON.stringify(e) },
})
);
}
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
};

View file

@ -5,6 +5,6 @@
*/
export { getTransformsFactory } from './get_transforms';
export { deleteTransform } from './delete_transform';
export { startTransform } from './start_transform';
export { stopTransform } from './stop_transform';
export { deleteTransforms } from './delete_transform';
export { startTransforms } from './start_transform';
export { stopTransforms } from './stop_transform';

View file

@ -11,30 +11,43 @@ import { ml } from '../../../../../services/ml_api_service';
import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../../../../common';
import {
DATA_FRAME_TRANSFORM_STATE,
DataFrameTransformListRow,
DataFrameTransformEndpointRequest,
DataFrameTransformEndpointResult,
} from '../../components/transform_list/common';
// @ts-ignore no declaration file
import { mlMessageBarService } from '../../../../../../public/components/messagebar/messagebar_service';
export const startTransform = async (d: DataFrameTransformListRow) => {
try {
await ml.dataFrame.startDataFrameTransform(
d.config.id,
d.stats.state === DATA_FRAME_TRANSFORM_STATE.FAILED
);
toastNotifications.addSuccess(
i18n.translate('xpack.ml.dataframe.transformList.startTransformSuccessMessage', {
defaultMessage: 'Data frame transform {transformId} started successfully.',
values: { transformId: d.config.id },
})
);
} catch (e) {
toastNotifications.addDanger(
i18n.translate('xpack.ml.dataframe.transformList.startTransformErrorMessage', {
defaultMessage:
'An error occurred starting the data frame transform {transformId}: {error}',
values: { transformId: d.config.id, error: JSON.stringify(e) },
})
);
export const startTransforms = async (dataFrames: DataFrameTransformListRow[]) => {
const dataFramesInfo: DataFrameTransformEndpointRequest[] = dataFrames.map(df => ({
id: df.config.id,
state: df.stats.state,
}));
const results: DataFrameTransformEndpointResult = await ml.dataFrame.startDataFrameTransforms(
dataFramesInfo
);
for (const transformId in results) {
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
if (results.hasOwnProperty(transformId)) {
if (results[transformId].success === true) {
toastNotifications.addSuccess(
i18n.translate('xpack.ml.dataframe.transformList.startTransformSuccessMessage', {
defaultMessage: 'Data frame transform {transformId} started successfully.',
values: { transformId },
})
);
} else {
toastNotifications.addDanger(
i18n.translate('xpack.ml.dataframe.transformList.startTransformErrorMessage', {
defaultMessage: 'An error occurred starting the data frame transform {transformId}',
values: { transformId },
})
);
mlMessageBarService.notify.error(results[transformId].error);
}
}
}
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
};

View file

@ -11,31 +11,43 @@ import { ml } from '../../../../../services/ml_api_service';
import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../../../../common';
import {
DATA_FRAME_TRANSFORM_STATE,
DataFrameTransformListRow,
DataFrameTransformEndpointRequest,
DataFrameTransformEndpointResult,
} from '../../components/transform_list/common';
// @ts-ignore no declaration file
import { mlMessageBarService } from '../../../../../../public/components/messagebar/messagebar_service';
export const stopTransform = async (d: DataFrameTransformListRow) => {
try {
await ml.dataFrame.stopDataFrameTransform(
d.config.id,
d.stats.state === DATA_FRAME_TRANSFORM_STATE.FAILED,
true
);
toastNotifications.addSuccess(
i18n.translate('xpack.ml.dataframe.transformList.stopTransformSuccessMessage', {
defaultMessage: 'Data frame transform {transformId} stopped successfully.',
values: { transformId: d.config.id },
})
);
} catch (e) {
toastNotifications.addDanger(
i18n.translate('xpack.ml.dataframe.transformList.stopTransformErrorMessage', {
defaultMessage:
'An error occurred stopping the data frame transform {transformId}: {error}',
values: { transformId: d.config.id, error: JSON.stringify(e) },
})
);
export const stopTransforms = async (dataFrames: DataFrameTransformListRow[]) => {
const dataFramesInfo: DataFrameTransformEndpointRequest[] = dataFrames.map(df => ({
id: df.config.id,
state: df.stats.state,
}));
const results: DataFrameTransformEndpointResult = await ml.dataFrame.stopDataFrameTransforms(
dataFramesInfo
);
for (const transformId in results) {
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
if (results.hasOwnProperty(transformId)) {
if (results[transformId].success === true) {
toastNotifications.addSuccess(
i18n.translate('xpack.ml.dataframe.transformList.stopTransformSuccessMessage', {
defaultMessage: 'Data frame transform {transformId} stopped successfully.',
values: { transformId },
})
);
} else {
toastNotifications.addDanger(
i18n.translate('xpack.ml.dataframe.transformList.stopTransformErrorMessage', {
defaultMessage: 'An error occurred stopping the data frame transform {transformId}',
values: { transformId },
})
);
mlMessageBarService.notify.error(results[transformId].error);
}
}
}
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
};

View file

@ -40,10 +40,13 @@ export const dataFrame = {
data: transformConfig
});
},
deleteDataFrameTransform(transformId) {
deleteDataFrameTransforms(transformsInfo) {
return http({
url: `${basePath}/_data_frame/transforms/${transformId}`,
method: 'DELETE',
url: `${basePath}/_data_frame/transforms/delete_transforms`,
method: 'POST',
data: {
transformsInfo
}
});
},
getDataFrameTransformsPreview(obj) {
@ -53,16 +56,22 @@ export const dataFrame = {
data: obj
});
},
startDataFrameTransform(transformId, force = false) {
startDataFrameTransforms(transformsInfo) {
return http({
url: `${basePath}/_data_frame/transforms/${transformId}/_start?force=${force}`,
url: `${basePath}/_data_frame/transforms/start_transforms`,
method: 'POST',
data: {
transformsInfo,
}
});
},
stopDataFrameTransform(transformId, force = false, waitForCompletion = false) {
stopDataFrameTransforms(transformsInfo) {
return http({
url: `${basePath}/_data_frame/transforms/${transformId}/_stop?force=${force}&wait_for_completion=${waitForCompletion}`,
url: `${basePath}/_data_frame/transforms/stop_transforms`,
method: 'POST',
data: {
transformsInfo,
}
});
},
getTransformAuditMessages(transformId) {

View file

@ -8,6 +8,10 @@ import { Annotation } from '../../../common/types/annotations';
import { AggFieldNamePair } from '../../../common/types/fields';
import { ExistingJobsAndGroups } from '../job_service';
import { PrivilegesResponse } from '../../../common/types/privileges';
import {
DataFrameTransformEndpointRequest,
DataFrameTransformEndpointResult,
} from '../../data_frame/pages/transform_management/components/transform_list/common';
// TODO This is not a complete representation of all methods of `ml.*`.
// It just satisfies needs for other parts of the code area which use
@ -47,14 +51,16 @@ declare interface Ml {
getDataFrameTransforms(jobId?: string): Promise<any>;
getDataFrameTransformsStats(jobId?: string): Promise<any>;
createDataFrameTransform(jobId: string, jobConfig: any): Promise<any>;
deleteDataFrameTransform(jobId: string): Promise<any>;
deleteDataFrameTransforms(
jobsData: DataFrameTransformEndpointRequest[]
): Promise<DataFrameTransformEndpointResult>;
getDataFrameTransformsPreview(payload: any): Promise<any>;
startDataFrameTransform(jobId: string, force?: boolean): Promise<any>;
stopDataFrameTransform(
jobId: string,
force?: boolean,
waitForCompletion?: boolean
): Promise<any>;
startDataFrameTransforms(
jobsData: DataFrameTransformEndpointRequest[]
): Promise<DataFrameTransformEndpointResult>;
stopDataFrameTransforms(
jobsData: DataFrameTransformEndpointRequest[]
): Promise<DataFrameTransformEndpointResult>;
getTransformAuditMessages(transformId: string): Promise<any>;
};

View file

@ -0,0 +1,66 @@
/*
* 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 no declaration file for module
export { isRequestTimeout } from '../job_service/error_utils';
import {
DataFrameTransformEndpointRequest,
DataFrameTransformEndpointResult,
} from '../../../public/data_frame/pages/transform_management/components/transform_list/common';
interface Params {
results: DataFrameTransformEndpointResult;
id: string;
items: DataFrameTransformEndpointRequest[];
action: string;
}
// populate a results object with timeout errors for the ids which haven't already been set
export function fillResultsWithTimeouts({ results, id, items, action }: Params) {
const extra =
items.length - Object.keys(results).length > 1
? i18n.translate('xpack.ml.models.transformService.allOtherRequestsCancelledDescription', {
defaultMessage: 'All other requests cancelled.',
})
: '';
const error = {
response: {
error: {
root_cause: [
{
reason: i18n.translate(
'xpack.ml.models.transformService.requestToActionTimedOutErrorMessage',
{
defaultMessage: `Request to {action} '{id}' timed out. {extra}`,
values: {
id,
action,
extra,
},
}
),
},
],
},
},
};
const newResults: DataFrameTransformEndpointResult = {};
return items.reduce((accumResults, currentVal) => {
if (results[currentVal.id] === undefined) {
accumResults[currentVal.id] = {
success: false,
error,
};
} else {
accumResults[currentVal.id] = results[currentVal.id];
}
return accumResults;
}, newResults);
}

View file

@ -6,3 +6,4 @@
export { transformAuditMessagesProvider } from './transform_audit_messages';
export { transformServiceProvider } from './transforms';

View file

@ -0,0 +1,147 @@
/*
* 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 { callWithRequestType } from '../../../common/types/kibana';
import {
DATA_FRAME_TRANSFORM_STATE,
DataFrameTransformEndpointRequest,
DataFrameTransformEndpointResult,
} from '../../../public/data_frame/pages/transform_management/components/transform_list/common';
import { DataFrameTransformId } from '../../../public/data_frame/common/transform';
import { isRequestTimeout, fillResultsWithTimeouts } from './error_utils';
enum TRANSFORM_ACTIONS {
STOP = 'stop',
START = 'start',
DELETE = 'delete',
}
interface StartStopOptions {
transformId: DataFrameTransformId;
force: boolean;
waitForCompletion?: boolean;
}
export function transformServiceProvider(callWithRequest: callWithRequestType) {
async function deleteTransform(transformId: DataFrameTransformId) {
return callWithRequest('ml.deleteDataFrameTransform', { transformId });
}
async function stopTransform(options: StartStopOptions) {
return callWithRequest('ml.stopDataFrameTransform', options);
}
async function startTransform(options: StartStopOptions) {
return callWithRequest('ml.startDataFrameTransform', options);
}
async function deleteTransforms(transformsInfo: DataFrameTransformEndpointRequest[]) {
const results: DataFrameTransformEndpointResult = {};
for (const transformInfo of transformsInfo) {
const transformId = transformInfo.id;
try {
if (transformInfo.state === DATA_FRAME_TRANSFORM_STATE.FAILED) {
try {
await stopTransform({
transformId,
force: true,
waitForCompletion: true,
});
} catch (e) {
if (isRequestTimeout(e)) {
return fillResultsWithTimeouts({
results,
id: transformId,
items: transformsInfo,
action: TRANSFORM_ACTIONS.DELETE,
});
}
}
}
await deleteTransform(transformId);
results[transformId] = { success: true };
} catch (e) {
if (isRequestTimeout(e)) {
return fillResultsWithTimeouts({
results,
id: transformInfo.id,
items: transformsInfo,
action: TRANSFORM_ACTIONS.DELETE,
});
}
results[transformId] = { success: false, error: JSON.stringify(e) };
}
}
return results;
}
async function startTransforms(transformsInfo: DataFrameTransformEndpointRequest[]) {
const results: DataFrameTransformEndpointResult = {};
for (const transformInfo of transformsInfo) {
const transformId = transformInfo.id;
try {
await startTransform({
transformId,
force:
transformInfo.state !== undefined
? transformInfo.state === DATA_FRAME_TRANSFORM_STATE.FAILED
: false,
});
results[transformId] = { success: true };
} catch (e) {
if (isRequestTimeout(e)) {
return fillResultsWithTimeouts({
results,
id: transformId,
items: transformsInfo,
action: TRANSFORM_ACTIONS.START,
});
}
results[transformId] = { success: false, error: JSON.stringify(e) };
}
}
return results;
}
async function stopTransforms(transformsInfo: DataFrameTransformEndpointRequest[]) {
const results: DataFrameTransformEndpointResult = {};
for (const transformInfo of transformsInfo) {
const transformId = transformInfo.id;
try {
await stopTransform({
transformId,
force:
transformInfo.state !== undefined
? transformInfo.state === DATA_FRAME_TRANSFORM_STATE.FAILED
: false,
waitForCompletion: true,
});
results[transformId] = { success: true };
} catch (e) {
if (isRequestTimeout(e)) {
return fillResultsWithTimeouts({
results,
id: transformId,
items: transformsInfo,
action: TRANSFORM_ACTIONS.STOP,
});
}
results[transformId] = { success: false, error: JSON.stringify(e) };
}
}
return results;
}
return {
deleteTransforms,
startTransforms,
stopTransforms,
};
}

View file

@ -7,6 +7,7 @@
import { callWithRequestFactory } from '../client/call_with_request_factory';
import { wrapError } from '../client/errors';
import { transformAuditMessagesProvider } from '../models/data_frame/transform_audit_messages';
import { transformServiceProvider } from '../models/data_frame';
export function dataFrameRoutes({ commonRouteConfig, elasticsearchPlugin, route }) {
@ -79,12 +80,13 @@ export function dataFrameRoutes({ commonRouteConfig, elasticsearchPlugin, route
});
route({
method: 'DELETE',
path: '/api/ml/_data_frame/transforms/{transformId}',
method: 'POST',
path: '/api/ml/_data_frame/transforms/delete_transforms',
handler(request) {
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
const { transformId } = request.params;
return callWithRequest('ml.deleteDataFrameTransform', { transformId })
const { deleteTransforms } = transformServiceProvider(callWithRequest);
const { transformsInfo } = request.payload;
return deleteTransforms(transformsInfo)
.catch(resp => wrapError(resp));
},
config: {
@ -107,18 +109,12 @@ export function dataFrameRoutes({ commonRouteConfig, elasticsearchPlugin, route
route({
method: 'POST',
path: '/api/ml/_data_frame/transforms/{transformId}/_start',
path: '/api/ml/_data_frame/transforms/start_transforms',
handler(request) {
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
const options = {
transformId: request.params.transformId
};
if (request.query.force !== undefined) {
options.force = request.query.force;
}
return callWithRequest('ml.startDataFrameTransform', options)
const { startTransforms } = transformServiceProvider(callWithRequest);
const { transformsInfo } = request.payload;
return startTransforms(transformsInfo)
.catch(resp => wrapError(resp));
},
config: {
@ -128,22 +124,12 @@ export function dataFrameRoutes({ commonRouteConfig, elasticsearchPlugin, route
route({
method: 'POST',
path: '/api/ml/_data_frame/transforms/{transformId}/_stop',
path: '/api/ml/_data_frame/transforms/stop_transforms',
handler(request) {
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
const options = {
transformId: request.params.transformId
};
if (request.query.force !== undefined) {
options.force = request.query.force;
}
if (request.query.wait_for_completion !== undefined) {
options.waitForCompletion = request.query.wait_for_completion;
}
return callWithRequest('ml.stopDataFrameTransform', options)
const { stopTransforms } = transformServiceProvider(callWithRequest);
const { transformsInfo } = request.payload;
return stopTransforms(transformsInfo)
.catch(resp => wrapError(resp));
},
config: {

View file

@ -5981,7 +5981,6 @@
"xpack.ml.dataframe.transformList.dataFrameTitle": "データフレームジョブ",
"xpack.ml.dataframe.transformList.deleteActionDisabledToolTipContent": "削除するにはデータフレームジョブを停止してください。",
"xpack.ml.dataframe.transformList.deleteActionName": "削除",
"xpack.ml.dataframe.transformList.deleteTransformErrorMessage": "データフレームジョブ {transformId} の削除中にエラーが発生しました: {error}",
"xpack.ml.dataframe.transformList.deleteTransformSuccessMessage": "データフレームジョブ {transformId} が削除されました",
"xpack.ml.dataframe.transformList.deleteModalBody": "このジョブを削除してよろしいですか?ジョブの最大インデックスとオプションの Kibana インデックスパターンは削除されません。",
"xpack.ml.dataframe.transformList.deleteModalCancelButton": "キャンセル",
@ -5991,14 +5990,11 @@
"xpack.ml.dataframe.transformList.rowCollapse": "{transformId} の詳細を非表示",
"xpack.ml.dataframe.transformList.rowExpand": "{transformId} の詳細を表示",
"xpack.ml.dataframe.transformList.startActionName": "開始",
"xpack.ml.dataframe.transformList.startTransformErrorMessage": "データフレームジョブ {transformId} の開始中にエラーが発生しました: {error}",
"xpack.ml.dataframe.transformList.startTransformSuccessMessage": "データフレームジョブ {transformId} が開始しました",
"xpack.ml.dataframe.transformList.startModalBody": "データフレームジョブは、クラスターの検索とインデックスによる負荷を増やします。過剰な負荷が生じた場合はジョブを停止してください。このジョブを開始してよろしいですか?",
"xpack.ml.dataframe.transformList.startModalCancelButton": "キャンセル",
"xpack.ml.dataframe.transformList.startModalStartButton": "開始",
"xpack.ml.dataframe.transformList.startModalTitle": "{transformId} を開始",
"xpack.ml.dataframe.transformList.stopActionName": "停止",
"xpack.ml.dataframe.transformList.stopTransformErrorMessage": "データフレームジョブ {transformId} の停止中にエラーが発生しました: {error}",
"xpack.ml.dataframe.transformList.stopTransformSuccessMessage": "データフレームジョブ {transformId} が停止しました",
"xpack.ml.dataframe.noGrantedPrivilegesDescription": "{kibanaUserParam} と {dataFrameUserParam} ロールの権限が必要です。{br}これらのロールはシステム管理者がユーザー管理ページで設定します。",
"xpack.ml.dataframe.noPermissionToAccessMLLabel": "データフレームへのアクセスにはパーミッションが必要です",

View file

@ -6125,7 +6125,6 @@
"xpack.ml.dataframe.transformList.dataFrameTitle": "数据帧作业",
"xpack.ml.dataframe.transformList.deleteActionDisabledToolTipContent": "停止数据帧作业,以便将其删除。",
"xpack.ml.dataframe.transformList.deleteActionName": "删除",
"xpack.ml.dataframe.transformList.deleteTransformErrorMessage": "删除数据帧作业 {transformId} 时发生错误:{error}",
"xpack.ml.dataframe.transformList.deleteTransformSuccessMessage": "数据帧作业 {transformId} 删除成功。",
"xpack.ml.dataframe.transformList.deleteModalBody": "是否确定要删除此作业?作业的目标索引和可选 Kibana 索引模式将不会删除。",
"xpack.ml.dataframe.transformList.deleteModalCancelButton": "取消",
@ -6135,14 +6134,11 @@
"xpack.ml.dataframe.transformList.rowCollapse": "隐藏 {transformId} 的详情",
"xpack.ml.dataframe.transformList.rowExpand": "显示 {transformId} 的详情",
"xpack.ml.dataframe.transformList.startActionName": "开始",
"xpack.ml.dataframe.transformList.startTransformErrorMessage": "启动数据帧作业 {transformId} 时发生错误:{error}",
"xpack.ml.dataframe.transformList.startTransformSuccessMessage": "数据帧作业 {transformId} 启动成功。",
"xpack.ml.dataframe.transformList.startModalBody": "数据帧作业将增加集群的搜索和索引负荷。如果负荷超载,请停止作业。是否确定要启动此作业?",
"xpack.ml.dataframe.transformList.startModalCancelButton": "取消",
"xpack.ml.dataframe.transformList.startModalStartButton": "开始",
"xpack.ml.dataframe.transformList.startModalTitle": "启动 {transformId}",
"xpack.ml.dataframe.transformList.stopActionName": "停止",
"xpack.ml.dataframe.transformList.stopTransformErrorMessage": "停止数据帧作业 {transformId} 时发生错误:{error}",
"xpack.ml.dataframe.transformList.stopTransformSuccessMessage": "数据帧作业 {transformId} 停止成功。",
"xpack.ml.dataframe.noGrantedPrivilegesDescription": "您必须具有 {kibanaUserParam} 和 {dataFrameUserParam} 角色授予的权限。{br}您的系统管理员可以在“管理用户”页面上设置这些角色。",
"xpack.ml.dataframe.noPermissionToAccessMLLabel": "您需要访问数据帧的权限",