mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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](https://github.com/elastic/security-team/issues/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](https://github.com/elastic/security-team/issues/10667)) Tests will be implemented in [this task](https://github.com/elastic/security-team/issues/11256) ## Example A Rule migration finishes while using Timeline in the Alerts page: https://github.com/user-attachments/assets/aac2b2c8-27fe-40d5-9f32-0bee74c9dc6a
This commit is contained in:
parent
eb41db27b1
commit
b6586a95f2
16 changed files with 278 additions and 89 deletions
|
@ -22,9 +22,8 @@ import {
|
|||
ElasticRulePartial,
|
||||
RuleMigrationTranslationResult,
|
||||
RuleMigrationComments,
|
||||
RuleMigrationAllTaskStats,
|
||||
RuleMigration,
|
||||
RuleMigrationTaskStats,
|
||||
RuleMigration,
|
||||
RuleMigrationResourceData,
|
||||
RuleMigrationResourceType,
|
||||
RuleMigrationResource,
|
||||
|
@ -44,7 +43,7 @@ export const CreateRuleMigrationResponse = z.object({
|
|||
});
|
||||
|
||||
export type GetAllStatsRuleMigrationResponse = z.infer<typeof GetAllStatsRuleMigrationResponse>;
|
||||
export const GetAllStatsRuleMigrationResponse = RuleMigrationAllTaskStats;
|
||||
export const GetAllStatsRuleMigrationResponse = z.array(RuleMigrationTaskStats);
|
||||
|
||||
export type GetRuleMigrationRequestParams = z.infer<typeof GetRuleMigrationRequestParams>;
|
||||
export const GetRuleMigrationRequestParams = z.object({
|
||||
|
|
|
@ -93,7 +93,9 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationAllTaskStats'
|
||||
type: array
|
||||
items:
|
||||
$ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationTaskStats'
|
||||
|
||||
## Specific rule migration APIs
|
||||
|
||||
|
|
|
@ -191,6 +191,10 @@ export const RuleMigration = z
|
|||
*/
|
||||
export type RuleMigrationTaskStats = z.infer<typeof RuleMigrationTaskStats>;
|
||||
export const RuleMigrationTaskStats = z.object({
|
||||
/**
|
||||
* The migration id
|
||||
*/
|
||||
id: NonEmptyString,
|
||||
/**
|
||||
* Indicates if the migration task status.
|
||||
*/
|
||||
|
@ -220,24 +224,16 @@ export const RuleMigrationTaskStats = z.object({
|
|||
*/
|
||||
failed: z.number().int(),
|
||||
}),
|
||||
/**
|
||||
* The moment the migration was created.
|
||||
*/
|
||||
created_at: z.string(),
|
||||
/**
|
||||
* The moment of the last update.
|
||||
*/
|
||||
last_updated_at: z.string().optional(),
|
||||
last_updated_at: z.string(),
|
||||
});
|
||||
|
||||
export type RuleMigrationAllTaskStats = z.infer<typeof RuleMigrationAllTaskStats>;
|
||||
export const RuleMigrationAllTaskStats = z.array(
|
||||
RuleMigrationTaskStats.merge(
|
||||
z.object({
|
||||
/**
|
||||
* The migration id
|
||||
*/
|
||||
migration_id: NonEmptyString,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* The type of the rule migration resource.
|
||||
*/
|
||||
|
|
|
@ -145,9 +145,15 @@ components:
|
|||
type: object
|
||||
description: The rule migration task stats object.
|
||||
required:
|
||||
- id
|
||||
- status
|
||||
- rules
|
||||
- created_at
|
||||
- last_updated_at
|
||||
properties:
|
||||
id:
|
||||
description: The migration id
|
||||
$ref: './common.schema.yaml#/components/schemas/NonEmptyString'
|
||||
status:
|
||||
type: string
|
||||
description: Indicates if the migration task status.
|
||||
|
@ -181,23 +187,13 @@ components:
|
|||
failed:
|
||||
type: integer
|
||||
description: The number of rules that have failed migration.
|
||||
created_at:
|
||||
type: string
|
||||
description: The moment the migration was created.
|
||||
last_updated_at:
|
||||
type: string
|
||||
description: The moment of the last update.
|
||||
|
||||
RuleMigrationAllTaskStats:
|
||||
type: array
|
||||
items:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/RuleMigrationTaskStats'
|
||||
- type: object
|
||||
required:
|
||||
- migration_id
|
||||
properties:
|
||||
migration_id:
|
||||
description: The migration id
|
||||
$ref: './common.schema.yaml#/components/schemas/NonEmptyString'
|
||||
|
||||
RuleMigrationTranslationResult:
|
||||
type: string
|
||||
description: The rule translation result.
|
||||
|
|
|
@ -19,6 +19,7 @@ import type { ConfigSettings } from '../common/config_settings';
|
|||
import { parseConfigSettings } from '../common/config_settings';
|
||||
import { APP_UI_ID } from '../common/constants';
|
||||
import { TopValuesPopoverService } from './app/components/top_values_popover/top_values_popover_service';
|
||||
import { createSiemMigrationsService } from './siem_migrations/service';
|
||||
import type { SecuritySolutionUiConfigType } from './common/types';
|
||||
import type {
|
||||
PluginStart,
|
||||
|
@ -152,6 +153,7 @@ export class PluginServices {
|
|||
customDataService,
|
||||
timelineDataService,
|
||||
topValuesPopover: new TopValuesPopoverService(),
|
||||
siemMigrations: await createSiemMigrationsService(coreStart),
|
||||
...(params && {
|
||||
onAppLeave: params.onAppLeave,
|
||||
setHeaderActionMenu: params.setHeaderActionMenu,
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
* 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 type { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { DEFAULT_QUERY_OPTIONS } from './constants';
|
||||
import { getRuleMigrationsStatsAll } from '../api';
|
||||
import type { GetAllStatsRuleMigrationResponse } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
|
||||
import { SIEM_RULE_MIGRATIONS_ALL_STATS_PATH } from '../../../../../common/siem_migrations/constants';
|
||||
|
||||
export const GET_RULE_MIGRATIONS_STATS_ALL_QUERY_KEY = ['GET', SIEM_RULE_MIGRATIONS_ALL_STATS_PATH];
|
||||
|
||||
export const useGetRuleMigrationsStatsAllQuery = (
|
||||
options?: UseQueryOptions<GetAllStatsRuleMigrationResponse>
|
||||
) => {
|
||||
return useQuery<GetAllStatsRuleMigrationResponse>(
|
||||
GET_RULE_MIGRATIONS_STATS_ALL_QUERY_KEY,
|
||||
async ({ signal }) => {
|
||||
return getRuleMigrationsStatsAll({ signal });
|
||||
},
|
||||
{
|
||||
...DEFAULT_QUERY_OPTIONS,
|
||||
...options,
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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 };
|
||||
};
|
|
@ -9,23 +9,20 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|||
|
||||
import { EuiSkeletonLoading, EuiSkeletonText, EuiSkeletonTitle } from '@elastic/eui';
|
||||
import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import { SecurityPageName } from '../../../app/types';
|
||||
import { HeaderPage } from '../../../common/components/header_page';
|
||||
import { SecuritySolutionPageWrapper } from '../../../common/components/page_wrapper';
|
||||
import { SpyRoute } from '../../../common/utils/route/spy_routes';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { RulesTable } from '../components/rules_table';
|
||||
import { NeedAdminForUpdateRulesCallOut } from '../../../detections/components/callouts/need_admin_for_update_callout';
|
||||
import { MissingPrivilegesCallOut } from '../../../detections/components/callouts/missing_privileges_callout';
|
||||
import { HeaderButtons } from '../components/header_buttons';
|
||||
import { useGetRuleMigrationsStatsAllQuery } from '../api/hooks/use_get_rule_migrations_stats_all';
|
||||
import { useRulePreviewFlyout } from '../hooks/use_rule_preview_flyout';
|
||||
import { NoMigrations } from '../components/no_migrations';
|
||||
import { useLatestStats } from '../hooks/use_latest_stats';
|
||||
|
||||
const RulesPageComponent: React.FC = () => {
|
||||
const { data: ruleMigrationsStatsAll, isLoading: isLoadingMigrationsStats } =
|
||||
useGetRuleMigrationsStatsAllQuery();
|
||||
export const RulesPage = React.memo(() => {
|
||||
const { data: ruleMigrationsStatsAll, isLoading: isLoadingMigrationsStats } = useLatestStats();
|
||||
|
||||
const migrationsIds = useMemo(() => {
|
||||
if (isLoadingMigrationsStats || !ruleMigrationsStatsAll?.length) {
|
||||
|
@ -33,7 +30,7 @@ const RulesPageComponent: React.FC = () => {
|
|||
}
|
||||
return ruleMigrationsStatsAll
|
||||
.filter((migration) => migration.status === 'finished')
|
||||
.map((migration) => migration.migration_id);
|
||||
.map((migration) => migration.id);
|
||||
}, [isLoadingMigrationsStats, ruleMigrationsStatsAll]);
|
||||
|
||||
const [selectedMigrationId, setSelectedMigrationId] = useState<string | undefined>();
|
||||
|
@ -94,11 +91,7 @@ const RulesPageComponent: React.FC = () => {
|
|||
/>
|
||||
{rulePreviewFlyout}
|
||||
</SecuritySolutionPageWrapper>
|
||||
|
||||
<SpyRoute pageName={SecurityPageName.siemMigrationsRules} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const RulesPage = React.memo(RulesPageComponent);
|
||||
});
|
||||
RulesPage.displayName = 'RulesPage';
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 type { RuleMigrationTaskStats } from '../../../common/siem_migrations/model/rule_migration.gen';
|
||||
|
||||
export interface RuleMigrationStats extends RuleMigrationTaskStats {
|
||||
/** The sequential number of the migration */
|
||||
number: number;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 type { CoreStart } from '@kbn/core-lifecycle-browser';
|
||||
|
||||
export type { SiemMigrationsService } from './siem_migrations_service';
|
||||
|
||||
export const createSiemMigrationsService = async (coreStart: CoreStart) => {
|
||||
const { SiemMigrationsService } = await import(
|
||||
/* webpackChunkName: "lazySiemMigrationsService" */
|
||||
'./siem_migrations_service'
|
||||
);
|
||||
return new SiemMigrationsService(coreStart);
|
||||
};
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 type { CoreStart } from '@kbn/core/public';
|
||||
import { SiemRulesMigrationsService } from '../rules/service/rule_migrations_service';
|
||||
|
||||
export class SiemMigrationsService {
|
||||
public rules: SiemRulesMigrationsService;
|
||||
|
||||
constructor(coreStart: CoreStart) {
|
||||
this.rules = new SiemRulesMigrationsService(coreStart);
|
||||
}
|
||||
}
|
|
@ -94,6 +94,7 @@ import type { ConfigSettings } from '../common/config_settings';
|
|||
import type { OnboardingService } from './onboarding/service';
|
||||
import type { SolutionNavigation } from './app/solution_navigation/solution_navigation';
|
||||
import type { TelemetryServiceStart } from './common/lib/telemetry';
|
||||
import type { SiemMigrationsService } from './siem_migrations/service';
|
||||
|
||||
export interface SetupPlugins {
|
||||
cloud?: CloudSetup;
|
||||
|
@ -193,6 +194,7 @@ export type StartServices = CoreStart &
|
|||
customDataService: DataPublicPluginStart;
|
||||
topValuesPopover: TopValuesPopoverService;
|
||||
timelineDataService: DataPublicPluginStart;
|
||||
siemMigrations: SiemMigrationsService;
|
||||
};
|
||||
|
||||
export type StartRenderServices = Pick<
|
||||
|
|
|
@ -6,8 +6,10 @@
|
|||
*/
|
||||
|
||||
import type {
|
||||
AggregationsAggregationContainer,
|
||||
AggregationsFilterAggregate,
|
||||
AggregationsMaxAggregate,
|
||||
AggregationsMinAggregate,
|
||||
AggregationsStringTermsAggregate,
|
||||
AggregationsStringTermsBucket,
|
||||
QueryDslQueryContainer,
|
||||
|
@ -30,7 +32,7 @@ export type UpdateRuleMigrationInput = { elastic_rule?: Partial<ElasticRule> } &
|
|||
'id' | 'translation_result' | 'comments'
|
||||
>;
|
||||
export type RuleMigrationDataStats = Omit<RuleMigrationTaskStats, 'status'>;
|
||||
export type RuleMigrationAllDataStats = Array<RuleMigrationDataStats & { migration_id: string }>;
|
||||
export type RuleMigrationAllDataStats = RuleMigrationDataStats[];
|
||||
|
||||
/* BULK_MAX_SIZE defines the number to break down the bulk operations by.
|
||||
* The 500 number was chosen as a reasonable number to avoid large payloads. It can be adjusted if needed.
|
||||
|
@ -217,6 +219,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
|
|||
processing: { filter: { term: { status: SiemMigrationStatus.PROCESSING } } },
|
||||
completed: { filter: { term: { status: SiemMigrationStatus.COMPLETED } } },
|
||||
failed: { filter: { term: { status: SiemMigrationStatus.FAILED } } },
|
||||
createdAt: { min: { field: '@timestamp' } },
|
||||
lastUpdatedAt: { max: { field: 'updated_at' } },
|
||||
};
|
||||
const result = await this.esClient
|
||||
|
@ -226,30 +229,33 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
|
|||
throw error;
|
||||
});
|
||||
|
||||
const { pending, processing, completed, lastUpdatedAt, failed } = result.aggregations ?? {};
|
||||
const bucket = result.aggregations ?? {};
|
||||
return {
|
||||
id: migrationId,
|
||||
rules: {
|
||||
total: this.getTotalHits(result),
|
||||
pending: (pending as AggregationsFilterAggregate)?.doc_count ?? 0,
|
||||
processing: (processing as AggregationsFilterAggregate)?.doc_count ?? 0,
|
||||
completed: (completed as AggregationsFilterAggregate)?.doc_count ?? 0,
|
||||
failed: (failed as AggregationsFilterAggregate)?.doc_count ?? 0,
|
||||
pending: (bucket.pending as AggregationsFilterAggregate)?.doc_count ?? 0,
|
||||
processing: (bucket.processing as AggregationsFilterAggregate)?.doc_count ?? 0,
|
||||
completed: (bucket.completed as AggregationsFilterAggregate)?.doc_count ?? 0,
|
||||
failed: (bucket.failed as AggregationsFilterAggregate)?.doc_count ?? 0,
|
||||
},
|
||||
last_updated_at: (lastUpdatedAt as AggregationsMaxAggregate)?.value_as_string,
|
||||
created_at: (bucket.createdAt as AggregationsMinAggregate)?.value_as_string ?? '',
|
||||
last_updated_at: (bucket.lastUpdatedAt as AggregationsMaxAggregate)?.value_as_string ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
/** Retrieves the stats for all the rule migrations aggregated by migration id */
|
||||
/** Retrieves the stats for all the rule migrations aggregated by migration id, in creation order */
|
||||
async getAllStats(): Promise<RuleMigrationAllDataStats> {
|
||||
const index = await this.getIndexName();
|
||||
const aggregations = {
|
||||
const aggregations: { migrationIds: AggregationsAggregationContainer } = {
|
||||
migrationIds: {
|
||||
terms: { field: 'migration_id' },
|
||||
terms: { field: 'migration_id', order: { createdAt: 'asc' } },
|
||||
aggregations: {
|
||||
pending: { filter: { term: { status: SiemMigrationStatus.PENDING } } },
|
||||
processing: { filter: { term: { status: SiemMigrationStatus.PROCESSING } } },
|
||||
completed: { filter: { term: { status: SiemMigrationStatus.COMPLETED } } },
|
||||
failed: { filter: { term: { status: SiemMigrationStatus.FAILED } } },
|
||||
createdAt: { min: { field: '@timestamp' } },
|
||||
lastUpdatedAt: { max: { field: 'updated_at' } },
|
||||
},
|
||||
},
|
||||
|
@ -264,7 +270,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
|
|||
const migrationsAgg = result.aggregations?.migrationIds as AggregationsStringTermsAggregate;
|
||||
const buckets = (migrationsAgg?.buckets as AggregationsStringTermsBucket[]) ?? [];
|
||||
return buckets.map((bucket) => ({
|
||||
migration_id: bucket.key,
|
||||
id: bucket.key,
|
||||
rules: {
|
||||
total: bucket.doc_count,
|
||||
pending: bucket.pending?.doc_count ?? 0,
|
||||
|
@ -272,6 +278,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
|
|||
completed: bucket.completed?.doc_count ?? 0,
|
||||
failed: bucket.failed?.doc_count ?? 0,
|
||||
},
|
||||
created_at: bucket.createdAt?.value_as_string,
|
||||
last_updated_at: bucket.lastUpdatedAt?.value_as_string,
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -9,10 +9,7 @@ import type { AuthenticatedUser, Logger } from '@kbn/core/server';
|
|||
import { AbortError, abortSignalToPromise } from '@kbn/kibana-utils-plugin/server';
|
||||
import type { RunnableConfig } from '@langchain/core/runnables';
|
||||
import { SiemMigrationStatus } from '../../../../../common/siem_migrations/constants';
|
||||
import type {
|
||||
RuleMigrationAllTaskStats,
|
||||
RuleMigrationTaskStats,
|
||||
} from '../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import type { RuleMigrationTaskStats } from '../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import type { RuleMigrationsDataClient } from '../data/rule_migrations_data_client';
|
||||
import type { RuleMigrationDataStats } from '../data/rule_migrations_data_rules_client';
|
||||
import { getRuleMigrationAgent } from './agent';
|
||||
|
@ -229,10 +226,10 @@ export class RuleMigrationsTaskClient {
|
|||
}
|
||||
|
||||
/** Returns the stats of all migrations */
|
||||
async getAllStats(): Promise<RuleMigrationAllTaskStats> {
|
||||
async getAllStats(): Promise<RuleMigrationTaskStats[]> {
|
||||
const allDataStats = await this.data.rules.getAllStats();
|
||||
return allDataStats.map((dataStats) => {
|
||||
const status = this.getTaskStatus(dataStats.migration_id, dataStats.rules);
|
||||
const status = this.getTaskStatus(dataStats.id, dataStats.rules);
|
||||
return { status, ...dataStats };
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue