-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Siem migrations] Implement UI service and migrations polling (#201503)
## Summary Sends "Rule migration complete" notifications from anywhere in the Security Solution app, whenever a rule migration finishes, with a link to the migrated rules. The polling logic has been encapsulated in the new `siemMigrations.rules` service so the request loop is centralized in one place. The value updates are broadcasted using the `latestStats$` observable. It will only keep requesting while there are _running_ migrations and will stop automatically when no more migrations are _running_. The reusable `useLatestStats` hook has been created for the UI components to consume. This approach allows multiple components to listen and update their content automatically with every rule migration stats update, having only one request loop running. The polling will only start if it's not already running and only if the SIEM migration functionality is available, which includes: - Experimental flag enabled - _Enterprise_ license - TODO: feature capability check (RBAC [issue](elastic/security-team#11262)) The polling will try to start when: - Automatically with the Security Solution application starts - The first render of every page that uses `useLatestStats` hook. - TODO: A new migration is created from the onboarding page ([issue](elastic/security-team#10667)) Tests will be implemented in [this task](elastic/security-team#11256) ## Example A Rule migration finishes while using Timeline in the Alerts page: https://github.com/user-attachments/assets/aac2b2c8-27fe-40d5-9f32-0bee74c9dc6a
- Loading branch information
Showing
16 changed files
with
278 additions
and
89 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
30 changes: 0 additions & 30 deletions
30
...rity_solution/public/siem_migrations/rules/api/hooks/use_get_rule_migrations_stats_all.ts
This file was deleted.
Oops, something went wrong.
23 changes: 23 additions & 0 deletions
23
x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_latest_stats.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
/* | ||
* 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 useObservable from 'react-use/lib/useObservable'; | ||
import { useEffect, useMemo } from 'react'; | ||
import { useKibana } from '../../../common/lib/kibana'; | ||
|
||
export const useLatestStats = () => { | ||
const { siemMigrations } = useKibana().services; | ||
|
||
useEffect(() => { | ||
siemMigrations.rules.startPolling(); | ||
}, [siemMigrations.rules]); | ||
|
||
const latestStats$ = useMemo(() => siemMigrations.rules.getLatestStats$(), [siemMigrations]); | ||
const latestStats = useObservable(latestStats$, null); | ||
|
||
return { data: latestStats ?? [], isLoading: latestStats === null }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
87 changes: 87 additions & 0 deletions
87
...plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
/* | ||
* 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 { BehaviorSubject, type Observable } from 'rxjs'; | ||
import type { CoreStart } from '@kbn/core/public'; | ||
import { i18n } from '@kbn/i18n'; | ||
import { ExperimentalFeaturesService } from '../../../common/experimental_features_service'; | ||
import { licenseService } from '../../../common/hooks/use_license'; | ||
import { getRuleMigrationsStatsAll } from '../api/api'; | ||
import type { RuleMigrationStats } from '../types'; | ||
import { getSuccessToast } from './success_notification'; | ||
|
||
const POLLING_ERROR_TITLE = i18n.translate( | ||
'xpack.securitySolution.siemMigrations.rulesService.polling.errorTitle', | ||
{ defaultMessage: 'Error fetching rule migrations' } | ||
); | ||
|
||
export class SiemRulesMigrationsService { | ||
private readonly pollingInterval = 5000; | ||
private readonly latestStats$: BehaviorSubject<RuleMigrationStats[]>; | ||
private isPolling = false; | ||
|
||
constructor(private readonly core: CoreStart) { | ||
this.latestStats$ = new BehaviorSubject<RuleMigrationStats[]>([]); | ||
this.startPolling(); | ||
} | ||
|
||
public getLatestStats$(): Observable<RuleMigrationStats[]> { | ||
return this.latestStats$.asObservable(); | ||
} | ||
|
||
public isAvailable() { | ||
return ExperimentalFeaturesService.get().siemMigrationsEnabled && licenseService.isEnterprise(); | ||
} | ||
|
||
public startPolling() { | ||
if (this.isPolling || !this.isAvailable()) { | ||
return; | ||
} | ||
|
||
this.isPolling = true; | ||
this.startStatsPolling() | ||
.catch((e) => { | ||
this.core.notifications.toasts.addError(e, { title: POLLING_ERROR_TITLE }); | ||
}) | ||
.finally(() => { | ||
this.isPolling = false; | ||
}); | ||
} | ||
|
||
private async startStatsPolling(): Promise<void> { | ||
let pendingMigrationIds: string[] = []; | ||
do { | ||
const results = await this.fetchRuleMigrationsStats(); | ||
this.latestStats$.next(results); | ||
|
||
if (pendingMigrationIds.length > 0) { | ||
// send notifications for finished migrations | ||
pendingMigrationIds.forEach((pendingMigrationId) => { | ||
const migration = results.find((item) => item.id === pendingMigrationId); | ||
if (migration && migration.status === 'finished') { | ||
this.core.notifications.toasts.addSuccess(getSuccessToast(migration, this.core)); | ||
} | ||
}); | ||
} | ||
|
||
// reassign pending migrations | ||
pendingMigrationIds = results.reduce<string[]>((acc, item) => { | ||
if (item.status === 'running') { | ||
acc.push(item.id); | ||
} | ||
return acc; | ||
}, []); | ||
|
||
await new Promise((resolve) => setTimeout(resolve, this.pollingInterval)); | ||
} while (pendingMigrationIds.length > 0); | ||
} | ||
|
||
private async fetchRuleMigrationsStats(): Promise<RuleMigrationStats[]> { | ||
const stats = await getRuleMigrationsStatsAll({ signal: new AbortController().signal }); | ||
return stats.map((stat, index) => ({ ...stat, number: index + 1 })); // the array order (by creation) is guaranteed by the API | ||
} | ||
} |
67 changes: 67 additions & 0 deletions
67
...k/plugins/security_solution/public/siem_migrations/rules/service/success_notification.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
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'; | ||
|
||
export const getSuccessToast = (migration: RuleMigrationStats, core: CoreStart): 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} /> | ||
</NavigationProvider>, | ||
core | ||
), | ||
}); | ||
|
||
const SuccessToastContent: React.FC<{ migration: RuleMigrationStats }> = ({ migration }) => { | ||
const navigation = { deepLinkId: SecurityPageName.siemMigrationsRules, path: migration.id }; | ||
|
||
const { navigateTo, getAppUrl } = useNavigation(); | ||
const onClick: React.MouseEventHandler = (ev) => { | ||
ev.preventDefault(); | ||
navigateTo(navigation); | ||
}; | ||
const url = getAppUrl(navigation); | ||
|
||
return ( | ||
<EuiFlexGroup direction="column" alignItems="flexEnd" gutterSize="s"> | ||
<EuiFlexItem> | ||
<FormattedMessage | ||
id="xpack.securitySolution.siemMigrations.rulesService.polling.successText" | ||
defaultMessage="SIEM rules migration #{number} has finished translating. Results have been added to a dedicated page." | ||
values={{ number: migration.number }} | ||
/> | ||
</EuiFlexItem> | ||
<EuiFlexItem> | ||
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */} | ||
<EuiButton onClick={onClick} href={url} color="success"> | ||
{i18n.translate( | ||
'xpack.securitySolution.siemMigrations.rulesService.polling.successLinkText', | ||
{ defaultMessage: 'Go to translated rules' } | ||
)} | ||
</EuiButton> | ||
</EuiFlexItem> | ||
</EuiFlexGroup> | ||
); | ||
}; |
Oops, something went wrong.