Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* We use Jest for assertions and mocking. We also use Jest’s fake timers to simulate the polling loop.
*/

import type { CoreStart, Toast } from '@kbn/core/public';
import type { CoreStart } from '@kbn/core/public';
import { firstValueFrom } from 'rxjs';
import { SiemMigrationTaskStatus } from '../../../../common/siem_migrations/constants';
import type { StartPluginsDependencies } from '../../../types';
Expand Down Expand Up @@ -329,88 +329,4 @@ describe('SiemMigrationsServiceBase', () => {
});
});
});

describe('removeFinishedMigrationsNotification', () => {
let mockToastRemove: jest.Mock;

beforeEach(() => {
mockToastRemove = jest.fn();
mockNotifications.toasts.remove = mockToastRemove;
});

it('should remove specific migration notification when migrationId is provided', () => {
const migrationId = 'mig-1';
const mockToast = { id: 'toast-1', title: 'Migration Complete' };

service.toastsByMigrationId[migrationId] = mockToast as Toast;

service.removeFinishedMigrationsNotification(migrationId);

expect(mockToastRemove).toHaveBeenCalledWith(mockToast);
expect(service.toastsByMigrationId[migrationId]).toBeUndefined();
});

it('should not call remove when migrationId is provided but no toast exists', () => {
const migrationId = 'mig-1';

service.toastsByMigrationId = {};

service.removeFinishedMigrationsNotification(migrationId);

expect(mockToastRemove).not.toHaveBeenCalled();
});

it('should remove all migration notifications when no migrationId is provided', () => {
const mockToast1 = { id: 'toast-1', title: 'Migration 1 Complete' };
const mockToast2 = { id: 'toast-2', title: 'Migration 2 Complete' };

service.toastsByMigrationId = {
'mig-1': mockToast1 as Toast,
'mig-2': mockToast2 as Toast,
};

service.removeFinishedMigrationsNotification();

expect(mockToastRemove).toHaveBeenCalledTimes(2);
expect(mockToastRemove).toHaveBeenCalledWith(mockToast1);
expect(mockToastRemove).toHaveBeenCalledWith(mockToast2);
expect(service.toastsByMigrationId).toEqual({});
});

it('should not call remove when no migrationId is provided and no toasts exist', () => {
service.toastsByMigrationId = {};

service.removeFinishedMigrationsNotification();

expect(mockToastRemove).not.toHaveBeenCalled();
});

it('should handle empty toastsByMigrationId object when no migrationId is provided', () => {
service.toastsByMigrationId = {};

service.removeFinishedMigrationsNotification();

expect(mockToastRemove).not.toHaveBeenCalled();
expect(service.toastsByMigrationId).toEqual({});
});

it('should only remove the specific migration toast when migrationId is provided, leaving others intact', () => {
const migrationId = 'mig-1';
const mockToast1 = { id: 'toast-1', title: 'Migration 1 Complete' };
const mockToast2 = { id: 'toast-2', title: 'Migration 2 Complete' };

service.toastsByMigrationId = {
'mig-1': mockToast1 as Toast,
'mig-2': mockToast2 as Toast,
};

service.removeFinishedMigrationsNotification(migrationId);

expect(mockToastRemove).toHaveBeenCalledTimes(1);
expect(mockToastRemove).toHaveBeenCalledWith(mockToast1);
expect(service.toastsByMigrationId).toEqual({
'mig-2': mockToast2,
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { isEqual } from 'lodash';
import { BehaviorSubject, distinctUntilChanged, type Observable } from 'rxjs';
import type { CoreStart, Toast } from '@kbn/core/public';
import type { CoreStart } from '@kbn/core/public';
import type { TraceOptions } from '@kbn/elastic-assistant/impl/assistant/types';
import {
DEFAULT_ASSISTANT_NAMESPACE,
Expand Down Expand Up @@ -43,7 +43,6 @@ export abstract class SiemMigrationsServiceBase<T extends MigrationTaskStats> {
private isPolling = false;
public connectorIdStorage: MigrationsStorage<string>;
public traceOptionsStorage: MigrationsStorage<TraceOptions>;
public toastsByMigrationId: Record<string, Toast>;

constructor(
protected readonly core: CoreStart,
Expand All @@ -56,7 +55,7 @@ export abstract class SiemMigrationsServiceBase<T extends MigrationTaskStats> {
});

this.latestStats$ = new BehaviorSubject<T[] | null>(null);
this.toastsByMigrationId = {};

this.plugins.spaces.getActiveSpace().then((space) => {
this.connectorIdStorage.setSpaceId(space.id);
this.startPolling();
Expand Down Expand Up @@ -204,18 +203,4 @@ export abstract class SiemMigrationsServiceBase<T extends MigrationTaskStats> {
}
} while (pendingMigrationIds.length > 0);
}

public removeFinishedMigrationsNotification(migrationId?: string) {
if (migrationId && this.toastsByMigrationId[migrationId]) {
this.core.notifications.toasts.remove(this.toastsByMigrationId[migrationId]);
delete this.toastsByMigrationId[migrationId];
} else {
if (Object.values(this.toastsByMigrationId).length > 0) {
Object.values(this.toastsByMigrationId).forEach((toast) => {
this.core.notifications.toasts.remove(toast);
});
this.toastsByMigrationId = {};
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import * as api from '../api';
import { getMissingCapabilitiesToast } from '../../common/service/notifications/missing_capabilities_notification';
import { getNoConnectorToast } from '../../common/service/notifications/no_connector_notification';
import { SiemMigrationTaskStatus } from '../../../../common/siem_migrations/constants';
import { getSuccessToast } from './notification/success_notification';
import { raiseSuccessToast } from './notification/success_notification';
import type { CapabilitiesLevel, MissingCapability } from '../../common/service/capabilities';
import { getMissingCapabilitiesChecker } from '../../common/service/capabilities';
import { requiredDashboardMigrationCapabilities } from './capabilities';
Expand Down Expand Up @@ -221,9 +221,7 @@ export class SiemDashboardMigrationsService extends SiemMigrationsServiceBase<Da
}

protected sendFinishedMigrationNotification(taskStats: DashboardMigrationStats) {
const toast = getSuccessToast(taskStats, this.core, this);
this.toastsByMigrationId[taskStats.id] = this.core.notifications.toasts.addSuccess(toast);
return toast;
raiseSuccessToast(taskStats, this.core);
}

/** Deletes a dashboard migration by its ID, refreshing the stats to remove it from the list */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,41 @@ import {
useNavigation,
NavigationProvider,
} from '@kbn/security-solution-navigation';
import type { ToastInput } from '@kbn/core-notifications-browser';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import type { SiemDashboardMigrationsService } from '../dashboard_migrations_service';
import type { DashboardMigrationTaskStats } from '../../../../../common/siem_migrations/model/dashboard_migration.gen';

export const getSuccessToast = (
export const raiseSuccessToast = (
migrationStats: DashboardMigrationTaskStats,
core: CoreStart,
service: SiemDashboardMigrationsService
): ToastInput => ({
color: 'success',
iconType: 'check',
toastLifeTimeMs: 1000 * 60 * 30, // 30 minutes
title: i18n.translate(
'xpack.securitySolution.siemMigrations.dashboardsService.polling.successTitle',
{
defaultMessage: 'Dashboards translation complete.',
}
),
text: toMountPoint(
<NavigationProvider core={core}>
<SuccessToastContent migrationStats={migrationStats} service={service} />
</NavigationProvider>,
core
),
});
core: CoreStart
): void => {
const toast = core.notifications.toasts.addSuccess({
color: 'success',
iconType: 'check',
toastLifeTimeMs: 1000 * 60 * 30, // 30 minutes
title: i18n.translate(
'xpack.securitySolution.siemMigrations.dashboardsService.polling.successTitle',
{
defaultMessage: 'Dashboards translation complete.',
}
),
text: toMountPoint(
<NavigationProvider core={core}>
<SuccessToastContent
migrationStats={migrationStats}
dismissHandler={() => core.notifications.toasts.remove(toast)}
/>
</NavigationProvider>,
core
),
});
};

const SuccessToastContent: React.FC<{
migrationStats: DashboardMigrationTaskStats;
service: SiemDashboardMigrationsService;
}> = ({ migrationStats, service }) => {
dismissHandler: () => void;
}> = ({ migrationStats, dismissHandler }) => {
const { navigateTo, getAppUrl } = useNavigation();

const navParams = useMemo(() => {
Expand All @@ -59,9 +61,9 @@ const SuccessToastContent: React.FC<{
deepLinkId: SecurityPageName.siemMigrationsDashboards,
path: migrationStats.id,
});
service.removeFinishedMigrationsNotification(migrationStats.id);
dismissHandler();
},
[navigateTo, migrationStats.id, service]
[navigateTo, migrationStats.id, dismissHandler]
);
const url = useMemo(() => getAppUrl(navParams), [getAppUrl, navParams]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import React from 'react';
import { render } from '@testing-library/react';
import { coreMock } from '@kbn/core/public/mocks';
import { useNavigation } from '@kbn/security-solution-navigation';
import { SuccessToastContent, getSuccessToast } from './success_notification';
import { SuccessToastContent, raiseSuccessToast } from './success_notification';
import { getRuleMigrationStatsMock } from '../../__mocks__';
import { TestProviders } from '../../../../common/mock';

Expand Down Expand Up @@ -69,7 +69,7 @@ describe('Success Notification', () => {
describe('getSuccessToast', () => {
it('returns a toast object with the correct properties', () => {
const migration = getRuleMigrationStatsMock();
const toast = getSuccessToast(migration, coreMock.createStart());
const toast = raiseSuccessToast(migration, coreMock.createStart());

expect(toast).toEqual({
color: 'success',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,62 +5,55 @@
* 2.0.
*/

import React, { useMemo, useCallback } from 'react';
import React from 'react';
import type { CoreStart } from '@kbn/core-lifecycle-browser';
import { i18n } from '@kbn/i18n';
import {
SecurityPageName,
useNavigation,
NavigationProvider,
} from '@kbn/security-solution-navigation';
import type { ToastInput } from '@kbn/core-notifications-browser';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import type { RuleMigrationStats } from '../../types';
import type { SiemRulesMigrationsService } from '../rule_migrations_service';

export const getSuccessToast = (
migration: RuleMigrationStats,
core: CoreStart,
service: SiemRulesMigrationsService
): ToastInput => ({
color: 'success',
iconType: 'check',
toastLifeTimeMs: 1000 * 60 * 30, // 30 minutes
title: i18n.translate('xpack.securitySolution.siemMigrations.rulesService.polling.successTitle', {
defaultMessage: 'Rules translation complete.',
}),
text: toMountPoint(
<NavigationProvider core={core}>
<SuccessToastContent migration={migration} service={service} />
</NavigationProvider>,
core
),
});
export const raiseSuccessToast = (migration: RuleMigrationStats, core: CoreStart) => {
const toast = core.notifications.toasts.addSuccess({
color: 'success',
iconType: 'check',
toastLifeTimeMs: 1000 * 60 * 30, // 30 minutes
title: i18n.translate(
'xpack.securitySolution.siemMigrations.rulesService.polling.successTitle',
{
defaultMessage: 'Rules translation complete.',
}
),
text: toMountPoint(
<NavigationProvider core={core}>
<SuccessToastContent
migration={migration}
dismissHandler={() => core.notifications.toasts.remove(toast)}
/>
</NavigationProvider>,
core
),
});
};

export const SuccessToastContent: React.FC<{
migration: RuleMigrationStats;
service: SiemRulesMigrationsService;
}> = ({ migration, service }) => {
const { navigateTo, getAppUrl } = useNavigation();
dismissHandler: () => void;
}> = ({ migration, dismissHandler }) => {
const navigation = { deepLinkId: SecurityPageName.siemMigrationsRules, path: migration.id };

const navParams = useMemo(() => {
return { deepLinkId: SecurityPageName.siemMigrationsRules, path: migration.id };
}, [migration.id]);

const onClick: React.MouseEventHandler = useCallback(
(ev) => {
ev.preventDefault();
navigateTo({
deepLinkId: SecurityPageName.siemMigrationsRules,
path: migration.id,
});
service.removeFinishedMigrationsNotification(migration.id);
},
[navigateTo, migration.id, service]
);
const url = useMemo(() => getAppUrl(navParams), [getAppUrl, navParams]);
const { navigateTo, getAppUrl } = useNavigation();
const onClick: React.MouseEventHandler = (ev) => {
ev.preventDefault();
navigateTo(navigation);
dismissHandler();
};
const url = getAppUrl(navigation);

return (
<EuiFlexGroup direction="column" alignItems="flexEnd" gutterSize="s">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
getNoConnectorToast,
} from '../../common/service';
import type { GetMigrationStatsParams, GetMigrationsStatsAllParams } from '../../common/types';
import { getSuccessToast } from './notification/success_notification';
import { raiseSuccessToast } from './notification/success_notification';
import { START_STOP_POLLING_SLEEP_SECONDS } from '../../common/constants';

const CREATE_MIGRATION_BODY_BATCH_SIZE = 50;
Expand Down Expand Up @@ -238,8 +238,6 @@ export class SiemRulesMigrationsService extends SiemMigrationsServiceBase<RuleMi
}

protected sendFinishedMigrationNotification(taskStats: RuleMigrationStats) {
const toast = getSuccessToast(taskStats, this.core, this);
this.toastsByMigrationId[taskStats.id] = this.core.notifications.toasts.addSuccess(toast);
return toast;
raiseSuccessToast(taskStats, this.core);
}
}
Loading