Skip to content

Commit

Permalink
[7.x] [ML] Add ability to delete target index & index pattern when de…
Browse files Browse the repository at this point in the history
…leting DFA job (#66934) (#67875)

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
qn895 and elasticmachine committed Jun 2, 2020
1 parent 0347989 commit cae1831
Show file tree
Hide file tree
Showing 15 changed files with 797 additions and 42 deletions.
11 changes: 11 additions & 0 deletions x-pack/plugins/ml/common/types/data_frame_analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* 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 { CustomHttpResponseOptions, ResponseError } from 'kibana/server';
export interface DeleteDataFrameAnalyticsWithIndexStatus {
success: boolean;
error?: CustomHttpResponseOptions<ResponseError>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,41 @@
*/

import React from 'react';
import { render } from '@testing-library/react';

import { fireEvent, render } from '@testing-library/react';
import * as CheckPrivilige from '../../../../../capabilities/check_capabilities';

import { DeleteAction } from './action_delete';

import mockAnalyticsListItem from './__mocks__/analytics_list_item.json';
import { DeleteAction } from './action_delete';
import { I18nProvider } from '@kbn/i18n/react';
import {
coreMock as mockCoreServices,
i18nServiceMock,
} from '../../../../../../../../../../src/core/public/mocks';

jest.mock('../../../../../capabilities/check_capabilities', () => ({
checkPermission: jest.fn(() => false),
createPermissionFailureMessage: jest.fn(),
}));

jest.mock('../../../../../../application/util/dependency_cache', () => ({
getToastNotifications: () => ({ addSuccess: jest.fn(), addDanger: jest.fn() }),
}));

jest.mock('../../../../../contexts/kibana', () => ({
useMlKibana: () => ({
services: mockCoreServices.createStart(),
}),
}));
export const MockI18nService = i18nServiceMock.create();
export const I18nServiceConstructor = jest.fn().mockImplementation(() => MockI18nService);
jest.doMock('@kbn/i18n', () => ({
I18nService: I18nServiceConstructor,
}));

describe('DeleteAction', () => {
afterEach(() => {
jest.clearAllMocks();
});

test('When canDeleteDataFrameAnalytics permission is false, button should be disabled.', () => {
const { getByTestId } = render(<DeleteAction item={mockAnalyticsListItem} />);
expect(getByTestId('mlAnalyticsJobDeleteButton')).toHaveAttribute('disabled');
Expand Down Expand Up @@ -46,4 +67,24 @@ describe('DeleteAction', () => {

expect(getByTestId('mlAnalyticsJobDeleteButton')).toHaveAttribute('disabled');
});

describe('When delete model is open', () => {
test('should allow to delete target index by default.', () => {
const mock = jest.spyOn(CheckPrivilige, 'checkPermission');
mock.mockImplementation((p) => p === 'canDeleteDataFrameAnalytics');
const { getByTestId, queryByTestId } = render(
<I18nProvider>
<DeleteAction item={mockAnalyticsListItem} />
</I18nProvider>
);
const deleteButton = getByTestId('mlAnalyticsJobDeleteButton');
fireEvent.click(deleteButton);
expect(getByTestId('mlAnalyticsJobDeleteModal')).toBeInTheDocument();
expect(getByTestId('mlAnalyticsJobDeleteIndexSwitch')).toBeInTheDocument();
const mlAnalyticsJobDeleteIndexSwitch = getByTestId('mlAnalyticsJobDeleteIndexSwitch');
expect(mlAnalyticsJobDeleteIndexSwitch).toHaveAttribute('aria-checked', 'true');
expect(queryByTestId('mlAnalyticsJobDeleteIndexPatternSwitch')).toBeNull();
mock.mockRestore();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,132 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { Fragment, FC, useState } from 'react';
import React, { Fragment, FC, useState, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiButtonEmpty,
EuiConfirmModal,
EuiOverlayMask,
EuiToolTip,
EuiSwitch,
EuiFlexGroup,
EuiFlexItem,
EUI_MODAL_CONFIRM_BUTTON,
} from '@elastic/eui';

import { deleteAnalytics } from '../../services/analytics_service';

import { IIndexPattern } from 'src/plugins/data/common';
import { FormattedMessage } from '@kbn/i18n/react';
import {
deleteAnalytics,
deleteAnalyticsAndDestIndex,
canDeleteIndex,
} from '../../services/analytics_service';
import {
checkPermission,
createPermissionFailureMessage,
} from '../../../../../capabilities/check_capabilities';

import { useMlKibana } from '../../../../../contexts/kibana';
import { isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common';
import { extractErrorMessage } from '../../../../../util/error_utils';

interface DeleteActionProps {
item: DataFrameAnalyticsListRow;
}

export const DeleteAction: FC<DeleteActionProps> = ({ item }) => {
const disabled = isDataFrameAnalyticsRunning(item.stats.state);

const canDeleteDataFrameAnalytics: boolean = checkPermission('canDeleteDataFrameAnalytics');

const [isModalVisible, setModalVisible] = useState(false);
const [deleteTargetIndex, setDeleteTargetIndex] = useState<boolean>(true);
const [deleteIndexPattern, setDeleteIndexPattern] = useState<boolean>(true);
const [userCanDeleteIndex, setUserCanDeleteIndex] = useState<boolean>(false);
const [indexPatternExists, setIndexPatternExists] = useState<boolean>(false);

const { savedObjects, notifications } = useMlKibana().services;
const savedObjectsClient = savedObjects.client;

const indexName = item.config.dest.index;

const checkIndexPatternExists = async () => {
try {
const response = await savedObjectsClient.find<IIndexPattern>({
type: 'index-pattern',
perPage: 10,
search: `"${indexName}"`,
searchFields: ['title'],
fields: ['title'],
});
const ip = response.savedObjects.find(
(obj) => obj.attributes.title.toLowerCase() === indexName.toLowerCase()
);
if (ip !== undefined) {
setIndexPatternExists(true);
}
} catch (e) {
const { toasts } = notifications;
const error = extractErrorMessage(e);

toasts.addDanger(
i18n.translate(
'xpack.ml.dataframe.analyticsList.errorWithCheckingIfIndexPatternExistsNotificationErrorMessage',
{
defaultMessage:
'An error occurred checking if index pattern {indexPattern} exists: {error}',
values: { indexPattern: indexName, error },
}
)
);
}
};
const checkUserIndexPermission = () => {
try {
const userCanDelete = canDeleteIndex(indexName);
if (userCanDelete) {
setUserCanDeleteIndex(true);
}
} catch (e) {
const { toasts } = notifications;
const error = extractErrorMessage(e);

toasts.addDanger(
i18n.translate(
'xpack.ml.dataframe.analyticsList.errorWithCheckingIfUserCanDeleteIndexNotificationErrorMessage',
{
defaultMessage:
'An error occurred checking if user can delete {destinationIndex}: {error}',
values: { destinationIndex: indexName, error },
}
)
);
}
};

useEffect(() => {
// Check if an index pattern exists corresponding to current DFA job
// if pattern does exist, show it to user
checkIndexPatternExists();

// Check if an user has permission to delete the index & index pattern
checkUserIndexPermission();
}, []);

const closeModal = () => setModalVisible(false);
const deleteAndCloseModal = () => {
setModalVisible(false);
deleteAnalytics(item);

if ((userCanDeleteIndex && deleteTargetIndex) || (userCanDeleteIndex && deleteIndexPattern)) {
deleteAnalyticsAndDestIndex(
item,
deleteTargetIndex,
indexPatternExists && deleteIndexPattern
);
} else {
deleteAnalytics(item);
}
};
const openModal = () => setModalVisible(true);
const toggleDeleteIndex = () => setDeleteTargetIndex(!deleteTargetIndex);
const toggleDeleteIndexPattern = () => setDeleteIndexPattern(!deleteIndexPattern);

const buttonDeleteText = i18n.translate('xpack.ml.dataframe.analyticsList.deleteActionName', {
defaultMessage: 'Delete',
Expand Down Expand Up @@ -84,8 +174,9 @@ export const DeleteAction: FC<DeleteActionProps> = ({ item }) => {
<Fragment>
{deleteButton}
{isModalVisible && (
<EuiOverlayMask>
<EuiOverlayMask data-test-subj="mlAnalyticsJobDeleteOverlay">
<EuiConfirmModal
data-test-subj="mlAnalyticsJobDeleteModal"
title={i18n.translate('xpack.ml.dataframe.analyticsList.deleteModalTitle', {
defaultMessage: 'Delete {analyticsId}',
values: { analyticsId: item.config.id },
Expand All @@ -108,10 +199,47 @@ export const DeleteAction: FC<DeleteActionProps> = ({ item }) => {
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.`,
})}
<FormattedMessage
id="xpack.ml.dataframe.analyticsList.deleteModalBody"
defaultMessage="Are you sure you want to delete this analytics job?"
/>
</p>

<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem>
{userCanDeleteIndex && (
<EuiSwitch
data-test-subj="mlAnalyticsJobDeleteIndexSwitch"
style={{ paddingBottom: 10 }}
label={i18n.translate(
'xpack.ml.dataframe.analyticsList.deleteDestinationIndexTitle',
{
defaultMessage: 'Delete destination index {indexName}',
values: { indexName },
}
)}
checked={deleteTargetIndex}
onChange={toggleDeleteIndex}
/>
)}
</EuiFlexItem>
<EuiFlexItem>
{userCanDeleteIndex && indexPatternExists && (
<EuiSwitch
data-test-subj="mlAnalyticsJobDeleteIndexPatternSwitch"
label={i18n.translate(
'xpack.ml.dataframe.analyticsList.deleteTargetIndexPatternTitle',
{
defaultMessage: 'Delete index pattern {indexPattern}',
values: { indexPattern: indexName },
}
)}
checked={deleteIndexPattern}
onChange={toggleDeleteIndexPattern}
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiConfirmModal>
</EuiOverlayMask>
)}
Expand Down
Loading

0 comments on commit cae1831

Please sign in to comment.