mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[Security Solution][Automatic Migrations] Siem migrations delete from UI (#223983)
## Summary This pr adds a button with confirm modal to enable deleting finished | stopped | aborted migrations from the UI cards, the api functionality already exists. Also changes the rename button to be within a context menu, and restructures the components around the panels and migration titles.  ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Sergi Massaneda <sergi.massaneda@elastic.co>
This commit is contained in:
parent
fd7f85149c
commit
1fe758c34f
11 changed files with 487 additions and 18 deletions
|
@ -17,6 +17,7 @@ export const siemMigrationEventNames = {
|
|||
[SiemMigrationsEventTypes.SetupConnectorSelected]: 'Connector Selected',
|
||||
[SiemMigrationsEventTypes.SetupMigrationOpenNew]: 'Open new rules migration',
|
||||
[SiemMigrationsEventTypes.SetupMigrationCreated]: 'Create new rules migration',
|
||||
[SiemMigrationsEventTypes.SetupMigrationDeleted]: 'Migration deleted',
|
||||
[SiemMigrationsEventTypes.SetupResourcesUploaded]: 'Upload rule resources',
|
||||
[SiemMigrationsEventTypes.SetupMigrationOpenResources]: 'Rules Open Resources',
|
||||
[SiemMigrationsEventTypes.SetupRulesQueryCopied]: 'Copy rules query',
|
||||
|
@ -128,6 +129,11 @@ const eventSchemas: SiemMigrationsTelemetryEventSchemas = {
|
|||
},
|
||||
},
|
||||
},
|
||||
[SiemMigrationsEventTypes.SetupMigrationDeleted]: {
|
||||
...migrationIdSchema,
|
||||
...baseResultActionSchema,
|
||||
...eventNameSchema,
|
||||
},
|
||||
[SiemMigrationsEventTypes.SetupRulesQueryCopied]: {
|
||||
...eventNameSchema,
|
||||
migrationId: {
|
||||
|
|
|
@ -19,6 +19,10 @@ export enum SiemMigrationsEventTypes {
|
|||
* When Rule Resources are uploaded
|
||||
*/
|
||||
SetupMigrationCreated = 'siem_migrations_setup_rules_migration_created',
|
||||
/**
|
||||
* When a rules migration is deleted
|
||||
*/
|
||||
SetupMigrationDeleted = 'siem_migrations_setup_rules_migration_deleted',
|
||||
/**
|
||||
* When new rules are uploaded to create a new migration
|
||||
*/
|
||||
|
@ -92,6 +96,10 @@ export interface ReportSetupMigrationCreatedActionParams extends BaseResultActio
|
|||
migrationId?: string;
|
||||
rulesCount: number;
|
||||
}
|
||||
export interface ReportSetupMigrationDeletedActionParams extends BaseResultActionParams {
|
||||
eventName: string;
|
||||
migrationId: string;
|
||||
}
|
||||
export interface ReportSetupMacrosQueryCopiedActionParams {
|
||||
eventName: string;
|
||||
migrationId: string;
|
||||
|
@ -154,6 +162,7 @@ export interface SiemMigrationsTelemetryEventsMap {
|
|||
[SiemMigrationsEventTypes.SetupMigrationOpenResources]: ReportSetupMigrationOpenResourcesActionParams;
|
||||
[SiemMigrationsEventTypes.SetupRulesQueryCopied]: ReportSetupRulesQueryCopiedActionParams;
|
||||
[SiemMigrationsEventTypes.SetupMigrationCreated]: ReportSetupMigrationCreatedActionParams;
|
||||
[SiemMigrationsEventTypes.SetupMigrationDeleted]: ReportSetupMigrationDeletedActionParams;
|
||||
[SiemMigrationsEventTypes.SetupMacrosQueryCopied]: ReportSetupMacrosQueryCopiedActionParams;
|
||||
[SiemMigrationsEventTypes.SetupLookupNameCopied]: ReportSetupLookupNameCopiedActionParams;
|
||||
[SiemMigrationsEventTypes.SetupResourcesUploaded]: ReportSetupResourcesUploadedActionParams;
|
||||
|
|
|
@ -403,3 +403,16 @@ export const updateMigration = async ({
|
|||
{ version: '1', body: JSON.stringify(body), signal }
|
||||
);
|
||||
};
|
||||
|
||||
export interface DeleteMigrationParams {
|
||||
/** `id` of the migration to delete */
|
||||
migrationId: string;
|
||||
/** Optional AbortSignal for cancelling request */
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
export const deleteMigration = async ({ migrationId, signal }: DeleteMigrationParams) => {
|
||||
return KibanaServices.get().http.delete<unknown>(
|
||||
replaceParams(SIEM_RULE_MIGRATION_PATH, { migration_id: migrationId }),
|
||||
{ version: '1', signal }
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,287 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { MigrationPanelTitle } from './migration_panel_title';
|
||||
import { useUpdateMigration } from '../../logic/use_update_migration';
|
||||
import { useDeleteMigration } from '../../logic/use_delete_migration';
|
||||
import { SiemMigrationTaskStatus } from '../../../../../common/siem_migrations/constants';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import type { RuleMigrationStats } from '../../types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana/use_kibana');
|
||||
|
||||
jest.mock('../../logic/use_update_migration');
|
||||
const useUpdateMigrationMock = useUpdateMigration as jest.Mock;
|
||||
const mockUpdateMigration = jest.fn();
|
||||
|
||||
jest.mock('../../logic/use_delete_migration');
|
||||
const useDeleteMigrationMock = useDeleteMigration as jest.Mock;
|
||||
const mockDeleteMigration = jest.fn();
|
||||
|
||||
const mockMigrationStatsReady: RuleMigrationStats = {
|
||||
id: 'test-migration-id',
|
||||
name: 'Test Migration',
|
||||
status: SiemMigrationTaskStatus.READY,
|
||||
rules: { total: 6, pending: 6, processing: 0, completed: 0, failed: 0 },
|
||||
created_at: '2025-05-27T12:12:17.563Z',
|
||||
last_updated_at: '2025-05-27T12:12:17.563Z',
|
||||
};
|
||||
|
||||
const mockMigrationStatsRunning: RuleMigrationStats = {
|
||||
...mockMigrationStatsReady,
|
||||
status: SiemMigrationTaskStatus.RUNNING,
|
||||
};
|
||||
|
||||
const renderMigrationPanelTitle = (migrationStats: RuleMigrationStats) => {
|
||||
return render(<MigrationPanelTitle migrationStats={migrationStats} />, {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
};
|
||||
|
||||
describe('MigrationPanelTitle', () => {
|
||||
beforeEach(() => {
|
||||
useUpdateMigrationMock.mockReturnValue({
|
||||
mutate: mockUpdateMigration,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
useDeleteMigrationMock.mockReturnValue({
|
||||
mutate: mockDeleteMigration,
|
||||
isLoading: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Basic rendering', () => {
|
||||
it('should render migration name correctly', () => {
|
||||
renderMigrationPanelTitle(mockMigrationStatsReady);
|
||||
expect(screen.getByText('Test Migration')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render options button', () => {
|
||||
renderMigrationPanelTitle(mockMigrationStatsReady);
|
||||
expect(screen.getByTestId('openMigrationOptionsButton')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have correct aria-label for options button', () => {
|
||||
renderMigrationPanelTitle(mockMigrationStatsReady);
|
||||
expect(screen.getByLabelText(i18n.OPEN_MIGRATION_OPTIONS_BUTTON)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Options menu', () => {
|
||||
it('should open options menu when button is clicked', () => {
|
||||
renderMigrationPanelTitle(mockMigrationStatsReady);
|
||||
|
||||
const optionsButton = screen.getByTestId('openMigrationOptionsButton');
|
||||
fireEvent.click(optionsButton);
|
||||
|
||||
expect(screen.getByTestId('renameMigrationItem')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('deleteMigrationItem')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show rename option in menu', () => {
|
||||
renderMigrationPanelTitle(mockMigrationStatsReady);
|
||||
|
||||
const optionsButton = screen.getByTestId('openMigrationOptionsButton');
|
||||
fireEvent.click(optionsButton);
|
||||
|
||||
expect(screen.getByTestId('renameMigrationItem')).toHaveTextContent(
|
||||
i18n.RENAME_MIGRATION_TEXT
|
||||
);
|
||||
});
|
||||
|
||||
it('should show delete option in menu', () => {
|
||||
renderMigrationPanelTitle(mockMigrationStatsReady);
|
||||
|
||||
const optionsButton = screen.getByTestId('openMigrationOptionsButton');
|
||||
fireEvent.click(optionsButton);
|
||||
|
||||
expect(screen.getByTestId('deleteMigrationItem')).toHaveTextContent(i18n.DELETE_BUTTON_TEXT);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rename functionality', () => {
|
||||
it('should enter edit mode when rename is clicked', () => {
|
||||
renderMigrationPanelTitle(mockMigrationStatsReady);
|
||||
|
||||
const optionsButton = screen.getByTestId('openMigrationOptionsButton');
|
||||
fireEvent.click(optionsButton);
|
||||
|
||||
const renameButton = screen.getByTestId('renameMigrationItem');
|
||||
fireEvent.click(renameButton);
|
||||
|
||||
expect(screen.getByLabelText('Migration name')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should save new name when edit is confirmed', async () => {
|
||||
renderMigrationPanelTitle(mockMigrationStatsReady);
|
||||
|
||||
const optionsButton = screen.getByTestId('openMigrationOptionsButton');
|
||||
fireEvent.click(optionsButton);
|
||||
|
||||
const renameButton = screen.getByTestId('renameMigrationItem');
|
||||
fireEvent.click(renameButton);
|
||||
|
||||
const input = screen.getByLabelText('Migration name');
|
||||
fireEvent.change(input, { target: { value: 'New Migration Name' } });
|
||||
fireEvent.keyDown(input, { key: 'Enter' });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateMigration).toHaveBeenCalledWith({ name: 'New Migration Name' });
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel edit when escape is pressed', () => {
|
||||
renderMigrationPanelTitle(mockMigrationStatsReady);
|
||||
|
||||
const optionsButton = screen.getByTestId('openMigrationOptionsButton');
|
||||
fireEvent.click(optionsButton);
|
||||
|
||||
const renameButton = screen.getByTestId('renameMigrationItem');
|
||||
fireEvent.click(renameButton);
|
||||
|
||||
const input = screen.getByLabelText('Migration name');
|
||||
fireEvent.keyDown(input, { key: 'Escape' });
|
||||
|
||||
expect(screen.queryByLabelText('Migration name')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Test Migration')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should revert name on update error', () => {
|
||||
useUpdateMigrationMock.mockReturnValue({
|
||||
mutate: mockUpdateMigration,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
renderMigrationPanelTitle(mockMigrationStatsReady);
|
||||
|
||||
const optionsButton = screen.getByTestId('openMigrationOptionsButton');
|
||||
fireEvent.click(optionsButton);
|
||||
|
||||
const renameButton = screen.getByTestId('renameMigrationItem');
|
||||
fireEvent.click(renameButton);
|
||||
|
||||
const input = screen.getByLabelText('Migration name');
|
||||
fireEvent.change(input, { target: { value: 'New Name' } });
|
||||
fireEvent.keyDown(input, { key: 'Enter' });
|
||||
|
||||
expect(mockUpdateMigration).toHaveBeenCalledWith({ name: 'New Name' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Delete functionality', () => {
|
||||
it('should show delete confirmation modal when delete is clicked', () => {
|
||||
renderMigrationPanelTitle(mockMigrationStatsReady);
|
||||
|
||||
const optionsButton = screen.getByTestId('openMigrationOptionsButton');
|
||||
fireEvent.click(optionsButton);
|
||||
|
||||
const deleteButton = screen.getByTestId('deleteMigrationItem');
|
||||
fireEvent.click(deleteButton);
|
||||
|
||||
expect(screen.getByText(i18n.DELETE_MIGRATION_TITLE)).toBeInTheDocument();
|
||||
expect(screen.getByText(i18n.DELETE_MIGRATION_DESCRIPTION)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call delete migration when confirmed', () => {
|
||||
renderMigrationPanelTitle(mockMigrationStatsReady);
|
||||
|
||||
const optionsButton = screen.getByTestId('openMigrationOptionsButton');
|
||||
fireEvent.click(optionsButton);
|
||||
|
||||
const deleteButton = screen.getByTestId('deleteMigrationItem');
|
||||
fireEvent.click(deleteButton);
|
||||
|
||||
const confirmButton = screen.getByText(i18n.DELETE_MIGRATION_TEXT);
|
||||
fireEvent.click(confirmButton);
|
||||
|
||||
expect(mockDeleteMigration).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should close modal when cancel is clicked', () => {
|
||||
renderMigrationPanelTitle(mockMigrationStatsReady);
|
||||
|
||||
const optionsButton = screen.getByTestId('openMigrationOptionsButton');
|
||||
fireEvent.click(optionsButton);
|
||||
|
||||
const deleteButton = screen.getByTestId('deleteMigrationItem');
|
||||
fireEvent.click(deleteButton);
|
||||
|
||||
const cancelButton = screen.getByText(i18n.CANCEL_DELETE_MIGRATION_TEXT);
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
expect(screen.queryByText(i18n.DELETE_MIGRATION_TITLE)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Delete button state based on migration status', () => {
|
||||
it('should enable delete button for ready migration', () => {
|
||||
renderMigrationPanelTitle(mockMigrationStatsReady);
|
||||
|
||||
const optionsButton = screen.getByTestId('openMigrationOptionsButton');
|
||||
fireEvent.click(optionsButton);
|
||||
|
||||
const deleteButton = screen.getByTestId('deleteMigrationItem');
|
||||
expect(deleteButton).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('should disable delete button for running migration', () => {
|
||||
renderMigrationPanelTitle(mockMigrationStatsRunning);
|
||||
|
||||
const optionsButton = screen.getByTestId('openMigrationOptionsButton');
|
||||
fireEvent.click(optionsButton);
|
||||
|
||||
const deleteButton = screen.getByTestId('deleteMigrationItem');
|
||||
expect(deleteButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Event handling', () => {
|
||||
it('should prevent event propagation on options button click', () => {
|
||||
const mockStopPropagation = jest.fn();
|
||||
const originalStopPropagation = Event.prototype.stopPropagation;
|
||||
Event.prototype.stopPropagation = mockStopPropagation;
|
||||
|
||||
renderMigrationPanelTitle(mockMigrationStatsReady);
|
||||
|
||||
const optionsButton = screen.getByTestId('openMigrationOptionsButton');
|
||||
fireEvent.click(optionsButton);
|
||||
|
||||
expect(mockStopPropagation).toHaveBeenCalled();
|
||||
|
||||
Event.prototype.stopPropagation = originalStopPropagation;
|
||||
});
|
||||
|
||||
it('should prevent event propagation on inline edit click', () => {
|
||||
const mockStopPropagation = jest.fn();
|
||||
const originalStopPropagation = Event.prototype.stopPropagation;
|
||||
Event.prototype.stopPropagation = mockStopPropagation;
|
||||
|
||||
renderMigrationPanelTitle(mockMigrationStatsReady);
|
||||
|
||||
const optionsButton = screen.getByTestId('openMigrationOptionsButton');
|
||||
fireEvent.click(optionsButton);
|
||||
|
||||
const renameButton = screen.getByTestId('renameMigrationItem');
|
||||
fireEvent.click(renameButton);
|
||||
|
||||
const input = screen.getByLabelText('Migration name');
|
||||
fireEvent.click(input);
|
||||
|
||||
expect(mockStopPropagation).toHaveBeenCalled();
|
||||
|
||||
Event.prototype.stopPropagation = originalStopPropagation;
|
||||
});
|
||||
});
|
||||
});
|
|
@ -8,23 +8,29 @@
|
|||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiConfirmModal,
|
||||
EuiContextMenuItem,
|
||||
EuiContextMenuPanel,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiInlineEditText,
|
||||
EuiPopover,
|
||||
EuiToolTip,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { SiemMigrationTaskStatus } from '../../../../../common/siem_migrations/constants';
|
||||
import { useIsOpenState } from '../../../../common/hooks/use_is_open_state';
|
||||
import { PanelText } from '../../../../common/components/panel_text';
|
||||
import { useUpdateMigration } from '../../logic/use_update_migration';
|
||||
import type { RuleMigrationStats } from '../../types';
|
||||
import * as i18n from './translations';
|
||||
import { useDeleteMigration } from '../../logic/use_delete_migration';
|
||||
|
||||
interface MigrationPanelTitleProps {
|
||||
migrationStats: RuleMigrationStats;
|
||||
}
|
||||
export const MigrationPanelTitle = React.memo<MigrationPanelTitleProps>(({ migrationStats }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const [name, setName] = useState<string>(migrationStats.name);
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
const {
|
||||
|
@ -32,11 +38,19 @@ export const MigrationPanelTitle = React.memo<MigrationPanelTitleProps>(({ migra
|
|||
close: closePopover,
|
||||
toggle: togglePopover,
|
||||
} = useIsOpenState(false);
|
||||
const {
|
||||
isOpen: isDeleteModalOpen,
|
||||
open: openDeleteModal,
|
||||
close: closeDeleteModal,
|
||||
} = useIsOpenState(false);
|
||||
|
||||
const onRenameError = useCallback(() => {
|
||||
setName(migrationStats.name); // revert to original name on error. Error toast will be shown by the useUpdateMigration hook
|
||||
}, [migrationStats.name]);
|
||||
|
||||
const { mutate: deleteMigration, isLoading: isDeletingMigration } = useDeleteMigration(
|
||||
migrationStats.id
|
||||
);
|
||||
const { mutate: updateMigration, isLoading: isUpdatingMigrationName } = useUpdateMigration(
|
||||
migrationStats.id,
|
||||
{ onError: onRenameError }
|
||||
|
@ -55,23 +69,26 @@ export const MigrationPanelTitle = React.memo<MigrationPanelTitleProps>(({ migra
|
|||
[updateMigration]
|
||||
);
|
||||
|
||||
const items = useMemo(
|
||||
() => [
|
||||
<EuiContextMenuItem
|
||||
key="rename"
|
||||
onClick={() => {
|
||||
closePopover();
|
||||
setIsEditing(true);
|
||||
}}
|
||||
icon="pencil"
|
||||
data-test-subj="renameMigrationItem"
|
||||
>
|
||||
{i18n.RENAME_MIGRATION_BUTTON}
|
||||
</EuiContextMenuItem>,
|
||||
],
|
||||
[closePopover, setIsEditing]
|
||||
const confirmDeleteMigration = useCallback(() => {
|
||||
deleteMigration();
|
||||
closeDeleteModal();
|
||||
}, [deleteMigration, closeDeleteModal]);
|
||||
|
||||
const isDeletable = useMemo(
|
||||
() => migrationStats.status !== SiemMigrationTaskStatus.RUNNING,
|
||||
[migrationStats.status]
|
||||
);
|
||||
|
||||
const showRename = useCallback(() => {
|
||||
closePopover();
|
||||
setIsEditing(true);
|
||||
}, [closePopover]);
|
||||
|
||||
const showDelete = useCallback(() => {
|
||||
closePopover();
|
||||
openDeleteModal();
|
||||
}, [closePopover, openDeleteModal]);
|
||||
|
||||
const stopPropagation = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation(); // prevent click events from bubbling up and toggle the collapsible panel
|
||||
}, []);
|
||||
|
@ -104,7 +121,7 @@ export const MigrationPanelTitle = React.memo<MigrationPanelTitleProps>(({ migra
|
|||
onClick={togglePopover}
|
||||
aria-label={i18n.OPEN_MIGRATION_OPTIONS_BUTTON}
|
||||
data-test-subj="openMigrationOptionsButton"
|
||||
isLoading={isUpdatingMigrationName}
|
||||
isLoading={isUpdatingMigrationName || isDeletingMigration}
|
||||
/>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
|
@ -112,8 +129,40 @@ export const MigrationPanelTitle = React.memo<MigrationPanelTitleProps>(({ migra
|
|||
panelPaddingSize="none"
|
||||
anchorPosition="downCenter"
|
||||
>
|
||||
<EuiContextMenuPanel size="s" items={items} />
|
||||
<EuiContextMenuPanel size="s">
|
||||
<EuiContextMenuItem
|
||||
icon="pencil"
|
||||
onClick={showRename}
|
||||
data-test-subj="renameMigrationItem"
|
||||
>
|
||||
{i18n.RENAME_MIGRATION_TEXT}
|
||||
</EuiContextMenuItem>
|
||||
<EuiContextMenuItem
|
||||
icon="trash"
|
||||
onClick={showDelete}
|
||||
disabled={!isDeletable}
|
||||
css={{ color: isDeletable ? euiTheme.colors.danger : undefined }}
|
||||
data-test-subj="deleteMigrationItem"
|
||||
>
|
||||
<EuiToolTip content={isDeletable ? undefined : i18n.NOT_DELETABLE_MIGRATION_TEXT}>
|
||||
<span>{i18n.DELETE_BUTTON_TEXT}</span>
|
||||
</EuiToolTip>
|
||||
</EuiContextMenuItem>
|
||||
</EuiContextMenuPanel>
|
||||
</EuiPopover>
|
||||
{isDeleteModalOpen && (
|
||||
<EuiConfirmModal
|
||||
title={i18n.DELETE_MIGRATION_TITLE}
|
||||
onCancel={closeDeleteModal}
|
||||
onConfirm={confirmDeleteMigration}
|
||||
confirmButtonText={i18n.DELETE_MIGRATION_TEXT}
|
||||
cancelButtonText={i18n.CANCEL_DELETE_MIGRATION_TEXT}
|
||||
buttonColor="danger"
|
||||
isLoading={isDeletingMigration}
|
||||
>
|
||||
<p>{i18n.DELETE_MIGRATION_DESCRIPTION}</p>
|
||||
</EuiConfirmModal>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -20,9 +20,9 @@ const useStopMigrationMock = useStopMigration as jest.Mock;
|
|||
const mockStopMigration = jest.fn();
|
||||
|
||||
const inProgressMigrationStats: RuleMigrationStats = {
|
||||
name: 'test-migration',
|
||||
status: SiemMigrationTaskStatus.RUNNING,
|
||||
id: 'c44d2c7d-0de1-4231-8b82-0dcfd67a9fe3',
|
||||
name: 'test migration',
|
||||
rules: { total: 26, pending: 6, processing: 10, completed: 9, failed: 1 },
|
||||
created_at: '2025-05-27T12:12:17.563Z',
|
||||
last_updated_at: '2025-05-27T12:12:17.563Z',
|
||||
|
|
|
@ -150,6 +150,42 @@ export const OPEN_MIGRATION_OPTIONS_BUTTON = i18n.translate(
|
|||
'xpack.securitySolution.siemMigrations.rules.panel.openMigrationOptionsButton',
|
||||
{ defaultMessage: 'Open migration options' }
|
||||
);
|
||||
export const RENAME_MIGRATION_TEXT = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.panel.renameMigrationText',
|
||||
{ defaultMessage: 'Rename' }
|
||||
);
|
||||
|
||||
export const DELETE_BUTTON_TEXT = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.panel.deleteButtonText',
|
||||
{ defaultMessage: 'Delete' }
|
||||
);
|
||||
|
||||
export const DELETE_MIGRATION_TEXT = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.panel.deleteMigrationText',
|
||||
{ defaultMessage: 'Delete Migration' }
|
||||
);
|
||||
export const NOT_DELETABLE_MIGRATION_TEXT = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.panel.notDeletableMigrationText',
|
||||
{ defaultMessage: 'Can not delete running migrations' }
|
||||
);
|
||||
|
||||
export const CANCEL_DELETE_MIGRATION_TEXT = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.panel.cancelDeleteMigrationText',
|
||||
{ defaultMessage: 'Cancel' }
|
||||
);
|
||||
|
||||
export const DELETE_MIGRATION_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.panel.delete.title',
|
||||
{ defaultMessage: 'Are you sure you want to delete this migration?' }
|
||||
);
|
||||
|
||||
export const DELETE_MIGRATION_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.panel.delete.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'This action cannot be undone. All translations related to this migration will be removed permanently.',
|
||||
}
|
||||
);
|
||||
export const RENAME_MIGRATION_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.panel.renameMigrationButton',
|
||||
{ defaultMessage: 'Rename' }
|
||||
|
|
|
@ -68,3 +68,17 @@ export const UPDATE_MIGRATION_NAME_FAILURE = i18n.translate(
|
|||
defaultMessage: 'Failed to update migration name',
|
||||
}
|
||||
);
|
||||
|
||||
export const DELETE_MIGRATION_SUCCESS = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.deleteMigrationSuccess',
|
||||
{
|
||||
defaultMessage: 'Migration deleted',
|
||||
}
|
||||
);
|
||||
|
||||
export const DELETE_MIGRATION_FAILURE = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.deleteMigrationFailDescription',
|
||||
{
|
||||
defaultMessage: 'Failed to delete migration',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useKibana } from '../../../common/lib/kibana/kibana_react';
|
||||
import { SIEM_RULE_MIGRATION_PATH } from '../../../../common/siem_migrations/constants';
|
||||
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const DELETE_MIGRATION_RULE_MUTATION_KEY = ['DELETE', SIEM_RULE_MIGRATION_PATH];
|
||||
|
||||
export const useDeleteMigration = (migrationId: string) => {
|
||||
const { addError, addSuccess } = useAppToasts();
|
||||
const rulesMigrationService = useKibana().services.siemMigrations.rules;
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => rulesMigrationService.deleteMigration(migrationId),
|
||||
mutationKey: DELETE_MIGRATION_RULE_MUTATION_KEY,
|
||||
onSuccess: () => {
|
||||
addSuccess(i18n.DELETE_MIGRATION_SUCCESS);
|
||||
},
|
||||
onError: (error) => {
|
||||
addError(error, { title: i18n.DELETE_MIGRATION_FAILURE });
|
||||
},
|
||||
});
|
||||
};
|
|
@ -158,6 +158,22 @@ export class SiemRulesMigrationsService {
|
|||
}
|
||||
}
|
||||
|
||||
/** Deletes a rule migration by its ID, refreshing the stats to remove it from the list */
|
||||
public async deleteMigration(migrationId: string): Promise<string> {
|
||||
try {
|
||||
await api.deleteMigration({ migrationId });
|
||||
|
||||
// Refresh stats to remove the deleted migration from the list. All UI observables will be updated automatically
|
||||
await this.getRuleMigrationsStats();
|
||||
|
||||
this.telemetry.reportSetupMigrationDeleted({ migrationId });
|
||||
return migrationId;
|
||||
} catch (error) {
|
||||
this.telemetry.reportSetupMigrationDeleted({ migrationId, error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/** Upserts resources for a rule migration, batching the requests to avoid hitting the max payload size limit of the API */
|
||||
public async upsertMigrationResources(
|
||||
migrationId: string,
|
||||
|
|
|
@ -68,6 +68,15 @@ export class SiemRulesMigrationsTelemetry {
|
|||
});
|
||||
};
|
||||
|
||||
reportSetupMigrationDeleted = (params: { migrationId: string; error?: Error }) => {
|
||||
const { migrationId, error } = params;
|
||||
this.telemetryService.reportEvent(SiemMigrationsEventTypes.SetupMigrationDeleted, {
|
||||
eventName: siemMigrationEventNames[SiemMigrationsEventTypes.SetupMigrationDeleted],
|
||||
migrationId,
|
||||
...this.getBaseResultParams(error),
|
||||
});
|
||||
};
|
||||
|
||||
reportSetupResourceUploaded = (params: {
|
||||
migrationId: string;
|
||||
type: RuleMigrationResourceType;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue