mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[Security Solution][SIEM migrations] Onboarding UI improvements (#204320)](https://github.com/elastic/kibana/pull/204320) <!--- Backport version: 8.9.8 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Sergi Massaneda","email":"sergi.massaneda@elastic.co"},"sourceCommit":{"committedDate":"2024-12-17T14:42:02Z","message":"[Security Solution][SIEM migrations] Onboarding UI improvements (#204320)\n\n## Summary\r\n\r\nPart of: https://github.com/elastic/security-team/issues/10667\r\n\r\n#### Improvements\r\n\r\n- Implementation of the Onboarding card to create migrations using the\r\nflyout\r\n- Migration complete summary panel implemented\r\n- Migration ready panel improved to detect missing resources\r\n- Migration processing improved\r\n- Migration missing resources panel implemented\r\n- All migration panels and refactored to be reusable by translation\r\ntable using the\r\n- `RuleMigrationDataInputWrapper` implemented to reuse the Flyout from\r\nthe translation table\r\n- Request poll interval increased from 5 to 10 seconds due to event loop\r\nusage.\r\n\r\n\r\n> [!NOTE] \r\n> This feature needs `siemMigrationsEnabled` experimental flag enabled\r\nto work.\r\n\r\n## Screenshots\r\n\r\nLookups input\r\n\r\n\r\n\r\n\r\nTranslation \"complete\" panel\r\n\r\n\r\nTranslation \"created\" panel (w/ and w/o missing macros)\r\n\r\n\r\nTranslation processing (preparing)\r\n\r\n\r\nTranslation processing (translating)\r\n\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"303eee8fee32f5922a87ab7f9cce651d0b2c5735","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","Team:Threat Hunting","Team: SecuritySolution","backport:version","v8.18.0"],"number":204320,"url":"https://github.com/elastic/kibana/pull/204320","mergeCommit":{"message":"[Security Solution][SIEM migrations] Onboarding UI improvements (#204320)\n\n## Summary\r\n\r\nPart of: https://github.com/elastic/security-team/issues/10667\r\n\r\n#### Improvements\r\n\r\n- Implementation of the Onboarding card to create migrations using the\r\nflyout\r\n- Migration complete summary panel implemented\r\n- Migration ready panel improved to detect missing resources\r\n- Migration processing improved\r\n- Migration missing resources panel implemented\r\n- All migration panels and refactored to be reusable by translation\r\ntable using the\r\n- `RuleMigrationDataInputWrapper` implemented to reuse the Flyout from\r\nthe translation table\r\n- Request poll interval increased from 5 to 10 seconds due to event loop\r\nusage.\r\n\r\n\r\n> [!NOTE] \r\n> This feature needs `siemMigrationsEnabled` experimental flag enabled\r\nto work.\r\n\r\n## Screenshots\r\n\r\nLookups input\r\n\r\n\r\n\r\n\r\nTranslation \"complete\" panel\r\n\r\n\r\nTranslation \"created\" panel (w/ and w/o missing macros)\r\n\r\n\r\nTranslation processing (preparing)\r\n\r\n\r\nTranslation processing (translating)\r\n\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"303eee8fee32f5922a87ab7f9cce651d0b2c5735"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/204320","number":204320,"mergeCommit":{"message":"[Security Solution][SIEM migrations] Onboarding UI improvements (#204320)\n\n## Summary\r\n\r\nPart of: https://github.com/elastic/security-team/issues/10667\r\n\r\n#### Improvements\r\n\r\n- Implementation of the Onboarding card to create migrations using the\r\nflyout\r\n- Migration complete summary panel implemented\r\n- Migration ready panel improved to detect missing resources\r\n- Migration processing improved\r\n- Migration missing resources panel implemented\r\n- All migration panels and refactored to be reusable by translation\r\ntable using the\r\n- `RuleMigrationDataInputWrapper` implemented to reuse the Flyout from\r\nthe translation table\r\n- Request poll interval increased from 5 to 10 seconds due to event loop\r\nusage.\r\n\r\n\r\n> [!NOTE] \r\n> This feature needs `siemMigrationsEnabled` experimental flag enabled\r\nto work.\r\n\r\n## Screenshots\r\n\r\nLookups input\r\n\r\n\r\n\r\n\r\nTranslation \"complete\" panel\r\n\r\n\r\nTranslation \"created\" panel (w/ and w/o missing macros)\r\n\r\n\r\nTranslation processing (preparing)\r\n\r\n\r\nTranslation processing (translating)\r\n\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"303eee8fee32f5922a87ab7f9cce651d0b2c5735"}},{"branch":"8.x","label":"v8.18.0","labelRegex":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
45c4faad10
commit
4b931d025f
67 changed files with 1850 additions and 639 deletions
|
@ -44,7 +44,7 @@ export enum SiemMigrationStatus {
|
|||
FAILED = 'failed',
|
||||
}
|
||||
|
||||
export enum SiemMigrationRuleTranslationResult {
|
||||
export enum RuleTranslationResult {
|
||||
FULL = 'full',
|
||||
PARTIAL = 'partial',
|
||||
UNTRANSLATABLE = 'untranslatable',
|
||||
|
@ -60,3 +60,5 @@ export const DEFAULT_TRANSLATION_FIELDS = {
|
|||
to: 'now',
|
||||
interval: '5m',
|
||||
} as const;
|
||||
|
||||
export const EMPTY_RESOURCE_PLACEHOLDER = '<empty>';
|
||||
|
|
|
@ -285,21 +285,47 @@ export const RuleMigrationTranslationStats = z.object({
|
|||
*/
|
||||
rules: z.object({
|
||||
/**
|
||||
* The total number of rules to migrate.
|
||||
* The total number of rules in the migration.
|
||||
*/
|
||||
total: z.number().int(),
|
||||
/**
|
||||
* The number of rules that matched Elastic prebuilt rules.
|
||||
* The number of rules that have been successfully translated.
|
||||
*/
|
||||
prebuilt: z.number().int(),
|
||||
success: z.object({
|
||||
/**
|
||||
* The total number of rules that have been successfully translated.
|
||||
*/
|
||||
total: z.number().int(),
|
||||
/**
|
||||
* The translation results
|
||||
*/
|
||||
result: z.object({
|
||||
/**
|
||||
* The number of rules that have been fully translated.
|
||||
*/
|
||||
full: z.number().int(),
|
||||
/**
|
||||
* The number of rules that have been partially translated.
|
||||
*/
|
||||
partial: z.number().int(),
|
||||
/**
|
||||
* The number of rules that could not be translated.
|
||||
*/
|
||||
untranslatable: z.number().int(),
|
||||
}),
|
||||
/**
|
||||
* The number of rules that have been successfully translated and can be installed.
|
||||
*/
|
||||
installable: z.number().int(),
|
||||
/**
|
||||
* The number of rules that have been successfully translated and matched Elastic prebuilt rules.
|
||||
*/
|
||||
prebuilt: z.number().int(),
|
||||
}),
|
||||
/**
|
||||
* The number of rules that did not match Elastic prebuilt rules and will be installed as custom rules.
|
||||
* The number of rules that have failed translation.
|
||||
*/
|
||||
custom: z.number().int(),
|
||||
/**
|
||||
* The number of rules that can be installed.
|
||||
*/
|
||||
installable: z.number().int(),
|
||||
failed: z.number().int(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
@ -234,23 +234,50 @@ components:
|
|||
description: The rules migration translation stats.
|
||||
required:
|
||||
- total
|
||||
- prebuilt
|
||||
- custom
|
||||
- installable
|
||||
- success
|
||||
- failed
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
description: The total number of rules to migrate.
|
||||
prebuilt:
|
||||
description: The total number of rules in the migration.
|
||||
success:
|
||||
type: object
|
||||
description: The number of rules that have been successfully translated.
|
||||
required:
|
||||
- total
|
||||
- result
|
||||
- installable
|
||||
- prebuilt
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
description: The total number of rules that have been successfully translated.
|
||||
result:
|
||||
type: object
|
||||
description: The translation results
|
||||
required:
|
||||
- full
|
||||
- partial
|
||||
- untranslatable
|
||||
properties:
|
||||
full:
|
||||
type: integer
|
||||
description: The number of rules that have been fully translated.
|
||||
partial:
|
||||
type: integer
|
||||
description: The number of rules that have been partially translated.
|
||||
untranslatable:
|
||||
type: integer
|
||||
description: The number of rules that could not be translated.
|
||||
installable:
|
||||
type: integer
|
||||
description: The number of rules that have been successfully translated and can be installed.
|
||||
prebuilt:
|
||||
type: integer
|
||||
description: The number of rules that have been successfully translated and matched Elastic prebuilt rules.
|
||||
failed:
|
||||
type: integer
|
||||
description: The number of rules that matched Elastic prebuilt rules.
|
||||
custom:
|
||||
type: integer
|
||||
description: The number of rules that did not match Elastic prebuilt rules and will be installed as custom rules.
|
||||
installable:
|
||||
type: integer
|
||||
description: The number of rules that can be installed.
|
||||
|
||||
description: The number of rules that have failed translation.
|
||||
RuleMigrationTranslationResult:
|
||||
type: string
|
||||
description: The rule translation result.
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { PanelText, type PanelTextProps } from './panel_text';
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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, { type PropsWithChildren } from 'react';
|
||||
import { css, type CSSInterpolation } from '@emotion/css';
|
||||
import { EuiText, useEuiTheme, COLOR_MODES_STANDARD, type EuiTextProps } from '@elastic/eui';
|
||||
|
||||
export interface PanelTextProps extends PropsWithChildren<EuiTextProps> {
|
||||
subdued?: true;
|
||||
semiBold?: true;
|
||||
}
|
||||
export const PanelText = React.memo<PanelTextProps>(({ children, subdued, semiBold, ...props }) => {
|
||||
const { euiTheme, colorMode } = useEuiTheme();
|
||||
const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark;
|
||||
|
||||
let color;
|
||||
if (subdued && !isDarkMode) {
|
||||
color = 'subdued';
|
||||
}
|
||||
|
||||
const style: CSSInterpolation = {};
|
||||
if (semiBold) {
|
||||
style.fontWeight = euiTheme.font.weight.semiBold;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiText {...props} color={color} className={css(style)}>
|
||||
{children}
|
||||
</EuiText>
|
||||
);
|
||||
});
|
||||
PanelText.displayName = 'PanelText';
|
|
@ -28,7 +28,7 @@ const getCardHash = (cardId: OnboardingCardId | null) => (cardId ? `#${cardId}`
|
|||
* This hook manages the expanded card id state in the LocalStorage and the hash in the URL.
|
||||
*/
|
||||
export const useUrlDetail = () => {
|
||||
const { spaceId, telemetry } = useOnboardingContext();
|
||||
const { config, spaceId, telemetry } = useOnboardingContext();
|
||||
const topicId = useTopicId();
|
||||
const [storedUrlDetail, setStoredUrlDetail] = useStoredUrlDetails(spaceId);
|
||||
|
||||
|
@ -56,6 +56,14 @@ export const useUrlDetail = () => {
|
|||
|
||||
const syncUrlDetails = useCallback(
|
||||
(pathTopicId: OnboardingTopicId | null, hashCardId: OnboardingCardId | null) => {
|
||||
if (storedUrlDetail) {
|
||||
// If the stored topic is not valid, clear it
|
||||
const [storedTopicId] = storedUrlDetail.split('#');
|
||||
if (storedTopicId && !config.has(storedTopicId as OnboardingTopicId)) {
|
||||
setStoredUrlDetail(null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const urlDetail = `${pathTopicId || ''}${hashCardId ? `#${hashCardId}` : ''}`;
|
||||
if (urlDetail && urlDetail !== storedUrlDetail) {
|
||||
if (hashCardId) {
|
||||
|
@ -67,7 +75,7 @@ export const useUrlDetail = () => {
|
|||
navigateTo({ deepLinkId: SecurityPageName.landing, path: storedUrlDetail });
|
||||
}
|
||||
},
|
||||
[navigateTo, setStoredUrlDetail, storedUrlDetail, telemetry]
|
||||
[config, navigateTo, setStoredUrlDetail, storedUrlDetail, telemetry]
|
||||
);
|
||||
|
||||
return { setTopicDetail, setCardDetail, syncUrlDetails };
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import React, { useCallback } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { CenteredLoadingSpinner } from '../../../../../../common/components/centered_loading_spinner';
|
||||
import { useKibana } from '../../../../../../common/lib/kibana/kibana_react';
|
||||
import { useDefinedLocalStorage } from '../../../../hooks/use_stored_state';
|
||||
import type { OnboardingCardComponent } from '../../../../../types';
|
||||
|
@ -35,9 +36,15 @@ export const AIConnectorCard: OnboardingCardComponent<AIConnectorCardMetadata> =
|
|||
[setComplete, setStoredConnectorId]
|
||||
);
|
||||
|
||||
const connectors = checkCompleteMetadata?.connectors;
|
||||
const canExecuteConnectors = checkCompleteMetadata?.canExecuteConnectors;
|
||||
const canCreateConnectors = checkCompleteMetadata?.canCreateConnectors;
|
||||
if (!checkCompleteMetadata) {
|
||||
return (
|
||||
<OnboardingCardContentPanel>
|
||||
<CenteredLoadingSpinner />
|
||||
</OnboardingCardContentPanel>
|
||||
);
|
||||
}
|
||||
|
||||
const { connectors, canExecuteConnectors, canCreateConnectors } = checkCompleteMetadata;
|
||||
|
||||
return (
|
||||
<OnboardingCardContentPanel>
|
||||
|
|
|
@ -1,35 +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 React, { createContext, useContext, useMemo, type PropsWithChildren } from 'react';
|
||||
import type { RuleMigrationTaskStats } from '../../../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
|
||||
interface StartMigrationContextValue {
|
||||
openFlyout: (migrationStats?: RuleMigrationTaskStats) => void;
|
||||
closeFlyout: () => void;
|
||||
}
|
||||
|
||||
const StartMigrationContext = createContext<StartMigrationContextValue | null>(null);
|
||||
|
||||
export const StartMigrationContextProvider: React.FC<
|
||||
PropsWithChildren<StartMigrationContextValue>
|
||||
> = React.memo(({ children, openFlyout, closeFlyout }) => {
|
||||
const value = useMemo<StartMigrationContextValue>(
|
||||
() => ({ openFlyout, closeFlyout }),
|
||||
[openFlyout, closeFlyout]
|
||||
);
|
||||
return <StartMigrationContext.Provider value={value}>{children}</StartMigrationContext.Provider>;
|
||||
});
|
||||
StartMigrationContextProvider.displayName = 'StartMigrationContextProvider';
|
||||
|
||||
export const useStartMigrationContext = (): StartMigrationContextValue => {
|
||||
const context = useContext(StartMigrationContext);
|
||||
if (context == null) {
|
||||
throw new Error('useStartMigrationContext must be used within a StartMigrationContextProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
|
@ -1,45 +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 React, { useMemo } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiPanel, EuiProgress } from '@elastic/eui';
|
||||
import type { RuleMigrationStats } from '../../../../../../../siem_migrations/rules/types';
|
||||
import * as i18n from '../translations';
|
||||
import { TITLE_CLASS_NAME } from '../start_migration_card.styles';
|
||||
|
||||
export interface MigrationProgressPanelProps {
|
||||
migrationStats: RuleMigrationStats;
|
||||
}
|
||||
export const MigrationProgressPanel = React.memo<MigrationProgressPanelProps>(
|
||||
({ migrationStats }) => {
|
||||
const progressValue = useMemo(() => {
|
||||
const finished = migrationStats.rules.completed + migrationStats.rules.failed;
|
||||
return (finished / migrationStats.rules.total) * 100;
|
||||
}, [migrationStats.rules]);
|
||||
|
||||
return (
|
||||
<EuiPanel hasShadow={false} hasBorder paddingSize="m">
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s" className={TITLE_CLASS_NAME}>
|
||||
<p>{i18n.START_MIGRATION_CARD_MIGRATION_TITLE(migrationStats.number)}</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s">
|
||||
<p>{i18n.START_MIGRATION_CARD_PROGRESS_DESCRIPTION}</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiProgress value={progressValue} max={100} color="success" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
);
|
||||
MigrationProgressPanel.displayName = 'MigrationProgressPanel';
|
|
@ -1,68 +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 React, { useCallback } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiText,
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiPanel,
|
||||
} from '@elastic/eui';
|
||||
import { useStartMigration } from '../../../../../../../siem_migrations/rules/service/hooks/use_start_migration';
|
||||
import type { RuleMigrationStats } from '../../../../../../../siem_migrations/rules/types';
|
||||
import * as i18n from '../translations';
|
||||
import { useStartMigrationContext } from '../context';
|
||||
import { TITLE_CLASS_NAME } from '../start_migration_card.styles';
|
||||
|
||||
export interface MigrationReadyPanelProps {
|
||||
migrationStats: RuleMigrationStats;
|
||||
}
|
||||
export const MigrationReadyPanel = React.memo<MigrationReadyPanelProps>(({ migrationStats }) => {
|
||||
const { openFlyout } = useStartMigrationContext();
|
||||
const onOpenFlyout = useCallback<React.MouseEventHandler>(() => {
|
||||
openFlyout(migrationStats);
|
||||
}, [openFlyout, migrationStats]);
|
||||
|
||||
const { startMigration, isLoading } = useStartMigration();
|
||||
const onStartMigration = useCallback(() => {
|
||||
startMigration(migrationStats.id);
|
||||
}, [migrationStats.id, startMigration]);
|
||||
|
||||
return (
|
||||
<EuiPanel hasShadow={false} hasBorder paddingSize="m">
|
||||
<EuiFlexGroup direction="row" alignItems="center" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="column" alignItems="flexStart" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s" className={TITLE_CLASS_NAME}>
|
||||
<p>{i18n.START_MIGRATION_CARD_MIGRATION_TITLE(migrationStats.number)}</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s" color="subdued">
|
||||
<p>{i18n.START_MIGRATION_CARD_MIGRATION_READY_DESCRIPTION}</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={onStartMigration} isLoading={isLoading}>
|
||||
{i18n.START_MIGRATION_CARD_TRANSLATE_BUTTON}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton iconType="download" iconSide="right" onClick={onOpenFlyout}>
|
||||
{i18n.START_MIGRATION_CARD_UPLOAD_MACROS_BUTTON}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
});
|
||||
MigrationReadyPanel.displayName = 'MigrationReadyPanel';
|
|
@ -1,87 +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 React from 'react';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiText,
|
||||
EuiPanel,
|
||||
EuiHorizontalRule,
|
||||
EuiIcon,
|
||||
} from '@elastic/eui';
|
||||
import { SecurityPageName } from '@kbn/security-solution-navigation';
|
||||
import { AssistantAvatar } from '@kbn/elastic-assistant/impl/assistant/assistant_avatar/assistant_avatar';
|
||||
import { SecuritySolutionLinkButton } from '../../../../../../../common/components/links';
|
||||
import type { RuleMigrationStats } from '../../../../../../../siem_migrations/rules/types';
|
||||
import * as i18n from '../translations';
|
||||
import { TITLE_CLASS_NAME } from '../start_migration_card.styles';
|
||||
|
||||
export interface MigrationResultPanelProps {
|
||||
migrationStats: RuleMigrationStats;
|
||||
}
|
||||
export const MigrationResultPanel = React.memo<MigrationResultPanelProps>(({ migrationStats }) => {
|
||||
return (
|
||||
<EuiPanel hasShadow={false} hasBorder paddingSize="none">
|
||||
<EuiPanel hasShadow={false} hasBorder={false} paddingSize="m">
|
||||
<EuiFlexGroup direction="column" alignItems="flexStart" gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s" className={TITLE_CLASS_NAME}>
|
||||
<p>{i18n.START_MIGRATION_CARD_RESULT_TITLE(migrationStats.number)}</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s" color="subdued">
|
||||
<p>
|
||||
{i18n.START_MIGRATION_CARD_RESULT_DESCRIPTION(
|
||||
moment(migrationStats.created_at).format('MMMM Do YYYY, h:mm:ss a'),
|
||||
moment(migrationStats.last_updated_at).fromNow()
|
||||
)}
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
<EuiPanel hasShadow={false} hasBorder={false} paddingSize="m">
|
||||
<EuiFlexGroup direction="column" alignItems="stretch" gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup direction="row" alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type={AssistantAvatar} size="m" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s" className={TITLE_CLASS_NAME}>
|
||||
<p>{i18n.VIEW_TRANSLATED_RULES_TITLE}</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiPanel hasShadow={false} hasBorder paddingSize="m">
|
||||
<EuiFlexGroup direction="column" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<p>{'TODO: chart'}</p>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<SecuritySolutionLinkButton
|
||||
deepLinkId={SecurityPageName.siemMigrationsRules}
|
||||
path={migrationStats.id}
|
||||
>
|
||||
{i18n.VIEW_TRANSLATED_RULES_BUTTON}
|
||||
</SecuritySolutionLinkButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiPanel>
|
||||
);
|
||||
});
|
||||
MigrationResultPanel.displayName = 'MigrationResultPanel';
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { SiemMigrationTaskStatus } from '../../../../../../../common/siem_migrations/constants';
|
||||
import type { RuleMigrationStats } from '../../../../../../siem_migrations/rules/types';
|
||||
import { UploadRulesPanel } from './upload_rules_panel';
|
||||
import { MigrationProgressPanel } from '../../../../../../siem_migrations/rules/components/migration_status_panels/migration_progress_panel';
|
||||
import { MigrationResultPanel } from '../../../../../../siem_migrations/rules/components/migration_status_panels/migration_result_panel';
|
||||
import { MigrationReadyPanel } from '../../../../../../siem_migrations/rules/components/migration_status_panels/migration_ready_panel';
|
||||
import { MissingAIConnectorCallout } from './missing_ai_connector_callout';
|
||||
|
||||
export interface RuleMigrationsPanelsProps {
|
||||
migrationsStats: RuleMigrationStats[];
|
||||
isConnectorsCardComplete: boolean;
|
||||
expandConnectorsCard: () => void;
|
||||
}
|
||||
export const RuleMigrationsPanels = React.memo<RuleMigrationsPanelsProps>(
|
||||
({ migrationsStats, isConnectorsCardComplete, expandConnectorsCard }) => {
|
||||
if (migrationsStats.length === 0) {
|
||||
return isConnectorsCardComplete ? (
|
||||
<UploadRulesPanel />
|
||||
) : (
|
||||
<MissingAIConnectorCallout onExpandAiConnectorsCard={expandConnectorsCard} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
{isConnectorsCardComplete ? (
|
||||
<UploadRulesPanel isUploadMore />
|
||||
) : (
|
||||
<MissingAIConnectorCallout onExpandAiConnectorsCard={expandConnectorsCard} />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
{migrationsStats.map((migrationStats) => (
|
||||
<EuiFlexItem grow={false} key={migrationStats.id}>
|
||||
{migrationStats.status === SiemMigrationTaskStatus.READY && (
|
||||
<MigrationReadyPanel migrationStats={migrationStats} />
|
||||
)}
|
||||
{migrationStats.status === SiemMigrationTaskStatus.RUNNING && (
|
||||
<MigrationProgressPanel migrationStats={migrationStats} />
|
||||
)}
|
||||
{migrationStats.status === SiemMigrationTaskStatus.FINISHED && (
|
||||
<MigrationResultPanel migrationStats={migrationStats} />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
RuleMigrationsPanels.displayName = 'RuleMigrationsPanels';
|
|
@ -5,32 +5,25 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { PanelText } from '../../../../../../common/components/panel_text';
|
||||
import { RuleMigrationDataInputWrapper } from '../../../../../../siem_migrations/rules/components/data_input_flyout/data_input_wrapper';
|
||||
import { SiemMigrationTaskStatus } from '../../../../../../../common/siem_migrations/constants';
|
||||
import { OnboardingCardId } from '../../../../../constants';
|
||||
import type { RuleMigrationTaskStats } from '../../../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import { useLatestStats } from '../../../../../../siem_migrations/rules/service/hooks/use_latest_stats';
|
||||
import { MigrationDataInputFlyout } from '../../../../../../siem_migrations/rules/components/data_input_flyout';
|
||||
import { CenteredLoadingSpinner } from '../../../../../../common/components/centered_loading_spinner';
|
||||
import type { OnboardingCardComponent } from '../../../../../types';
|
||||
import { OnboardingCardContentPanel } from '../../common/card_content_panel';
|
||||
import { UploadRulesPanels } from './upload_rules_panels';
|
||||
import { StartMigrationContextProvider } from './context';
|
||||
import { RuleMigrationsPanels } from './rule_migrations_panels';
|
||||
import { useStyles } from './start_migration_card.styles';
|
||||
import * as i18n from './translations';
|
||||
import { MissingAIConnectorCallout } from './missing_ai_connector_callout';
|
||||
|
||||
export const StartMigrationCard: OnboardingCardComponent = React.memo(
|
||||
({ setComplete, isCardComplete, setExpandedCardId }) => {
|
||||
const styles = useStyles();
|
||||
const { data: migrationsStats, isLoading, refreshStats } = useLatestStats();
|
||||
|
||||
const [isFlyoutOpen, setIsFlyoutOpen] = useState<boolean>();
|
||||
const [flyoutMigrationStats, setFlyoutMigrationStats] = useState<
|
||||
RuleMigrationTaskStats | undefined
|
||||
>();
|
||||
|
||||
useEffect(() => {
|
||||
// Set card complete if any migration is finished
|
||||
if (!isCardComplete(OnboardingCardId.siemMigrationsStart) && migrationsStats) {
|
||||
|
@ -40,44 +33,33 @@ export const StartMigrationCard: OnboardingCardComponent = React.memo(
|
|||
}
|
||||
}, [isCardComplete, migrationsStats, setComplete]);
|
||||
|
||||
const closeFlyout = useCallback(() => {
|
||||
setIsFlyoutOpen(false);
|
||||
setFlyoutMigrationStats(undefined);
|
||||
refreshStats();
|
||||
}, [refreshStats]);
|
||||
const isConnectorsCardComplete = useMemo(
|
||||
() => isCardComplete(OnboardingCardId.siemMigrationsAiConnectors),
|
||||
[isCardComplete]
|
||||
);
|
||||
|
||||
const openFlyout = useCallback((migrationStats?: RuleMigrationTaskStats) => {
|
||||
setFlyoutMigrationStats(migrationStats);
|
||||
setIsFlyoutOpen(true);
|
||||
}, []);
|
||||
|
||||
if (!isCardComplete(OnboardingCardId.siemMigrationsAiConnectors)) {
|
||||
return (
|
||||
<MissingAIConnectorCallout
|
||||
onExpandAiConnectorsCard={() =>
|
||||
setExpandedCardId(OnboardingCardId.siemMigrationsAiConnectors)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const expandConnectorsCard = useCallback(() => {
|
||||
setExpandedCardId(OnboardingCardId.siemMigrationsAiConnectors);
|
||||
}, [setExpandedCardId]);
|
||||
|
||||
return (
|
||||
<StartMigrationContextProvider openFlyout={openFlyout} closeFlyout={closeFlyout}>
|
||||
<RuleMigrationDataInputWrapper onFlyoutClosed={refreshStats}>
|
||||
<OnboardingCardContentPanel paddingSize="none" className={styles}>
|
||||
{isLoading ? (
|
||||
<CenteredLoadingSpinner />
|
||||
) : (
|
||||
<UploadRulesPanels migrationsStats={migrationsStats} />
|
||||
<RuleMigrationsPanels
|
||||
migrationsStats={migrationsStats}
|
||||
isConnectorsCardComplete={isConnectorsCardComplete}
|
||||
expandConnectorsCard={expandConnectorsCard}
|
||||
/>
|
||||
)}
|
||||
<EuiSpacer size="m" />
|
||||
<EuiText size="xs" color="subdued">
|
||||
<PanelText size="xs" subdued>
|
||||
<p>{i18n.START_MIGRATION_CARD_FOOTER_NOTE}</p>
|
||||
</EuiText>
|
||||
</PanelText>
|
||||
</OnboardingCardContentPanel>
|
||||
{isFlyoutOpen && (
|
||||
<MigrationDataInputFlyout onClose={closeFlyout} migrationStats={flyoutMigrationStats} />
|
||||
)}
|
||||
</StartMigrationContextProvider>
|
||||
</RuleMigrationDataInputWrapper>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -55,64 +55,3 @@ export const START_MIGRATION_CARD_UPLOAD_MORE_BUTTON = i18n.translate(
|
|||
'xpack.securitySolution.onboarding.startMigration.uploadMore.button',
|
||||
{ defaultMessage: 'Upload more rules' }
|
||||
);
|
||||
|
||||
export const START_MIGRATION_CARD_UPLOAD_READ_MORE = i18n.translate(
|
||||
'xpack.securitySolution.onboarding.startMigration.upload.readMore',
|
||||
{ defaultMessage: 'Read more about our AI powered translations and other features.' }
|
||||
);
|
||||
|
||||
export const START_MIGRATION_CARD_UPLOAD_READ_DOCS = i18n.translate(
|
||||
'xpack.securitySolution.onboarding.startMigration.upload.readAiDocsLink',
|
||||
{ defaultMessage: 'Read AI docs' }
|
||||
);
|
||||
|
||||
export const START_MIGRATION_CARD_MIGRATION_READY_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.onboarding.startMigration.ready.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Migration is created and ready but the translation has not started yet. You can either upload macros & lookups or start the translation process',
|
||||
}
|
||||
);
|
||||
export const START_MIGRATION_CARD_TRANSLATE_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.onboarding.startMigration.translate.button',
|
||||
{ defaultMessage: 'Start translation' }
|
||||
);
|
||||
export const START_MIGRATION_CARD_UPLOAD_MACROS_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.onboarding.startMigration.uploadMacros.button',
|
||||
{ defaultMessage: 'Upload macros' }
|
||||
);
|
||||
|
||||
export const START_MIGRATION_CARD_MIGRATION_TITLE = (number: number) =>
|
||||
i18n.translate('xpack.securitySolution.onboarding.startMigration.migrationTitle', {
|
||||
defaultMessage: 'SIEM rules migration #{number}',
|
||||
values: { number },
|
||||
});
|
||||
|
||||
export const START_MIGRATION_CARD_PROGRESS_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.onboarding.startMigration.progress.description',
|
||||
{
|
||||
defaultMessage: `This may take a few minutes & the task will work in the background. Just stay logged in and we'll notify you when done.`,
|
||||
}
|
||||
);
|
||||
|
||||
export const START_MIGRATION_CARD_RESULT_TITLE = (number: number) =>
|
||||
i18n.translate('xpack.securitySolution.onboarding.startMigration.result.title', {
|
||||
defaultMessage: 'SIEM rules migration #{number} complete',
|
||||
values: { number },
|
||||
});
|
||||
|
||||
export const START_MIGRATION_CARD_RESULT_DESCRIPTION = (createdAt: string, finishedAt: string) =>
|
||||
i18n.translate('xpack.securitySolution.onboarding.startMigration.result.description', {
|
||||
defaultMessage: 'Export uploaded on {createdAt} and translation finished {finishedAt}.',
|
||||
values: { createdAt, finishedAt },
|
||||
});
|
||||
|
||||
export const VIEW_TRANSLATED_RULES_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.onboarding.startMigration.result.translatedRules.title',
|
||||
{ defaultMessage: 'Translation Summary' }
|
||||
);
|
||||
|
||||
export const VIEW_TRANSLATED_RULES_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.onboarding.startMigration.result.translatedRules.button',
|
||||
{ defaultMessage: 'View translated rules' }
|
||||
);
|
||||
|
|
|
@ -15,10 +15,11 @@ import {
|
|||
EuiButtonEmpty,
|
||||
EuiPanel,
|
||||
} from '@elastic/eui';
|
||||
import { SiemMigrationsIcon } from '../../../../../../../siem_migrations/common/icon';
|
||||
import * as i18n from '../translations';
|
||||
import { useStartMigrationContext } from '../context';
|
||||
import { TITLE_CLASS_NAME } from '../start_migration_card.styles';
|
||||
import { RuleMigrationsReadMore } from '../../../../../../siem_migrations/rules/components/migration_status_panels/read_more';
|
||||
import { SiemMigrationsIcon } from '../../../../../../siem_migrations/common/icon';
|
||||
import * as i18n from './translations';
|
||||
import { TITLE_CLASS_NAME } from './start_migration_card.styles';
|
||||
import { useRuleMigrationDataInputContext } from '../../../../../../siem_migrations/rules/components/data_input_flyout/context';
|
||||
import { useStyles } from './upload_rules_panel.styles';
|
||||
|
||||
export interface UploadRulesPanelProps {
|
||||
|
@ -26,7 +27,7 @@ export interface UploadRulesPanelProps {
|
|||
}
|
||||
export const UploadRulesPanel = React.memo<UploadRulesPanelProps>(({ isUploadMore = false }) => {
|
||||
const styles = useStyles(isUploadMore);
|
||||
const { openFlyout } = useStartMigrationContext();
|
||||
const { openFlyout } = useRuleMigrationDataInputContext();
|
||||
const onOpenFlyout = useCallback<React.MouseEventHandler>(() => {
|
||||
openFlyout();
|
||||
}, [openFlyout]);
|
||||
|
@ -55,9 +56,7 @@ export const UploadRulesPanel = React.memo<UploadRulesPanelProps>(({ isUploadMor
|
|||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs">
|
||||
<p>{i18n.START_MIGRATION_CARD_UPLOAD_READ_MORE}</p>
|
||||
</EuiText>
|
||||
<RuleMigrationsReadMore />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
|
@ -1,46 +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 React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { SiemMigrationTaskStatus } from '../../../../../../../common/siem_migrations/constants';
|
||||
import type { RuleMigrationStats } from '../../../../../../siem_migrations/rules/types';
|
||||
import { UploadRulesPanel } from './panels/upload_rules_panel';
|
||||
import { MigrationProgressPanel } from './panels/migration_progress_panel';
|
||||
import { MigrationResultPanel } from './panels/migration_result_panel';
|
||||
import { MigrationReadyPanel } from './panels/migration_ready_panel';
|
||||
|
||||
export interface UploadRulesPanelsProps {
|
||||
migrationsStats: RuleMigrationStats[];
|
||||
}
|
||||
export const UploadRulesPanels = React.memo<UploadRulesPanelsProps>(({ migrationsStats }) => {
|
||||
if (migrationsStats.length === 0) {
|
||||
return <UploadRulesPanel />;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<UploadRulesPanel isUploadMore />
|
||||
</EuiFlexItem>
|
||||
{migrationsStats.map((migrationStats) => (
|
||||
<EuiFlexItem grow={false} key={migrationStats.id}>
|
||||
{migrationStats.status === SiemMigrationTaskStatus.READY && (
|
||||
<MigrationReadyPanel migrationStats={migrationStats} />
|
||||
)}
|
||||
{migrationStats.status === SiemMigrationTaskStatus.RUNNING && (
|
||||
<MigrationProgressPanel migrationStats={migrationStats} />
|
||||
)}
|
||||
{migrationStats.status === SiemMigrationTaskStatus.FINISHED && (
|
||||
<MigrationResultPanel migrationStats={migrationStats} />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
});
|
||||
UploadRulesPanels.displayName = 'UploadRulesPanels';
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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, { createContext, useContext, useMemo, type PropsWithChildren } from 'react';
|
||||
import type { RuleMigrationTaskStats } from '../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
|
||||
interface RuleMigrationDataInputContextValue {
|
||||
openFlyout: (migrationStats?: RuleMigrationTaskStats) => void;
|
||||
closeFlyout: () => void;
|
||||
}
|
||||
|
||||
const RuleMigrationDataInputContext = createContext<RuleMigrationDataInputContextValue | null>(
|
||||
null
|
||||
);
|
||||
|
||||
export const RuleMigrationDataInputContextProvider: React.FC<
|
||||
PropsWithChildren<RuleMigrationDataInputContextValue>
|
||||
> = React.memo(({ children, openFlyout, closeFlyout }) => {
|
||||
const value = useMemo<RuleMigrationDataInputContextValue>(
|
||||
() => ({ openFlyout, closeFlyout }),
|
||||
[openFlyout, closeFlyout]
|
||||
);
|
||||
return (
|
||||
<RuleMigrationDataInputContext.Provider value={value}>
|
||||
{children}
|
||||
</RuleMigrationDataInputContext.Provider>
|
||||
);
|
||||
});
|
||||
RuleMigrationDataInputContextProvider.displayName = 'RuleMigrationDataInputContextProvider';
|
||||
|
||||
export const useRuleMigrationDataInputContext = (): RuleMigrationDataInputContextValue => {
|
||||
const context = useContext(RuleMigrationDataInputContext);
|
||||
if (context == null) {
|
||||
throw new Error(
|
||||
'useRuleMigrationDataInputContext must be used within a RuleMigrationDataInputContextProvider'
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
|
@ -15,6 +15,7 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type {
|
||||
|
@ -23,8 +24,9 @@ import type {
|
|||
} from '../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import { RulesDataInput } from './steps/rules/rules_data_input';
|
||||
import { useStartMigration } from '../../service/hooks/use_start_migration';
|
||||
import { DataInputStep } from './types';
|
||||
import { DataInputStep } from './steps/constants';
|
||||
import { MacrosDataInput } from './steps/macros/macros_data_input';
|
||||
import { LookupsDataInput } from './steps/lookups/lookups_data_input';
|
||||
|
||||
interface MissingResourcesIndexed {
|
||||
macros: string[];
|
||||
|
@ -84,8 +86,8 @@ export const MigrationDataInputFlyout = React.memo<MigrationDataInputFlyoutProps
|
|||
[]
|
||||
);
|
||||
|
||||
const onMacrosCreated = useCallback(() => {
|
||||
setDataInputStep(DataInputStep.Lookups);
|
||||
const onAllLookupsCreated = useCallback(() => {
|
||||
setDataInputStep(DataInputStep.End);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
@ -121,21 +123,28 @@ export const MigrationDataInputFlyout = React.memo<MigrationDataInputFlyoutProps
|
|||
dataInputStep={dataInputStep}
|
||||
missingMacros={missingResourcesIndexed?.macros}
|
||||
migrationStats={migrationStats}
|
||||
onMacrosCreated={onMacrosCreated}
|
||||
onMissingResourcesFetched={onMissingResourcesFetched}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<LookupsDataInput
|
||||
dataInputStep={dataInputStep}
|
||||
missingLookups={missingResourcesIndexed?.lookups}
|
||||
migrationStats={migrationStats}
|
||||
onAllLookupsCreated={onAllLookupsCreated}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton fill onClick={onClose}>
|
||||
<EuiButtonEmpty onClick={onClose}>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.siemMigrations.rules.dataInputFlyout.closeButton"
|
||||
defaultMessage="Close"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 { PropsWithChildren } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import type { RuleMigrationTaskStats } from '../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import { RuleMigrationDataInputContextProvider } from './context';
|
||||
import { MigrationDataInputFlyout } from './data_input_flyout';
|
||||
|
||||
interface RuleMigrationDataInputWrapperProps {
|
||||
onFlyoutClosed: () => void;
|
||||
}
|
||||
export const RuleMigrationDataInputWrapper = React.memo<
|
||||
PropsWithChildren<RuleMigrationDataInputWrapperProps>
|
||||
>(({ children, onFlyoutClosed }) => {
|
||||
const [isFlyoutOpen, setIsFlyoutOpen] = useState<boolean>();
|
||||
const [flyoutMigrationStats, setFlyoutMigrationStats] = useState<
|
||||
RuleMigrationTaskStats | undefined
|
||||
>();
|
||||
|
||||
const closeFlyout = useCallback(() => {
|
||||
setIsFlyoutOpen(false);
|
||||
setFlyoutMigrationStats(undefined);
|
||||
onFlyoutClosed?.();
|
||||
}, [onFlyoutClosed]);
|
||||
|
||||
const openFlyout = useCallback((migrationStats?: RuleMigrationTaskStats) => {
|
||||
setFlyoutMigrationStats(migrationStats);
|
||||
setIsFlyoutOpen(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<RuleMigrationDataInputContextProvider openFlyout={openFlyout} closeFlyout={closeFlyout}>
|
||||
{children}
|
||||
{isFlyoutOpen && (
|
||||
<MigrationDataInputFlyout onClose={closeFlyout} migrationStats={flyoutMigrationStats} />
|
||||
)}
|
||||
</RuleMigrationDataInputContextProvider>
|
||||
);
|
||||
});
|
||||
RuleMigrationDataInputWrapper.displayName = 'RuleMigrationDataInputWrapper';
|
|
@ -5,9 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiPanel } from '@elastic/eui';
|
||||
import { EuiPanel, EuiSteps, type EuiStepProps } from '@elastic/eui';
|
||||
import { css } from '@emotion/css';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
const style = css`
|
||||
|
@ -16,11 +15,11 @@ const style = css`
|
|||
}
|
||||
`;
|
||||
|
||||
export const SubStepsWrapper = React.memo<PropsWithChildren<{}>>(({ children }) => {
|
||||
export const SubSteps = React.memo<{ steps: EuiStepProps[] }>(({ steps }) => {
|
||||
return (
|
||||
<EuiPanel hasShadow={false} paddingSize="xs" className={style}>
|
||||
{children}
|
||||
<EuiSteps titleSize="xxs" steps={steps} />
|
||||
</EuiPanel>
|
||||
);
|
||||
});
|
||||
SubStepsWrapper.displayName = 'SubStepsWrapper';
|
||||
SubSteps.displayName = 'SubSteps';
|
|
@ -26,7 +26,7 @@ export const useParseFileInput = (onFileParsed: OnFileParsed) => {
|
|||
|
||||
setError(undefined);
|
||||
|
||||
const rulesFile = files[0];
|
||||
const file = files[0];
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onloadstart = () => setIsParsing(true);
|
||||
|
@ -68,7 +68,7 @@ export const useParseFileInput = (onFileParsed: OnFileParsed) => {
|
|||
reader.onerror = handleReaderError;
|
||||
reader.onabort = handleReaderError;
|
||||
|
||||
reader.readAsText(rulesFile);
|
||||
reader.readAsText(file);
|
||||
},
|
||||
[onFileParsed]
|
||||
);
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
||||
export enum DataInputStep {
|
||||
Rules = 1,
|
||||
Macros = 2,
|
||||
Lookups = 3,
|
||||
End = 10,
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* 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 { EuiStepProps } from '@elastic/eui';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPanel,
|
||||
EuiStepNumber,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { EMPTY_RESOURCE_PLACEHOLDER } from '../../../../../../../common/siem_migrations/constants';
|
||||
import type {
|
||||
RuleMigrationResourceData,
|
||||
RuleMigrationTaskStats,
|
||||
} from '../../../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import type { OnResourcesCreated } from '../../types';
|
||||
import { getStatus } from '../common/get_status';
|
||||
import * as i18n from './translations';
|
||||
import { DataInputStep } from '../constants';
|
||||
import { SubSteps } from '../common/sub_step';
|
||||
import { useMissingLookupsListStep } from './sub_steps/missing_lookups_list';
|
||||
import { useLookupsFileUploadStep } from './sub_steps/lookups_file_upload';
|
||||
|
||||
export type UploadedLookups = Record<string, string>;
|
||||
export type AddUploadedLookups = (lookups: RuleMigrationResourceData[]) => void;
|
||||
|
||||
interface LookupsDataInputSubStepsProps {
|
||||
migrationStats: RuleMigrationTaskStats;
|
||||
missingLookups: string[];
|
||||
onAllLookupsCreated: OnResourcesCreated;
|
||||
}
|
||||
interface LookupsDataInputProps
|
||||
extends Omit<LookupsDataInputSubStepsProps, 'migrationStats' | 'missingLookups'> {
|
||||
dataInputStep: DataInputStep;
|
||||
migrationStats?: RuleMigrationTaskStats;
|
||||
missingLookups?: string[];
|
||||
}
|
||||
export const LookupsDataInput = React.memo<LookupsDataInputProps>(
|
||||
({ dataInputStep, migrationStats, missingLookups, onAllLookupsCreated }) => {
|
||||
const dataInputStatus = useMemo(
|
||||
() => getStatus(DataInputStep.Lookups, dataInputStep),
|
||||
[dataInputStep]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPanel hasShadow={false} hasBorder>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="row" justifyContent="center" gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiStepNumber
|
||||
titleSize="xs"
|
||||
number={DataInputStep.Lookups}
|
||||
status={dataInputStatus}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<b>{i18n.LOOKUPS_DATA_INPUT_TITLE}</b>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
{dataInputStatus === 'current' && migrationStats && missingLookups && (
|
||||
<>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s" color="subdued">
|
||||
{i18n.LOOKUPS_DATA_INPUT_DESCRIPTION}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<LookupsDataInputSubSteps
|
||||
migrationStats={migrationStats}
|
||||
missingLookups={missingLookups}
|
||||
onAllLookupsCreated={onAllLookupsCreated}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
);
|
||||
LookupsDataInput.displayName = 'LookupsDataInput';
|
||||
|
||||
const END = 10 as const;
|
||||
type SubStep = 1 | 2 | typeof END;
|
||||
export const LookupsDataInputSubSteps = React.memo<LookupsDataInputSubStepsProps>(
|
||||
({ migrationStats, missingLookups, onAllLookupsCreated }) => {
|
||||
const [subStep, setSubStep] = useState<SubStep>(1);
|
||||
const [uploadedLookups, setUploadedLookups] = useState<UploadedLookups>({});
|
||||
|
||||
const addUploadedLookups = useCallback<AddUploadedLookups>((lookups) => {
|
||||
setUploadedLookups((prevUploadedLookups) => ({
|
||||
...prevUploadedLookups,
|
||||
...Object.fromEntries(
|
||||
lookups.map((lookup) => [lookup.name, lookup.content ?? EMPTY_RESOURCE_PLACEHOLDER])
|
||||
),
|
||||
}));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (missingLookups.every((lookupName) => uploadedLookups[lookupName])) {
|
||||
setSubStep(END);
|
||||
onAllLookupsCreated();
|
||||
}
|
||||
}, [uploadedLookups, missingLookups, onAllLookupsCreated]);
|
||||
|
||||
// Copy query step
|
||||
const onCopied = useCallback(() => {
|
||||
setSubStep(2);
|
||||
}, []);
|
||||
const copyStep = useMissingLookupsListStep({
|
||||
status: getStatus(1, subStep),
|
||||
migrationStats,
|
||||
missingLookups,
|
||||
uploadedLookups,
|
||||
addUploadedLookups,
|
||||
onCopied,
|
||||
});
|
||||
|
||||
// Upload macros step
|
||||
const uploadStep = useLookupsFileUploadStep({
|
||||
status: getStatus(2, subStep),
|
||||
migrationStats,
|
||||
missingLookups,
|
||||
addUploadedLookups,
|
||||
});
|
||||
|
||||
const steps = useMemo<EuiStepProps[]>(() => [copyStep, uploadStep], [copyStep, uploadStep]);
|
||||
|
||||
return <SubSteps steps={steps} />;
|
||||
}
|
||||
);
|
||||
LookupsDataInputSubSteps.displayName = 'LookupsDataInputActive';
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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, { useCallback, useMemo } from 'react';
|
||||
import type { EuiStepProps, EuiStepStatus } from '@elastic/eui';
|
||||
import { useUpsertResources } from '../../../../../../service/hooks/use_upsert_resources';
|
||||
import type {
|
||||
RuleMigrationResourceData,
|
||||
RuleMigrationTaskStats,
|
||||
} from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import type { AddUploadedLookups } from '../../lookups_data_input';
|
||||
import * as i18n from './translations';
|
||||
import { LookupsFileUpload } from './lookups_file_upload';
|
||||
|
||||
export interface RulesFileUploadStepProps {
|
||||
status: EuiStepStatus;
|
||||
migrationStats: RuleMigrationTaskStats;
|
||||
missingLookups: string[];
|
||||
addUploadedLookups: AddUploadedLookups;
|
||||
}
|
||||
export const useLookupsFileUploadStep = ({
|
||||
status,
|
||||
migrationStats,
|
||||
addUploadedLookups,
|
||||
}: RulesFileUploadStepProps): EuiStepProps => {
|
||||
const { upsertResources, isLoading, error } = useUpsertResources(addUploadedLookups);
|
||||
|
||||
const upsertMigrationResources = useCallback(
|
||||
(lookupsFromFile: RuleMigrationResourceData[]) => {
|
||||
if (lookupsFromFile.length === 0) {
|
||||
return; // No lookups provided
|
||||
}
|
||||
upsertResources(migrationStats.id, lookupsFromFile);
|
||||
},
|
||||
[upsertResources, migrationStats]
|
||||
);
|
||||
|
||||
const uploadStepStatus = useMemo(() => {
|
||||
if (isLoading) {
|
||||
return 'loading';
|
||||
}
|
||||
if (error) {
|
||||
return 'danger';
|
||||
}
|
||||
return status;
|
||||
}, [isLoading, error, status]);
|
||||
|
||||
return {
|
||||
title: i18n.LOOKUPS_DATA_INPUT_FILE_UPLOAD_TITLE,
|
||||
status: uploadStepStatus,
|
||||
children: (
|
||||
<LookupsFileUpload
|
||||
createResources={upsertMigrationResources}
|
||||
isLoading={isLoading}
|
||||
apiError={error?.message}
|
||||
/>
|
||||
),
|
||||
};
|
||||
};
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* 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, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFilePicker,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import type {
|
||||
EuiFilePickerClass,
|
||||
EuiFilePickerProps,
|
||||
} from '@elastic/eui/src/components/form/file_picker/file_picker';
|
||||
import type { RuleMigrationResourceData } from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import { FILE_UPLOAD_ERROR } from '../../../../translations';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export interface LookupsFileUploadProps {
|
||||
createResources: (resources: RuleMigrationResourceData[]) => void;
|
||||
apiError?: string;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
export const LookupsFileUpload = React.memo<LookupsFileUploadProps>(
|
||||
({ createResources, apiError, isLoading }) => {
|
||||
const [lookupResources, setLookupResources] = useState<RuleMigrationResourceData[]>([]);
|
||||
const filePickerRef = useRef<EuiFilePickerClass>(null);
|
||||
|
||||
const createLookups = useCallback(() => {
|
||||
filePickerRef.current?.removeFiles();
|
||||
createResources(lookupResources);
|
||||
}, [createResources, lookupResources]);
|
||||
|
||||
const [isParsing, setIsParsing] = useState<boolean>(false);
|
||||
const [fileErrors, setErrors] = useState<string[]>([]);
|
||||
const addError = useCallback((error: string) => {
|
||||
setErrors((current) => [...current, error]);
|
||||
}, []);
|
||||
|
||||
const parseFile = useCallback(
|
||||
async (files: FileList | null) => {
|
||||
setErrors([]);
|
||||
setLookupResources([]);
|
||||
|
||||
if (!files?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lookups = await Promise.all(
|
||||
Array.from(files).map((file) => {
|
||||
return new Promise<RuleMigrationResourceData>((resolve) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onloadstart = () => setIsParsing(true);
|
||||
reader.onloadend = () => setIsParsing(false);
|
||||
|
||||
reader.onload = function (e) {
|
||||
// We can safely cast to string since we call `readAsText` to load the file.
|
||||
const content = e.target?.result as string | undefined;
|
||||
|
||||
if (content == null) {
|
||||
addError(FILE_UPLOAD_ERROR.CAN_NOT_READ);
|
||||
return;
|
||||
}
|
||||
|
||||
if (content === '' && e.loaded > 100000) {
|
||||
// V8-based browsers can't handle large files and return an empty string
|
||||
// instead of an error; see https://stackoverflow.com/a/61316641
|
||||
addError(FILE_UPLOAD_ERROR.TOO_LARGE_TO_PARSE);
|
||||
return;
|
||||
}
|
||||
|
||||
const name = file.name.replace(/\.[^/.]+$/, '').trim();
|
||||
resolve({ type: 'list', name, content });
|
||||
};
|
||||
|
||||
const handleReaderError = function () {
|
||||
const message = reader.error?.message;
|
||||
if (message) {
|
||||
addError(FILE_UPLOAD_ERROR.CAN_NOT_READ_WITH_REASON(message));
|
||||
} else {
|
||||
addError(FILE_UPLOAD_ERROR.CAN_NOT_READ);
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = handleReaderError;
|
||||
reader.onabort = handleReaderError;
|
||||
|
||||
reader.readAsText(file);
|
||||
});
|
||||
})
|
||||
).catch((e) => {
|
||||
addError(e.message);
|
||||
return [];
|
||||
});
|
||||
// Set the loaded lookups to the state
|
||||
setLookupResources((current) => [...current, ...lookups]);
|
||||
},
|
||||
[addError]
|
||||
);
|
||||
|
||||
const errors = useMemo(() => {
|
||||
if (apiError) {
|
||||
return [apiError];
|
||||
}
|
||||
return fileErrors;
|
||||
}, [apiError, fileErrors]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
helpText={errors.map((error) => (
|
||||
<EuiText color="danger" size="xs">
|
||||
{error}
|
||||
</EuiText>
|
||||
))}
|
||||
isInvalid={errors.length > 0}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFilePicker
|
||||
id="lookupsFilePicker"
|
||||
ref={filePickerRef as React.Ref<Omit<EuiFilePickerProps, 'stylesMemoizer'>>}
|
||||
fullWidth
|
||||
initialPromptText={
|
||||
<>
|
||||
<EuiText size="s" textAlign="center">
|
||||
{i18n.LOOKUPS_DATA_INPUT_FILE_UPLOAD_PROMPT}
|
||||
</EuiText>
|
||||
</>
|
||||
}
|
||||
accept="application/text"
|
||||
onChange={parseFile}
|
||||
multiple
|
||||
display="large"
|
||||
aria-label="Upload lookups files"
|
||||
isLoading={isParsing || isLoading}
|
||||
disabled={isParsing || isLoading}
|
||||
data-test-subj="lookupsFilePicker"
|
||||
data-loading={isParsing}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="flexEnd" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton onClick={createLookups} isLoading={isLoading} color="success">
|
||||
{i18n.LOOKUPS_DATA_INPUT_FILE_UPLOAD_BUTTON}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
LookupsFileUpload.displayName = 'LookupsFileUpload';
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const LOOKUPS_DATA_INPUT_FILE_UPLOAD_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.lookupsFileUpload.title',
|
||||
{ defaultMessage: 'Update your lookups export' }
|
||||
);
|
||||
export const LOOKUPS_DATA_INPUT_FILE_UPLOAD_PROMPT = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.lookupsFileUpload.prompt',
|
||||
{ defaultMessage: 'Select or drag and drop the exported lookup files' }
|
||||
);
|
||||
export const LOOKUPS_DATA_INPUT_FILE_UPLOAD_NOT_UPLOADED_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.lookupsFileUpload.notUploadedTitle',
|
||||
{ defaultMessage: 'Lookups not uploaded' }
|
||||
);
|
||||
export const LOOKUPS_DATA_INPUT_FILE_UPLOAD_NOT_UPLOADED = (lookupsNames: string) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.lookupsFileUpload.notUploaded',
|
||||
{
|
||||
defaultMessage: 'The following files did not match any missing lookup: {lookupsNames}',
|
||||
values: { lookupsNames },
|
||||
}
|
||||
);
|
||||
|
||||
export const LOOKUPS_DATA_INPUT_FILE_UPLOAD_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.lookupsFileUpload.button',
|
||||
{ defaultMessage: 'Upload' }
|
||||
);
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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, { useCallback, useMemo } from 'react';
|
||||
import type { EuiStepProps, EuiStepStatus } from '@elastic/eui';
|
||||
import { EMPTY_RESOURCE_PLACEHOLDER } from '../../../../../../../../../common/siem_migrations/constants';
|
||||
import { useUpsertResources } from '../../../../../../service/hooks/use_upsert_resources';
|
||||
import type { RuleMigrationTaskStats } from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import type { UploadedLookups, AddUploadedLookups } from '../../lookups_data_input';
|
||||
import * as i18n from './translations';
|
||||
import { MissingLookupsList } from './missing_lookups_list';
|
||||
|
||||
export interface MissingLookupsListStepProps {
|
||||
status: EuiStepStatus;
|
||||
migrationStats: RuleMigrationTaskStats;
|
||||
missingLookups: string[];
|
||||
uploadedLookups: UploadedLookups;
|
||||
addUploadedLookups: AddUploadedLookups;
|
||||
onCopied: () => void;
|
||||
}
|
||||
export const useMissingLookupsListStep = ({
|
||||
status,
|
||||
migrationStats,
|
||||
missingLookups,
|
||||
uploadedLookups,
|
||||
addUploadedLookups,
|
||||
onCopied,
|
||||
}: MissingLookupsListStepProps): EuiStepProps => {
|
||||
const { upsertResources, isLoading, error } = useUpsertResources(addUploadedLookups);
|
||||
|
||||
const clearLookup = useCallback(
|
||||
(lookupName: string) => {
|
||||
upsertResources(migrationStats.id, [
|
||||
{ type: 'list', name: lookupName, content: EMPTY_RESOURCE_PLACEHOLDER },
|
||||
]);
|
||||
},
|
||||
[upsertResources, migrationStats]
|
||||
);
|
||||
|
||||
const listStepStatus = useMemo(() => {
|
||||
if (isLoading) {
|
||||
return 'loading';
|
||||
}
|
||||
if (error) {
|
||||
return 'danger';
|
||||
}
|
||||
return status;
|
||||
}, [isLoading, error, status]);
|
||||
|
||||
return {
|
||||
title: i18n.LOOKUPS_DATA_INPUT_COPY_TITLE,
|
||||
status: listStepStatus,
|
||||
children: (
|
||||
<MissingLookupsList
|
||||
onCopied={onCopied}
|
||||
missingLookups={missingLookups}
|
||||
uploadedLookups={uploadedLookups}
|
||||
clearLookup={clearLookup}
|
||||
/>
|
||||
),
|
||||
};
|
||||
};
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* 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, { useCallback, useMemo, useState } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiCopy,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { EMPTY_RESOURCE_PLACEHOLDER } from '../../../../../../../../../common/siem_migrations/constants';
|
||||
import type { UploadedLookups } from '../../lookups_data_input';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const scrollPanelCss = css`
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
`;
|
||||
|
||||
interface MissingLookupsListProps {
|
||||
missingLookups: string[];
|
||||
uploadedLookups: UploadedLookups;
|
||||
clearLookup: (lookupsName: string) => void;
|
||||
onCopied: () => void;
|
||||
}
|
||||
export const MissingLookupsList = React.memo<MissingLookupsListProps>(
|
||||
({ missingLookups, uploadedLookups, clearLookup, onCopied }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return (
|
||||
<>
|
||||
<EuiPanel hasShadow={false} hasBorder className={scrollPanelCss}>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
{missingLookups.map((lookupName) => {
|
||||
const isMarkedAsEmpty = uploadedLookups[lookupName] === EMPTY_RESOURCE_PLACEHOLDER;
|
||||
return (
|
||||
<EuiFlexItem key={lookupName}>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
alignItems="center"
|
||||
justifyContent="flexStart"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
{uploadedLookups[lookupName] ? (
|
||||
<EuiIcon type="checkInCircleFilled" color={euiTheme.colors.success} />
|
||||
) : (
|
||||
<EuiIcon type="dot" />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText
|
||||
size="s"
|
||||
style={isMarkedAsEmpty ? { textDecoration: 'line-through' } : {}}
|
||||
>
|
||||
{lookupName}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiCopy textToCopy={lookupName}>
|
||||
{(copy) => (
|
||||
<CopyLookupNameButton
|
||||
lookupName={lookupName}
|
||||
onCopied={onCopied}
|
||||
copy={copy}
|
||||
/>
|
||||
)}
|
||||
</EuiCopy>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ClearLookupButton
|
||||
lookupName={lookupName}
|
||||
clearLookup={clearLookup}
|
||||
isDisabled={isMarkedAsEmpty}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText size="s" color="subdued">
|
||||
{i18n.MISSING_LOOKUPS_DESCRIPTION}
|
||||
</EuiText>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
MissingLookupsList.displayName = 'MissingLookupsList';
|
||||
|
||||
interface CopyLookupNameButtonProps {
|
||||
lookupName: string;
|
||||
onCopied: () => void;
|
||||
copy: () => void;
|
||||
}
|
||||
const CopyLookupNameButton = React.memo<CopyLookupNameButtonProps>(
|
||||
({ lookupName, onCopied, copy }) => {
|
||||
const onClick = useCallback(() => {
|
||||
copy();
|
||||
onCopied();
|
||||
}, [copy, onCopied]);
|
||||
return (
|
||||
<EuiToolTip content={i18n.COPY_LOOKUP_NAME_TOOLTIP}>
|
||||
<EuiButtonIcon
|
||||
onClick={onClick}
|
||||
iconType="copyClipboard"
|
||||
color="text"
|
||||
aria-label={`${i18n.COPY_LOOKUP_NAME_TOOLTIP} ${lookupName}`}
|
||||
data-test-subj="lookupNameCopy"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
);
|
||||
CopyLookupNameButton.displayName = 'CopyLookupNameButton';
|
||||
|
||||
interface ClearLookupButtonProps {
|
||||
lookupName: string;
|
||||
clearLookup: (lookupName: string) => void;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
const ClearLookupButton = React.memo<ClearLookupButtonProps>(
|
||||
({ lookupName, clearLookup, isDisabled: isDisabledDefault }) => {
|
||||
const [isDisabled, setIsDisabled] = useState(isDisabledDefault);
|
||||
const onClick = useCallback(() => {
|
||||
setIsDisabled(true);
|
||||
clearLookup(lookupName);
|
||||
}, [clearLookup, lookupName]);
|
||||
|
||||
const button = useMemo(
|
||||
() => (
|
||||
<EuiButtonIcon
|
||||
onClick={onClick}
|
||||
iconType="cross"
|
||||
color="text"
|
||||
aria-label={i18n.CLEAR_EMPTY_LOOKUP_TOOLTIP}
|
||||
data-test-subj="lookupNameClear"
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
),
|
||||
[onClick, isDisabled]
|
||||
);
|
||||
if (isDisabled) {
|
||||
return button;
|
||||
}
|
||||
return <EuiToolTip content={i18n.CLEAR_EMPTY_LOOKUP_TOOLTIP}>{button}</EuiToolTip>;
|
||||
}
|
||||
);
|
||||
ClearLookupButton.displayName = 'ClearLookupButton';
|
|
@ -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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const LOOKUPS_DATA_INPUT_COPY_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.missingLookupsList.title',
|
||||
{ defaultMessage: 'Lookups found in your rules' }
|
||||
);
|
||||
|
||||
export const MISSING_LOOKUPS_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.missingLookupsList.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'For your lookups, go to your admin Splunk account and the Search and Reporting app Lookups page. Download the following lookups individually and upload below.',
|
||||
}
|
||||
);
|
||||
|
||||
export const COPY_LOOKUP_NAME_TOOLTIP = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.missingLookupsList.copyLookupNameTooltip',
|
||||
{ defaultMessage: 'Copy lookup name' }
|
||||
);
|
||||
export const CLEAR_EMPTY_LOOKUP_TOOLTIP = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.missingLookupsList.clearEmptyLookupTooltip',
|
||||
{ defaultMessage: 'Mark the lookup as empty' }
|
||||
);
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const LOOKUPS_DATA_INPUT_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.title',
|
||||
{ defaultMessage: 'Upload identified lookups' }
|
||||
);
|
||||
export const LOOKUPS_DATA_INPUT_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.lookups.description',
|
||||
{
|
||||
defaultMessage: `We've also found lookups within your rules. To fully translate those rules containing these lookups, follow the step-by-step guide to export and upload them all.`,
|
||||
}
|
||||
);
|
|
@ -6,30 +6,21 @@
|
|||
*/
|
||||
|
||||
import type { EuiStepProps } from '@elastic/eui';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPanel,
|
||||
EuiStepNumber,
|
||||
EuiSteps,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiStepNumber, EuiTitle } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import type { RuleMigrationTaskStats } from '../../../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import { SubStepsWrapper } from '../common/sub_step_wrapper';
|
||||
import type { OnResourcesCreated, OnMissingResourcesFetched, DataInputStep } from '../../types';
|
||||
import type { OnResourcesCreated, OnMissingResourcesFetched } from '../../types';
|
||||
import { getStatus } from '../common/get_status';
|
||||
import * as i18n from './translations';
|
||||
import { DataInputStep } from '../constants';
|
||||
import { SubSteps } from '../common/sub_step';
|
||||
import { useCopyExportQueryStep } from './sub_steps/copy_export_query';
|
||||
import { useMacrosFileUploadStep } from './sub_steps/macros_file_upload';
|
||||
import * as i18n from './translations';
|
||||
import { useCheckResourcesStep } from './sub_steps/check_resources';
|
||||
|
||||
const DataInputStepNumber: DataInputStep = 2;
|
||||
|
||||
interface MacrosDataInputSubStepsProps {
|
||||
migrationStats: RuleMigrationTaskStats;
|
||||
missingMacros: string[];
|
||||
onMacrosCreated: OnResourcesCreated;
|
||||
onMissingResourcesFetched: OnMissingResourcesFetched;
|
||||
}
|
||||
interface MacrosDataInputProps
|
||||
|
@ -39,15 +30,9 @@ interface MacrosDataInputProps
|
|||
missingMacros?: string[];
|
||||
}
|
||||
export const MacrosDataInput = React.memo<MacrosDataInputProps>(
|
||||
({
|
||||
dataInputStep,
|
||||
migrationStats,
|
||||
missingMacros,
|
||||
onMacrosCreated,
|
||||
onMissingResourcesFetched,
|
||||
}) => {
|
||||
({ dataInputStep, migrationStats, missingMacros, onMissingResourcesFetched }) => {
|
||||
const dataInputStatus = useMemo(
|
||||
() => getStatus(DataInputStepNumber, dataInputStep),
|
||||
() => getStatus(DataInputStep.Macros, dataInputStep),
|
||||
[dataInputStep]
|
||||
);
|
||||
|
||||
|
@ -59,7 +44,7 @@ export const MacrosDataInput = React.memo<MacrosDataInputProps>(
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiStepNumber
|
||||
titleSize="xs"
|
||||
number={DataInputStepNumber}
|
||||
number={DataInputStep.Macros}
|
||||
status={dataInputStatus}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
@ -75,7 +60,6 @@ export const MacrosDataInput = React.memo<MacrosDataInputProps>(
|
|||
<MacrosDataInputSubSteps
|
||||
migrationStats={migrationStats}
|
||||
missingMacros={missingMacros}
|
||||
onMacrosCreated={onMacrosCreated}
|
||||
onMissingResourcesFetched={onMissingResourcesFetched}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
@ -90,7 +74,7 @@ MacrosDataInput.displayName = 'MacrosDataInput';
|
|||
const END = 10 as const;
|
||||
type SubStep = 1 | 2 | 3 | typeof END;
|
||||
export const MacrosDataInputSubSteps = React.memo<MacrosDataInputSubStepsProps>(
|
||||
({ migrationStats, missingMacros, onMacrosCreated, onMissingResourcesFetched }) => {
|
||||
({ migrationStats, missingMacros, onMissingResourcesFetched }) => {
|
||||
const [subStep, setSubStep] = useState<SubStep>(missingMacros.length ? 1 : 3);
|
||||
|
||||
// Copy query step
|
||||
|
@ -101,9 +85,8 @@ export const MacrosDataInputSubSteps = React.memo<MacrosDataInputSubStepsProps>(
|
|||
|
||||
// Upload macros step
|
||||
const onMacrosCreatedStep = useCallback<OnResourcesCreated>(() => {
|
||||
onMacrosCreated();
|
||||
setSubStep(3);
|
||||
}, [onMacrosCreated]);
|
||||
}, []);
|
||||
const uploadStep = useMacrosFileUploadStep({
|
||||
status: getStatus(2, subStep),
|
||||
migrationStats,
|
||||
|
@ -130,11 +113,7 @@ export const MacrosDataInputSubSteps = React.memo<MacrosDataInputSubStepsProps>(
|
|||
[copyStep, uploadStep, resourcesStep]
|
||||
);
|
||||
|
||||
return (
|
||||
<SubStepsWrapper>
|
||||
<EuiSteps titleSize="xxs" steps={steps} />
|
||||
</SubStepsWrapper>
|
||||
);
|
||||
return <SubSteps steps={steps} />;
|
||||
}
|
||||
);
|
||||
MacrosDataInputSubSteps.displayName = 'MacrosDataInputActive';
|
||||
|
|
|
@ -42,7 +42,7 @@ export const CopyExportQuery = React.memo<CopyExportQueryProps>(({ onCopied }) =
|
|||
id="xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.copyExportQuery.description"
|
||||
defaultMessage="From you admin Splunk account, go to the {section} app and run the above query. Export your results as {format}."
|
||||
values={{
|
||||
section: <b>{i18n.RULES_DATA_INPUT_COPY_DESCRIPTION_SECTION}</b>,
|
||||
section: <b>{i18n.MACROS_DATA_INPUT_COPY_DESCRIPTION_SECTION}</b>,
|
||||
format: <b>{'JSON'}</b>,
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -19,7 +19,7 @@ export const useCopyExportQueryStep = ({
|
|||
onCopied,
|
||||
}: CopyExportQueryStepProps): EuiStepProps => {
|
||||
return {
|
||||
title: i18n.RULES_DATA_INPUT_COPY_TITLE,
|
||||
title: i18n.MACROS_DATA_INPUT_COPY_TITLE,
|
||||
status,
|
||||
children: <CopyExportQuery onCopied={onCopied} />,
|
||||
};
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const RULES_DATA_INPUT_COPY_TITLE = i18n.translate(
|
||||
export const MACROS_DATA_INPUT_COPY_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.macros.copyExportQuery.title',
|
||||
{ defaultMessage: 'Copy macros query' }
|
||||
);
|
||||
|
||||
export const RULES_DATA_INPUT_COPY_DESCRIPTION_SECTION = i18n.translate(
|
||||
export const MACROS_DATA_INPUT_COPY_DESCRIPTION_SECTION = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.macros.copyExportQuery.description.section',
|
||||
{ defaultMessage: 'Search and Reporting' }
|
||||
);
|
||||
|
|
|
@ -81,7 +81,7 @@ export const useMacrosFileUploadStep = ({
|
|||
}, [isLoading, error, status]);
|
||||
|
||||
return {
|
||||
title: i18n.RULES_DATA_INPUT_FILE_UPLOAD_TITLE,
|
||||
title: i18n.MACROS_DATA_INPUT_FILE_UPLOAD_TITLE,
|
||||
status: uploadStepStatus,
|
||||
children: (
|
||||
<MacrosFileUpload
|
||||
|
|
|
@ -25,8 +25,8 @@ export const MacrosFileUpload = React.memo<MacrosFileUploadProps>(
|
|||
({ createResources, apiError, isLoading }) => {
|
||||
const onFileParsed = useCallback(
|
||||
(content: Array<SplunkRow<SplunkMacroResult>>) => {
|
||||
const rules = content.map(formatMacroRow);
|
||||
createResources(rules);
|
||||
const macros = content.map(formatMacroRow);
|
||||
createResources(macros);
|
||||
},
|
||||
[createResources]
|
||||
);
|
||||
|
@ -56,14 +56,14 @@ export const MacrosFileUpload = React.memo<MacrosFileUploadProps>(
|
|||
initialPromptText={
|
||||
<>
|
||||
<EuiText size="s" textAlign="center">
|
||||
{i18n.RULES_DATA_INPUT_FILE_UPLOAD_PROMPT}
|
||||
{i18n.MACROS_DATA_INPUT_FILE_UPLOAD_PROMPT}
|
||||
</EuiText>
|
||||
</>
|
||||
}
|
||||
accept="application/json, application/x-ndjson"
|
||||
onChange={parseFile}
|
||||
display="large"
|
||||
aria-label="Upload logs sample file"
|
||||
aria-label="Upload macros file"
|
||||
isLoading={isParsing || isLoading}
|
||||
disabled={isParsing || isLoading}
|
||||
data-test-subj="macrosFilePicker"
|
||||
|
|
|
@ -7,20 +7,11 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const RULES_DATA_INPUT_FILE_UPLOAD_TITLE = i18n.translate(
|
||||
export const MACROS_DATA_INPUT_FILE_UPLOAD_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.macros.macrosFileUpload.title',
|
||||
{ defaultMessage: 'Update your macros export' }
|
||||
);
|
||||
export const RULES_DATA_INPUT_FILE_UPLOAD_PROMPT = i18n.translate(
|
||||
export const MACROS_DATA_INPUT_FILE_UPLOAD_PROMPT = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.macros.macrosFileUpload.prompt',
|
||||
{ defaultMessage: 'Select or drag and drop the exported JSON file' }
|
||||
);
|
||||
|
||||
export const RULES_DATA_INPUT_CREATE_MIGRATION_SUCCESS = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.macros.macrosFileUpload.createSuccess',
|
||||
{ defaultMessage: 'Macros uploaded successfully' }
|
||||
);
|
||||
export const RULES_DATA_INPUT_CREATE_MIGRATION_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.macros.macrosFileUpload.createError',
|
||||
{ defaultMessage: 'Failed to upload macros file' }
|
||||
);
|
||||
|
|
|
@ -6,25 +6,17 @@
|
|||
*/
|
||||
|
||||
import type { EuiStepProps } from '@elastic/eui';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPanel,
|
||||
EuiStepNumber,
|
||||
EuiSteps,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiStepNumber, EuiTitle } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import type { RuleMigrationTaskStats } from '../../../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import { SubStepsWrapper } from '../common/sub_step_wrapper';
|
||||
import type { OnMigrationCreated, OnMissingResourcesFetched, DataInputStep } from '../../types';
|
||||
import type { OnMigrationCreated, OnMissingResourcesFetched } from '../../types';
|
||||
import * as i18n from './translations';
|
||||
import { DataInputStep } from '../constants';
|
||||
import { getStatus } from '../common/get_status';
|
||||
import { SubSteps } from '../common/sub_step';
|
||||
import { useCopyExportQueryStep } from './sub_steps/copy_export_query';
|
||||
import { useRulesFileUploadStep } from './sub_steps/rules_file_upload';
|
||||
import * as i18n from './translations';
|
||||
import { useCheckResourcesStep } from './sub_steps/check_resources';
|
||||
import { getStatus } from '../common/get_status';
|
||||
|
||||
const DataInputStepNumber: DataInputStep = 1;
|
||||
|
||||
interface RulesDataInputSubStepsProps {
|
||||
migrationStats?: RuleMigrationTaskStats;
|
||||
|
@ -37,7 +29,7 @@ interface RulesDataInputProps extends RulesDataInputSubStepsProps {
|
|||
export const RulesDataInput = React.memo<RulesDataInputProps>(
|
||||
({ dataInputStep, migrationStats, onMigrationCreated, onMissingResourcesFetched }) => {
|
||||
const dataInputStatus = useMemo(
|
||||
() => getStatus(DataInputStepNumber, dataInputStep),
|
||||
() => getStatus(DataInputStep.Rules, dataInputStep),
|
||||
[dataInputStep]
|
||||
);
|
||||
|
||||
|
@ -49,7 +41,7 @@ export const RulesDataInput = React.memo<RulesDataInputProps>(
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiStepNumber
|
||||
titleSize="xs"
|
||||
number={DataInputStepNumber}
|
||||
number={DataInputStep.Rules}
|
||||
status={dataInputStatus}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
@ -121,11 +113,7 @@ export const RulesDataInputSubSteps = React.memo<RulesDataInputSubStepsProps>(
|
|||
[copyStep, uploadStep, resourcesStep]
|
||||
);
|
||||
|
||||
return (
|
||||
<SubStepsWrapper>
|
||||
<EuiSteps titleSize="xxs" steps={steps} />
|
||||
</SubStepsWrapper>
|
||||
);
|
||||
return <SubSteps steps={steps} />;
|
||||
}
|
||||
);
|
||||
RulesDataInputSubSteps.displayName = 'RulesDataInputActive';
|
||||
|
|
|
@ -13,10 +13,3 @@ import type {
|
|||
export type OnMigrationCreated = (migrationStats: RuleMigrationTaskStats) => void;
|
||||
export type OnResourcesCreated = () => void;
|
||||
export type OnMissingResourcesFetched = (missingResources: RuleMigrationResourceData[]) => void;
|
||||
|
||||
export enum DataInputStep {
|
||||
Rules = 1,
|
||||
Macros = 2,
|
||||
Lookups = 3,
|
||||
End = 10,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiText,
|
||||
EuiPanel,
|
||||
EuiProgress,
|
||||
EuiLoadingSpinner,
|
||||
EuiIcon,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { AssistantIcon } from '@kbn/ai-assistant-icon';
|
||||
import { PanelText } from '../../../../common/components/panel_text';
|
||||
import type { RuleMigrationStats } from '../../types';
|
||||
import * as i18n from './translations';
|
||||
import { RuleMigrationsReadMore } from './read_more';
|
||||
|
||||
export interface MigrationProgressPanelProps {
|
||||
migrationStats: RuleMigrationStats;
|
||||
}
|
||||
export const MigrationProgressPanel = React.memo<MigrationProgressPanelProps>(
|
||||
({ migrationStats }) => {
|
||||
const finishedCount = migrationStats.rules.completed + migrationStats.rules.failed;
|
||||
const progressValue = (finishedCount / migrationStats.rules.total) * 100;
|
||||
|
||||
const preparing = migrationStats.rules.pending === migrationStats.rules.total;
|
||||
|
||||
return (
|
||||
<EuiPanel hasShadow={false} hasBorder paddingSize="m">
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<PanelText size="s" semiBold>
|
||||
<p>{i18n.RULE_MIGRATION_TITLE(migrationStats.number)}</p>
|
||||
</PanelText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s">
|
||||
{i18n.RULE_MIGRATION_PROGRESS_DESCRIPTION(migrationStats.rules.total)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
justifyContent="flexStart"
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon size="m" type={AssistantIcon} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<PanelText size="s" subdued>
|
||||
{preparing ? i18n.RULE_MIGRATION_PREPARING : i18n.RULE_MIGRATION_TRANSLATING}
|
||||
</PanelText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="s" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{!preparing && (
|
||||
<>
|
||||
<EuiProgress
|
||||
value={progressValue}
|
||||
valueText={`${Math.floor(progressValue)}%`}
|
||||
max={100}
|
||||
color="success"
|
||||
/>
|
||||
<EuiSpacer size="xs" />
|
||||
<RuleMigrationsReadMore />
|
||||
</>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
);
|
||||
MigrationProgressPanel.displayName = 'MigrationProgressPanel';
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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, { useCallback, useEffect } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiPanel } from '@elastic/eui';
|
||||
import { CenteredLoadingSpinner } from '../../../../common/components/centered_loading_spinner';
|
||||
import type { RuleMigrationResourceData } from '../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import { PanelText } from '../../../../common/components/panel_text';
|
||||
import { useStartMigration } from '../../service/hooks/use_start_migration';
|
||||
import type { RuleMigrationStats } from '../../types';
|
||||
import { useRuleMigrationDataInputContext } from '../data_input_flyout/context';
|
||||
import * as i18n from './translations';
|
||||
import { useGetMissingResources } from '../../service/hooks/use_get_missing_resources';
|
||||
|
||||
export interface MigrationReadyPanelProps {
|
||||
migrationStats: RuleMigrationStats;
|
||||
}
|
||||
export const MigrationReadyPanel = React.memo<MigrationReadyPanelProps>(({ migrationStats }) => {
|
||||
const { openFlyout } = useRuleMigrationDataInputContext();
|
||||
const [missingResources, setMissingResources] = React.useState<RuleMigrationResourceData[]>([]);
|
||||
const { getMissingResources, isLoading } = useGetMissingResources(setMissingResources);
|
||||
|
||||
useEffect(() => {
|
||||
getMissingResources(migrationStats.id);
|
||||
}, [getMissingResources, migrationStats.id]);
|
||||
|
||||
const onOpenFlyout = useCallback<React.MouseEventHandler>(() => {
|
||||
openFlyout(migrationStats);
|
||||
}, [openFlyout, migrationStats]);
|
||||
|
||||
return (
|
||||
<EuiPanel hasShadow={false} hasBorder paddingSize="m">
|
||||
<EuiFlexGroup direction="row" gutterSize="m" alignItems="flexEnd">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<PanelText size="s" semiBold>
|
||||
<p>{i18n.RULE_MIGRATION_TITLE(migrationStats.number)}</p>
|
||||
</PanelText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<PanelText size="s" subdued>
|
||||
{i18n.RULE_MIGRATION_READY_DESCRIPTION(
|
||||
migrationStats.rules.total,
|
||||
!isLoading && missingResources.length > 0
|
||||
? i18n.RULE_MIGRATION_READY_MISSING_RESOURCES
|
||||
: ''
|
||||
)}
|
||||
</PanelText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
{isLoading ? (
|
||||
<CenteredLoadingSpinner />
|
||||
) : (
|
||||
<EuiFlexItem grow={false}>
|
||||
{missingResources.length > 0 ? (
|
||||
<EuiButton fill iconType="download" iconSide="right" onClick={onOpenFlyout} size="s">
|
||||
{i18n.RULE_MIGRATION_UPLOAD_BUTTON}
|
||||
</EuiButton>
|
||||
) : (
|
||||
<StartTranslationButton migrationId={migrationStats.id} />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
});
|
||||
MigrationReadyPanel.displayName = 'MigrationReadyPanel';
|
||||
|
||||
const StartTranslationButton = React.memo<{ migrationId: string }>(({ migrationId }) => {
|
||||
const { startMigration, isLoading } = useStartMigration();
|
||||
const onStartMigration = useCallback(() => {
|
||||
startMigration(migrationId);
|
||||
}, [migrationId, startMigration]);
|
||||
|
||||
return (
|
||||
<EuiButton fill onClick={onStartMigration} isLoading={isLoading} size="s">
|
||||
{i18n.RULE_MIGRATION_START_TRANSLATION_BUTTON}
|
||||
</EuiButton>
|
||||
);
|
||||
});
|
||||
StartTranslationButton.displayName = 'StartTranslationButton';
|
|
@ -0,0 +1,227 @@
|
|||
/*
|
||||
* 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, { useMemo } from 'react';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPanel,
|
||||
EuiHorizontalRule,
|
||||
EuiIcon,
|
||||
EuiBasicTable,
|
||||
EuiHealth,
|
||||
EuiText,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { Chart, BarSeries, Settings, ScaleType, DARK_THEME, LIGHT_THEME } from '@elastic/charts';
|
||||
import { SecurityPageName } from '@kbn/security-solution-navigation';
|
||||
import { AssistantIcon } from '@kbn/ai-assistant-icon';
|
||||
import { PanelText } from '../../../../common/components/panel_text';
|
||||
import {
|
||||
convertTranslationResultIntoText,
|
||||
useResultVisColors,
|
||||
} from '../../utils/translation_results';
|
||||
import type { RuleMigrationTranslationStats } from '../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import { useGetMigrationTranslationStats } from '../../logic/use_get_migration_translation_stats';
|
||||
import { CenteredLoadingSpinner } from '../../../../common/components/centered_loading_spinner';
|
||||
import { SecuritySolutionLinkButton } from '../../../../common/components/links';
|
||||
import type { RuleMigrationStats } from '../../types';
|
||||
import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export interface MigrationResultPanelProps {
|
||||
migrationStats: RuleMigrationStats;
|
||||
}
|
||||
export const MigrationResultPanel = React.memo<MigrationResultPanelProps>(({ migrationStats }) => {
|
||||
const { data: translationStats, isLoading: isLoadingTranslationStats } =
|
||||
useGetMigrationTranslationStats(migrationStats.id);
|
||||
return (
|
||||
<EuiPanel hasShadow={false} hasBorder paddingSize="none">
|
||||
<EuiPanel hasShadow={false} hasBorder={false} paddingSize="m">
|
||||
<EuiFlexGroup direction="column" alignItems="flexStart" gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<PanelText size="s" semiBold>
|
||||
<p>{i18n.RULE_MIGRATION_COMPLETE_TITLE(migrationStats.number)}</p>
|
||||
</PanelText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<PanelText size="s" subdued>
|
||||
<p>
|
||||
{i18n.RULE_MIGRATION_COMPLETE_DESCRIPTION(
|
||||
moment(migrationStats.created_at).format('MMMM Do YYYY, h:mm:ss a'),
|
||||
moment(migrationStats.last_updated_at).fromNow()
|
||||
)}
|
||||
</p>
|
||||
</PanelText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
<EuiPanel hasShadow={false} hasBorder={false} paddingSize="m">
|
||||
<EuiFlexGroup direction="column" alignItems="stretch" gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup direction="row" alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type={AssistantIcon} size="m" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<PanelText size="s" semiBold>
|
||||
<p>{i18n.RULE_MIGRATION_SUMMARY_TITLE}</p>
|
||||
</PanelText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiPanel hasShadow={false} hasBorder paddingSize="m">
|
||||
<EuiFlexGroup direction="column" alignItems="stretch" justifyContent="center">
|
||||
<EuiFlexItem>
|
||||
{isLoadingTranslationStats ? (
|
||||
<CenteredLoadingSpinner />
|
||||
) : (
|
||||
translationStats && (
|
||||
<>
|
||||
<EuiText size="m" style={{ textAlign: 'center' }}>
|
||||
<b>{i18n.RULE_MIGRATION_SUMMARY_CHART_TITLE}</b>
|
||||
</EuiText>
|
||||
<TranslationResultsChart translationStats={translationStats} />
|
||||
<TranslationResultsTable translationStats={translationStats} />
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup direction="column" alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<SecuritySolutionLinkButton
|
||||
deepLinkId={SecurityPageName.siemMigrationsRules}
|
||||
path={migrationStats.id}
|
||||
>
|
||||
{i18n.RULE_MIGRATION_VIEW_TRANSLATED_RULES_BUTTON}
|
||||
</SecuritySolutionLinkButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{/* TODO: uncomment when retry API is ready <RuleMigrationsUploadMissingPanel migrationStats={migrationStats} spacerSizeTop="s" /> */}
|
||||
</EuiPanel>
|
||||
</EuiPanel>
|
||||
);
|
||||
});
|
||||
MigrationResultPanel.displayName = 'MigrationResultPanel';
|
||||
|
||||
const TranslationResultsChart = React.memo<{
|
||||
translationStats: RuleMigrationTranslationStats;
|
||||
}>(({ translationStats }) => {
|
||||
const { colorMode } = useEuiTheme();
|
||||
const translationResultColors = useResultVisColors();
|
||||
const data = [
|
||||
{
|
||||
category: 'Results',
|
||||
type: convertTranslationResultIntoText(RuleTranslationResult.FULL),
|
||||
value: translationStats.rules.success.result.full,
|
||||
},
|
||||
{
|
||||
category: 'Results',
|
||||
type: convertTranslationResultIntoText(RuleTranslationResult.PARTIAL),
|
||||
value: translationStats.rules.success.result.partial,
|
||||
},
|
||||
{
|
||||
category: 'Results',
|
||||
type: convertTranslationResultIntoText(RuleTranslationResult.UNTRANSLATABLE),
|
||||
value: translationStats.rules.success.result.untranslatable,
|
||||
},
|
||||
{
|
||||
category: 'Results',
|
||||
type: i18n.RULE_MIGRATION_TRANSLATION_FAILED,
|
||||
value: translationStats.rules.failed,
|
||||
},
|
||||
];
|
||||
|
||||
const colors = [
|
||||
translationResultColors[RuleTranslationResult.FULL],
|
||||
translationResultColors[RuleTranslationResult.PARTIAL],
|
||||
translationResultColors[RuleTranslationResult.UNTRANSLATABLE],
|
||||
translationResultColors.error,
|
||||
];
|
||||
|
||||
return (
|
||||
<Chart size={{ height: 130 }}>
|
||||
<Settings
|
||||
showLegend={false}
|
||||
rotation={90}
|
||||
baseTheme={colorMode === 'DARK' ? DARK_THEME : LIGHT_THEME}
|
||||
/>
|
||||
<BarSeries
|
||||
id="results"
|
||||
name="Results"
|
||||
data={data}
|
||||
xAccessor="category"
|
||||
yAccessors={['value']}
|
||||
splitSeriesAccessors={['type']}
|
||||
stackAccessors={['category']}
|
||||
xScaleType={ScaleType.Ordinal}
|
||||
yScaleType={ScaleType.Linear}
|
||||
color={colors}
|
||||
/>
|
||||
</Chart>
|
||||
);
|
||||
});
|
||||
TranslationResultsChart.displayName = 'TranslationResultsChart';
|
||||
|
||||
const TranslationResultsTable = React.memo<{
|
||||
translationStats: RuleMigrationTranslationStats;
|
||||
}>(({ translationStats }) => {
|
||||
const translationResultColors = useResultVisColors();
|
||||
const items = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
title: convertTranslationResultIntoText(RuleTranslationResult.FULL),
|
||||
value: translationStats.rules.success.result.full,
|
||||
color: translationResultColors[RuleTranslationResult.FULL],
|
||||
},
|
||||
{
|
||||
title: convertTranslationResultIntoText(RuleTranslationResult.PARTIAL),
|
||||
value: translationStats.rules.success.result.partial,
|
||||
color: translationResultColors[RuleTranslationResult.PARTIAL],
|
||||
},
|
||||
{
|
||||
title: convertTranslationResultIntoText(RuleTranslationResult.UNTRANSLATABLE),
|
||||
value: translationStats.rules.success.result.untranslatable,
|
||||
color: translationResultColors[RuleTranslationResult.UNTRANSLATABLE],
|
||||
},
|
||||
{
|
||||
title: i18n.RULE_MIGRATION_TRANSLATION_FAILED,
|
||||
value: translationStats.rules.failed,
|
||||
color: translationResultColors.error,
|
||||
},
|
||||
];
|
||||
}, [translationStats, translationResultColors]);
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
items={items}
|
||||
compressed
|
||||
columns={[
|
||||
{
|
||||
field: 'title',
|
||||
name: i18n.RULE_MIGRATION_TABLE_COLUMN_RESULT,
|
||||
render: (value: string, { color }) => <EuiHealth color={color}>{value}</EuiHealth>,
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
name: i18n.RULE_MIGRATION_TABLE_COLUMN_RULES,
|
||||
align: 'right',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
TranslationResultsTable.displayName = 'TranslationResultsTable';
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { EuiLink } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { PanelText } from '../../../../common/components/panel_text';
|
||||
import { useKibana } from '../../../../common/lib/kibana/kibana_react';
|
||||
|
||||
export const RuleMigrationsReadMore = React.memo(() => {
|
||||
const docLink = useKibana().services.docLinks.links.siem.gettingStarted;
|
||||
return (
|
||||
<PanelText size="xs" subdued>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.siemMigrations.rules.panel.help.readMore"
|
||||
defaultMessage="Read more about our AI powered translations and other features. {readMore}"
|
||||
values={{
|
||||
readMore: (
|
||||
<EuiLink href={docLink} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.siemMigrations.rules.panel.help.readDocs"
|
||||
defaultMessage="Read AI docs"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</PanelText>
|
||||
);
|
||||
});
|
||||
RuleMigrationsReadMore.displayName = 'RuleMigrationsReadMore';
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const RULE_MIGRATION_READY_DESCRIPTION = (
|
||||
totalRules: number,
|
||||
missingResourcesText: string
|
||||
) =>
|
||||
i18n.translate('xpack.securitySolution.siemMigrations.rules.panel.ready.description', {
|
||||
defaultMessage:
|
||||
'Migration of {totalRules} rules is created but the translation has not started yet. {missingResourcesText}',
|
||||
values: { totalRules, missingResourcesText },
|
||||
});
|
||||
export const RULE_MIGRATION_READY_MISSING_RESOURCES = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.panel.ready.missingResources',
|
||||
{ defaultMessage: 'Upload macros & lookups and start the translation process' }
|
||||
);
|
||||
|
||||
export const RULE_MIGRATION_START_TRANSLATION_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.panel.translate.button',
|
||||
{ defaultMessage: 'Start translation' }
|
||||
);
|
||||
export const RULE_MIGRATION_TITLE = (number: number) =>
|
||||
i18n.translate('xpack.securitySolution.siemMigrations.rules.panel.migrationTitle', {
|
||||
defaultMessage: 'SIEM rules migration #{number}',
|
||||
values: { number },
|
||||
});
|
||||
|
||||
export const RULE_MIGRATION_PROGRESS_DESCRIPTION = (totalRules: number) =>
|
||||
i18n.translate('xpack.securitySolution.siemMigrations.rules.panel.progress.description', {
|
||||
defaultMessage: `Processing migration of {totalRules} rules.`,
|
||||
values: { totalRules },
|
||||
});
|
||||
export const RULE_MIGRATION_PREPARING = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.panel.preparing',
|
||||
{ defaultMessage: `Preparing environment for the AI powered translation.` }
|
||||
);
|
||||
export const RULE_MIGRATION_TRANSLATING = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.panel.translating',
|
||||
{ defaultMessage: `Translating rules` }
|
||||
);
|
||||
|
||||
export const RULE_MIGRATION_COMPLETE_TITLE = (number: number) =>
|
||||
i18n.translate('xpack.securitySolution.siemMigrations.rules.panel.result.title', {
|
||||
defaultMessage: 'SIEM rules migration #{number} complete',
|
||||
values: { number },
|
||||
});
|
||||
|
||||
export const RULE_MIGRATION_COMPLETE_DESCRIPTION = (createdAt: string, finishedAt: string) =>
|
||||
i18n.translate('xpack.securitySolution.siemMigrations.rules.panel.result.description', {
|
||||
defaultMessage: 'Export uploaded on {createdAt} and translation finished {finishedAt}.',
|
||||
values: { createdAt, finishedAt },
|
||||
});
|
||||
|
||||
export const RULE_MIGRATION_SUMMARY_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.panel.result.summary.title',
|
||||
{ defaultMessage: 'Translation Summary' }
|
||||
);
|
||||
|
||||
export const RULE_MIGRATION_SUMMARY_CHART_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.panel.result.summary.chartTitle',
|
||||
{ defaultMessage: 'Rules by translation status' }
|
||||
);
|
||||
|
||||
export const RULE_MIGRATION_VIEW_TRANSLATED_RULES_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.panel.result.summary.button',
|
||||
{ defaultMessage: 'View translated rules' }
|
||||
);
|
||||
|
||||
export const RULE_MIGRATION_TRANSLATION_FAILED = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.panel.result.summary.failed',
|
||||
{ defaultMessage: 'Failed' }
|
||||
);
|
||||
|
||||
export const RULE_MIGRATION_TABLE_COLUMN_RESULT = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.panel.result.summary.tableColumn.result',
|
||||
{ defaultMessage: 'Result' }
|
||||
);
|
||||
export const RULE_MIGRATION_TABLE_COLUMN_RULES = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.panel.result.summary.tableColumn.rules',
|
||||
{ defaultMessage: 'Rules' }
|
||||
);
|
||||
|
||||
export const RULE_MIGRATION_UPLOAD_MISSING_RESOURCES_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.panel.uploadMissingResources',
|
||||
{ defaultMessage: 'Upload missing Macros and Lookups.' }
|
||||
);
|
||||
export const RULE_MIGRATION_UPLOAD_MISSING_RESOURCES_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.panel.uploadMissingResourcesDescription',
|
||||
{ defaultMessage: 'Click upload for step-by-step guidance to finish partially translated rules.' }
|
||||
);
|
||||
|
||||
export const RULE_MIGRATION_UPLOAD_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.panel.uploadMacros.button',
|
||||
{ defaultMessage: 'Upload' }
|
||||
);
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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, { useCallback, useEffect } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { AssistantIcon } from '@kbn/ai-assistant-icon';
|
||||
import type { SpacerSize } from '@elastic/eui/src/components/spacer/spacer';
|
||||
import type { RuleMigrationResourceData } from '../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import { PanelText } from '../../../../common/components/panel_text';
|
||||
import { useGetMissingResources } from '../../service/hooks/use_get_missing_resources';
|
||||
import * as i18n from './translations';
|
||||
import { useRuleMigrationDataInputContext } from '../data_input_flyout/context';
|
||||
import type { RuleMigrationStats } from '../../types';
|
||||
|
||||
interface RuleMigrationsUploadMissingPanelProps {
|
||||
migrationStats: RuleMigrationStats;
|
||||
spacerSizeTop?: SpacerSize;
|
||||
}
|
||||
export const RuleMigrationsUploadMissingPanel = React.memo<RuleMigrationsUploadMissingPanelProps>(
|
||||
({ migrationStats, spacerSizeTop }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const { openFlyout } = useRuleMigrationDataInputContext();
|
||||
const [missingResources, setMissingResources] = React.useState<RuleMigrationResourceData[]>([]);
|
||||
const { getMissingResources, isLoading } = useGetMissingResources(setMissingResources);
|
||||
|
||||
useEffect(() => {
|
||||
getMissingResources(migrationStats.id);
|
||||
}, [getMissingResources, migrationStats.id]);
|
||||
|
||||
const onOpenFlyout = useCallback(() => {
|
||||
openFlyout(migrationStats);
|
||||
}, [migrationStats, openFlyout]);
|
||||
|
||||
if (isLoading || missingResources.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{spacerSizeTop && <EuiSpacer size={spacerSizeTop} />}
|
||||
<EuiPanel
|
||||
hasShadow={false}
|
||||
hasBorder
|
||||
paddingSize="s"
|
||||
style={{ backgroundColor: euiTheme.colors.backgroundBasePrimary }}
|
||||
>
|
||||
<EuiFlexGroup direction="row" gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<AssistantIcon />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<PanelText size="s" semiBold>
|
||||
{i18n.RULE_MIGRATION_UPLOAD_MISSING_RESOURCES_TITLE}
|
||||
</PanelText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<PanelText size="s" subdued>
|
||||
{i18n.RULE_MIGRATION_UPLOAD_MISSING_RESOURCES_DESCRIPTION}
|
||||
</PanelText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
color="primary"
|
||||
onClick={onOpenFlyout}
|
||||
iconType="download"
|
||||
iconSide="right"
|
||||
size="s"
|
||||
>
|
||||
{i18n.RULE_MIGRATION_UPLOAD_BUTTON}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
RuleMigrationsUploadMissingPanel.displayName = 'RuleMigrationsUploadMissingPanel';
|
|
@ -26,7 +26,7 @@ import * as i18n from './translations';
|
|||
import {
|
||||
convertTranslationResultIntoColor,
|
||||
convertTranslationResultIntoText,
|
||||
} from '../../../../utils/helpers';
|
||||
} from '../../../../utils/translation_results';
|
||||
import { TranslationCallOut } from './callout';
|
||||
|
||||
interface TranslationTabProps {
|
||||
|
|
|
@ -31,7 +31,7 @@ import { useGetMigrationPrebuiltRules } from '../../logic/use_get_migration_preb
|
|||
import * as logicI18n from '../../logic/translations';
|
||||
import { BulkActions } from './bulk_actions';
|
||||
import { SearchField } from './search_field';
|
||||
import { SiemMigrationRuleTranslationResult } from '../../../../../common/siem_migrations/constants';
|
||||
import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 10;
|
||||
|
@ -80,10 +80,7 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
|
|||
const tableSelection: EuiTableSelectionType<RuleMigration> = useMemo(
|
||||
() => ({
|
||||
selectable: (item: RuleMigration) => {
|
||||
return (
|
||||
!item.elastic_rule?.id &&
|
||||
item.translation_result === SiemMigrationRuleTranslationResult.FULL
|
||||
);
|
||||
return !item.elastic_rule?.id && item.translation_result === RuleTranslationResult.FULL;
|
||||
},
|
||||
selectableMessage: (selectable: boolean, item: RuleMigration) => {
|
||||
if (selectable) {
|
||||
|
@ -190,7 +187,7 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
|
|||
const canMigrationRuleBeInstalled =
|
||||
!isLoading &&
|
||||
!ruleMigration.elastic_rule?.id &&
|
||||
ruleMigration.translation_result === SiemMigrationRuleTranslationResult.FULL;
|
||||
ruleMigration.translation_result === RuleTranslationResult.FULL;
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
|
@ -271,7 +268,7 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
|
|||
<EuiFlexItem grow={false}>
|
||||
<BulkActions
|
||||
isTableLoading={isLoading}
|
||||
numberOfTranslatedRules={translationStats?.rules.installable ?? 0}
|
||||
numberOfTranslatedRules={translationStats?.rules.success.installable ?? 0}
|
||||
numberOfSelectedRules={selectedRuleMigrations.length}
|
||||
installTranslatedRule={installTranslatedRules}
|
||||
installSelectedRule={installSelectedRule}
|
||||
|
|
|
@ -6,16 +6,18 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { euiLightVars } from '@kbn/ui-theme';
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiHealth, EuiIcon, EuiToolTip } from '@elastic/eui';
|
||||
import { css } from '@emotion/css';
|
||||
import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants';
|
||||
import {
|
||||
convertTranslationResultIntoText,
|
||||
useResultVisColors,
|
||||
} from '../../utils/translation_results';
|
||||
import {
|
||||
RuleMigrationStatusEnum,
|
||||
type RuleMigration,
|
||||
type RuleMigrationTranslationResult,
|
||||
} from '../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import { convertTranslationResultIntoText } from '../../utils/helpers';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const statusTextWrapperClassName = css`
|
||||
|
@ -23,13 +25,6 @@ const statusTextWrapperClassName = css`
|
|||
display: inline-grid;
|
||||
`;
|
||||
|
||||
const { euiColorVis0, euiColorVis7, euiColorVis9 } = euiLightVars;
|
||||
const statusToColorMap: Record<RuleMigrationTranslationResult, string> = {
|
||||
full: euiColorVis0,
|
||||
partial: euiColorVis7,
|
||||
untranslatable: euiColorVis9,
|
||||
};
|
||||
|
||||
interface StatusBadgeProps {
|
||||
migrationRule: RuleMigration;
|
||||
'data-test-subj'?: string;
|
||||
|
@ -37,13 +32,14 @@ interface StatusBadgeProps {
|
|||
|
||||
export const StatusBadge: React.FC<StatusBadgeProps> = React.memo(
|
||||
({ migrationRule, 'data-test-subj': dataTestSubj = 'translation-result' }) => {
|
||||
const colors = useResultVisColors();
|
||||
// Installed
|
||||
if (migrationRule.elastic_rule?.id) {
|
||||
return (
|
||||
<EuiToolTip content={i18n.RULE_STATUS_INSTALLED}>
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="check" color={statusToColorMap.full} />
|
||||
<EuiIcon type="check" color={colors[RuleTranslationResult.FULL]} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{i18n.RULE_STATUS_INSTALLED}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -67,7 +63,7 @@ export const StatusBadge: React.FC<StatusBadgeProps> = React.memo(
|
|||
|
||||
const translationResult = migrationRule.translation_result ?? 'untranslatable';
|
||||
const displayValue = convertTranslationResultIntoText(translationResult);
|
||||
const color = statusToColorMap[translationResult];
|
||||
const color = colors[translationResult];
|
||||
|
||||
return (
|
||||
<EuiToolTip content={displayValue}>
|
||||
|
|
|
@ -20,7 +20,7 @@ export type UpsertResources = (
|
|||
migrationId: string,
|
||||
data: UpsertRuleMigrationResourcesRequestBody
|
||||
) => void;
|
||||
export type OnSuccess = () => void;
|
||||
export type OnSuccess = (data: UpsertRuleMigrationResourcesRequestBody) => void;
|
||||
|
||||
export const useUpsertResources = (onSuccess: OnSuccess) => {
|
||||
const { siemMigrations, notifications } = useKibana().services;
|
||||
|
@ -33,7 +33,7 @@ export const useUpsertResources = (onSuccess: OnSuccess) => {
|
|||
dispatch({ type: 'start' });
|
||||
await siemMigrations.rules.upsertMigrationResources(migrationId, data);
|
||||
|
||||
onSuccess();
|
||||
onSuccess(data);
|
||||
dispatch({ type: 'success' });
|
||||
} catch (err) {
|
||||
const apiError = err.body ?? err;
|
||||
|
|
|
@ -45,7 +45,7 @@ import * as i18n from './translations';
|
|||
const NAMESPACE_TRACE_OPTIONS_SESSION_STORAGE_KEY =
|
||||
`${DEFAULT_ASSISTANT_NAMESPACE}.${TRACE_OPTIONS_SESSION_STORAGE_KEY}` as const;
|
||||
|
||||
const REQUEST_POLLING_INTERVAL_MS = 5000 as const;
|
||||
const REQUEST_POLLING_INTERVAL_SECONDS = 10 as const;
|
||||
const CREATE_MIGRATION_BODY_BATCH_SIZE = 50 as const;
|
||||
|
||||
export class SiemRulesMigrationsService {
|
||||
|
@ -213,7 +213,7 @@ export class SiemRulesMigrationsService {
|
|||
}
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, REQUEST_POLLING_INTERVAL_MS));
|
||||
await new Promise((resolve) => setTimeout(resolve, REQUEST_POLLING_INTERVAL_SECONDS * 1000));
|
||||
} while (pendingMigrationIds.length > 0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,44 +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 {
|
||||
RuleMigrationTranslationResultEnum,
|
||||
type RuleMigrationTranslationResult,
|
||||
} from '../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const convertTranslationResultIntoColor = (status?: RuleMigrationTranslationResult) => {
|
||||
switch (status) {
|
||||
case RuleMigrationTranslationResultEnum.full:
|
||||
return 'primary';
|
||||
|
||||
case RuleMigrationTranslationResultEnum.partial:
|
||||
return 'warning';
|
||||
|
||||
case RuleMigrationTranslationResultEnum.untranslatable:
|
||||
return 'danger';
|
||||
|
||||
default:
|
||||
throw new Error(i18n.SIEM_TRANSLATION_RESULT_UNKNOWN_ERROR(status));
|
||||
}
|
||||
};
|
||||
|
||||
export const convertTranslationResultIntoText = (status?: RuleMigrationTranslationResult) => {
|
||||
switch (status) {
|
||||
case RuleMigrationTranslationResultEnum.full:
|
||||
return i18n.SIEM_TRANSLATION_RESULT_FULL_LABEL;
|
||||
|
||||
case RuleMigrationTranslationResultEnum.partial:
|
||||
return i18n.SIEM_TRANSLATION_RESULT_PARTIAL_LABEL;
|
||||
|
||||
case RuleMigrationTranslationResultEnum.untranslatable:
|
||||
return i18n.SIEM_TRANSLATION_RESULT_UNTRANSLATABLE_LABEL;
|
||||
|
||||
default:
|
||||
throw new Error(i18n.SIEM_TRANSLATION_RESULT_UNKNOWN_ERROR(status));
|
||||
}
|
||||
};
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { useEuiTheme } from '@elastic/eui';
|
||||
import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants';
|
||||
import type { RuleMigrationTranslationResult } from '../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const useResultVisColors = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return {
|
||||
[RuleTranslationResult.FULL]: euiTheme.colors.vis.euiColorVis0,
|
||||
[RuleTranslationResult.PARTIAL]: euiTheme.colors.vis.euiColorVis5,
|
||||
[RuleTranslationResult.UNTRANSLATABLE]: euiTheme.colors.vis.euiColorVis7,
|
||||
error: euiTheme.colors.vis.euiColorVis9,
|
||||
};
|
||||
};
|
||||
|
||||
export const convertTranslationResultIntoColor = (status?: RuleMigrationTranslationResult) => {
|
||||
switch (status) {
|
||||
case RuleTranslationResult.FULL:
|
||||
return 'primary';
|
||||
case RuleTranslationResult.PARTIAL:
|
||||
return 'warning';
|
||||
case RuleTranslationResult.UNTRANSLATABLE:
|
||||
return 'danger';
|
||||
default:
|
||||
return 'subdued';
|
||||
}
|
||||
};
|
||||
|
||||
export const convertTranslationResultIntoText = (status?: RuleMigrationTranslationResult) => {
|
||||
switch (status) {
|
||||
case RuleTranslationResult.FULL:
|
||||
return i18n.SIEM_TRANSLATION_RESULT_FULL_LABEL;
|
||||
case RuleTranslationResult.PARTIAL:
|
||||
return i18n.SIEM_TRANSLATION_RESULT_PARTIAL_LABEL;
|
||||
case RuleTranslationResult.UNTRANSLATABLE:
|
||||
return i18n.SIEM_TRANSLATION_RESULT_UNTRANSLATABLE_LABEL;
|
||||
default:
|
||||
return i18n.SIEM_TRANSLATION_RESULT_UNKNOWN_LABEL;
|
||||
}
|
||||
};
|
|
@ -24,7 +24,7 @@ export const SIEM_TRANSLATION_RESULT_PARTIAL_LABEL = i18n.translate(
|
|||
export const SIEM_TRANSLATION_RESULT_UNTRANSLATABLE_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.translationResult.untranslatable',
|
||||
{
|
||||
defaultMessage: 'Not translated',
|
||||
defaultMessage: 'Needs manual translation',
|
||||
}
|
||||
);
|
||||
|
|
@ -27,6 +27,7 @@ export const registerSiemRuleMigrationsResourceUpsertRoute = (
|
|||
path: SIEM_RULE_MIGRATION_RESOURCES_PATH,
|
||||
access: 'internal',
|
||||
security: { authz: { requiredPrivileges: ['securitySolution'] } },
|
||||
options: { body: { maxBytes: 26214400 } }, // rise payload limit to 25MB
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
|
|
|
@ -41,6 +41,9 @@ export const registerSiemRuleMigrationsStatsRoute = (
|
|||
|
||||
const stats = await ruleMigrationsClient.task.getStats(migrationId);
|
||||
|
||||
if (stats.rules.total === 0) {
|
||||
return res.noContent();
|
||||
}
|
||||
return res.ok({ body: stats });
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
|
|
|
@ -45,6 +45,9 @@ export const registerSiemRuleMigrationsTranslationStatsRoute = (
|
|||
|
||||
const stats = await ruleMigrationsClient.data.rules.getTranslationStats(migrationId);
|
||||
|
||||
if (stats.rules.total === 0) {
|
||||
return res.noContent();
|
||||
}
|
||||
return res.ok({ body: stats });
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
|
|
|
@ -16,7 +16,10 @@ import type {
|
|||
Duration,
|
||||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { StoredRuleMigration } from '../types';
|
||||
import { SiemMigrationStatus } from '../../../../../common/siem_migrations/constants';
|
||||
import {
|
||||
SiemMigrationStatus,
|
||||
RuleTranslationResult,
|
||||
} from '../../../../../common/siem_migrations/constants';
|
||||
import {
|
||||
type RuleMigration,
|
||||
type RuleMigrationTaskStats,
|
||||
|
@ -128,19 +131,14 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
|
|||
/** Retrieves an array of rule documents of a specific migrations */
|
||||
async get(
|
||||
migrationId: string,
|
||||
{ filters = {}, sort = {}, from, size }: RuleMigrationGetOptions = {}
|
||||
{ filters = {}, sort: sortParam = {}, from, size }: RuleMigrationGetOptions = {}
|
||||
): Promise<{ total: number; data: StoredRuleMigration[] }> {
|
||||
const index = await this.getIndexName();
|
||||
const query = this.getFilterQuery(migrationId, filters);
|
||||
const sort = sortParam.sortField ? getSortingOptions(sortParam) : undefined;
|
||||
|
||||
const result = await this.esClient
|
||||
.search<RuleMigration>({
|
||||
index,
|
||||
query,
|
||||
sort: sort.sortField ? getSortingOptions(sort) : undefined,
|
||||
from,
|
||||
size,
|
||||
})
|
||||
.search<RuleMigration>({ index, query, sort, from, size })
|
||||
.catch((error) => {
|
||||
this.logger.error(`Error searching rule migrations: ${error.message}`);
|
||||
throw error;
|
||||
|
@ -268,8 +266,15 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
|
|||
const query = this.getFilterQuery(migrationId);
|
||||
|
||||
const aggregations = {
|
||||
prebuilt: { filter: searchConditions.isPrebuilt() },
|
||||
installable: { filter: { bool: { must: searchConditions.isInstallable() } } },
|
||||
success: {
|
||||
filter: { term: { status: SiemMigrationStatus.COMPLETED } },
|
||||
aggs: {
|
||||
result: { terms: { field: 'translation_result' } },
|
||||
installable: { filter: { bool: { must: searchConditions.isInstallable() } } },
|
||||
prebuilt: { filter: searchConditions.isPrebuilt() },
|
||||
},
|
||||
},
|
||||
failed: { filter: { term: { status: SiemMigrationStatus.FAILED } } },
|
||||
};
|
||||
const result = await this.esClient
|
||||
.search({ index, query, aggregations, _source: false })
|
||||
|
@ -278,16 +283,22 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
|
|||
throw error;
|
||||
});
|
||||
|
||||
const bucket = result.aggregations ?? {};
|
||||
const aggs = result.aggregations ?? {};
|
||||
const total = this.getTotalHits(result);
|
||||
const prebuilt = (bucket.prebuilt as AggregationsFilterAggregate)?.doc_count ?? 0;
|
||||
const successAgg = aggs.success as AggregationsFilterAggregate;
|
||||
const translationResultsAgg = successAgg.result as AggregationsStringTermsAggregate;
|
||||
|
||||
return {
|
||||
id: migrationId,
|
||||
rules: {
|
||||
total,
|
||||
prebuilt,
|
||||
custom: total - prebuilt,
|
||||
installable: (bucket.installable as AggregationsFilterAggregate)?.doc_count ?? 0,
|
||||
success: {
|
||||
total: (successAgg as AggregationsFilterAggregate)?.doc_count ?? 0,
|
||||
result: this.translationResultAggCount(translationResultsAgg),
|
||||
installable: (successAgg.installable as AggregationsFilterAggregate)?.doc_count ?? 0,
|
||||
prebuilt: (successAgg.prebuilt as AggregationsFilterAggregate)?.doc_count ?? 0,
|
||||
},
|
||||
failed: (aggs.failed as AggregationsFilterAggregate)?.doc_count ?? 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -297,10 +308,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
|
|||
const index = await this.getIndexName();
|
||||
const query = this.getFilterQuery(migrationId);
|
||||
const 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 } } },
|
||||
status: { terms: { field: 'status' } },
|
||||
createdAt: { min: { field: '@timestamp' } },
|
||||
lastUpdatedAt: { max: { field: 'updated_at' } },
|
||||
};
|
||||
|
@ -311,18 +319,16 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
|
|||
throw error;
|
||||
});
|
||||
|
||||
const bucket = result.aggregations ?? {};
|
||||
const aggs = result.aggregations ?? {};
|
||||
|
||||
return {
|
||||
id: migrationId,
|
||||
rules: {
|
||||
total: this.getTotalHits(result),
|
||||
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,
|
||||
...this.statusAggCounts(aggs.status as AggregationsStringTermsAggregate),
|
||||
},
|
||||
created_at: (bucket.createdAt as AggregationsMinAggregate)?.value_as_string ?? '',
|
||||
last_updated_at: (bucket.lastUpdatedAt as AggregationsMaxAggregate)?.value_as_string ?? '',
|
||||
created_at: (aggs.createdAt as AggregationsMinAggregate)?.value_as_string ?? '',
|
||||
last_updated_at: (aggs.lastUpdatedAt as AggregationsMaxAggregate)?.value_as_string ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -331,12 +337,9 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
|
|||
const index = await this.getIndexName();
|
||||
const aggregations: { migrationIds: AggregationsAggregationContainer } = {
|
||||
migrationIds: {
|
||||
terms: { field: 'migration_id', order: { createdAt: 'asc' } },
|
||||
terms: { field: 'migration_id', order: { createdAt: 'asc' }, size: 10000 },
|
||||
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 } } },
|
||||
status: { terms: { field: 'status' } },
|
||||
createdAt: { min: { field: '@timestamp' } },
|
||||
lastUpdatedAt: { max: { field: 'updated_at' } },
|
||||
},
|
||||
|
@ -355,16 +358,43 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
|
|||
id: bucket.key,
|
||||
rules: {
|
||||
total: bucket.doc_count,
|
||||
pending: bucket.pending?.doc_count ?? 0,
|
||||
processing: bucket.processing?.doc_count ?? 0,
|
||||
completed: bucket.completed?.doc_count ?? 0,
|
||||
failed: bucket.failed?.doc_count ?? 0,
|
||||
...this.statusAggCounts(bucket.status),
|
||||
},
|
||||
created_at: bucket.createdAt?.value_as_string,
|
||||
last_updated_at: bucket.lastUpdatedAt?.value_as_string,
|
||||
}));
|
||||
}
|
||||
|
||||
private statusAggCounts(
|
||||
statusAgg: AggregationsStringTermsAggregate
|
||||
): Record<SiemMigrationStatus, number> {
|
||||
const buckets = statusAgg.buckets as AggregationsStringTermsBucket[];
|
||||
return {
|
||||
[SiemMigrationStatus.PENDING]:
|
||||
buckets.find(({ key }) => key === SiemMigrationStatus.PENDING)?.doc_count ?? 0,
|
||||
[SiemMigrationStatus.PROCESSING]:
|
||||
buckets.find(({ key }) => key === SiemMigrationStatus.PROCESSING)?.doc_count ?? 0,
|
||||
[SiemMigrationStatus.COMPLETED]:
|
||||
buckets.find(({ key }) => key === SiemMigrationStatus.COMPLETED)?.doc_count ?? 0,
|
||||
[SiemMigrationStatus.FAILED]:
|
||||
buckets.find(({ key }) => key === SiemMigrationStatus.FAILED)?.doc_count ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
private translationResultAggCount(
|
||||
resultAgg: AggregationsStringTermsAggregate
|
||||
): Record<RuleTranslationResult, number> {
|
||||
const buckets = resultAgg.buckets as AggregationsStringTermsBucket[];
|
||||
return {
|
||||
[RuleTranslationResult.FULL]:
|
||||
buckets.find(({ key }) => key === RuleTranslationResult.FULL)?.doc_count ?? 0,
|
||||
[RuleTranslationResult.PARTIAL]:
|
||||
buckets.find(({ key }) => key === RuleTranslationResult.PARTIAL)?.doc_count ?? 0,
|
||||
[RuleTranslationResult.UNTRANSLATABLE]:
|
||||
buckets.find(({ key }) => key === RuleTranslationResult.UNTRANSLATABLE)?.doc_count ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
private getFilterQuery(
|
||||
migrationId: string,
|
||||
{ status, ids, installable, prebuilt, searchTerm }: RuleMigrationFilters = {}
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
*/
|
||||
|
||||
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { SiemMigrationRuleTranslationResult } from '../../../../../common/siem_migrations/constants';
|
||||
import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants';
|
||||
|
||||
export const conditions = {
|
||||
isFullyTranslated(): QueryDslQueryContainer {
|
||||
return { term: { translation_result: SiemMigrationRuleTranslationResult.FULL } };
|
||||
return { term: { translation_result: RuleTranslationResult.FULL } };
|
||||
},
|
||||
isNotInstalled(): QueryDslQueryContainer {
|
||||
return {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { END, START, StateGraph } from '@langchain/langgraph';
|
||||
import { SiemMigrationRuleTranslationResult } from '../../../../../../common/siem_migrations/constants';
|
||||
import { RuleTranslationResult } from '../../../../../../common/siem_migrations/constants';
|
||||
import { getCreateSemanticQueryNode } from './nodes/create_semantic_query';
|
||||
import { getMatchPrebuiltRuleNode } from './nodes/match_prebuilt_rule';
|
||||
import { getProcessQueryNode } from './nodes/process_query';
|
||||
|
@ -62,7 +62,7 @@ const matchedPrebuiltRuleConditional = (state: MigrateRuleState) => {
|
|||
if (state.elastic_rule?.prebuilt_rule_id) {
|
||||
return END;
|
||||
}
|
||||
if (state.translation_result === SiemMigrationRuleTranslationResult.UNTRANSLATABLE) {
|
||||
if (state.translation_result === RuleTranslationResult.UNTRANSLATABLE) {
|
||||
return END;
|
||||
}
|
||||
return 'processQuery';
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
import { JsonOutputParser } from '@langchain/core/output_parsers';
|
||||
import { SiemMigrationRuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants';
|
||||
import { RuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants';
|
||||
import type { RuleMigrationsRetriever } from '../../../retrievers';
|
||||
import type { ChatModel } from '../../../util/actions_client_chat';
|
||||
import type { GraphNode } from '../../types';
|
||||
|
@ -69,7 +69,7 @@ export const getMatchPrebuiltRuleNode = ({
|
|||
id: matchedRule.installedRuleId,
|
||||
prebuilt_rule_id: matchedRule.rule_id,
|
||||
},
|
||||
translation_result: SiemMigrationRuleTranslationResult.FULL,
|
||||
translation_result: RuleTranslationResult.FULL,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ export const getMatchPrebuiltRuleNode = ({
|
|||
logger.debug(
|
||||
`Rule: ${state.original_rule?.title} did not match any prebuilt rule, but contains inputlookup, dropping`
|
||||
);
|
||||
return { translation_result: SiemMigrationRuleTranslationResult.UNTRANSLATABLE };
|
||||
return { translation_result: RuleTranslationResult.UNTRANSLATABLE };
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import type { BaseMessage } from '@langchain/core/messages';
|
||||
import { Annotation, messagesStateReducer } from '@langchain/langgraph';
|
||||
import type { SiemMigrationRuleTranslationResult } from '../../../../../../common/siem_migrations/constants';
|
||||
import type { RuleTranslationResult } from '../../../../../../common/siem_migrations/constants';
|
||||
import type {
|
||||
ElasticRule,
|
||||
OriginalRule,
|
||||
|
@ -31,7 +31,7 @@ export const migrateRuleState = Annotation.Root({
|
|||
reducer: (current, value) => value ?? current,
|
||||
default: () => '',
|
||||
}),
|
||||
translation_result: Annotation<SiemMigrationRuleTranslationResult>(),
|
||||
translation_result: Annotation<RuleTranslationResult>(),
|
||||
comments: Annotation<RuleMigration['comments']>({
|
||||
reducer: (current, value) => (value ? (current ?? []).concat(value) : current),
|
||||
default: () => [],
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { END, START, StateGraph } from '@langchain/langgraph';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import { SiemMigrationRuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants';
|
||||
import { RuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants';
|
||||
import { getEcsMappingNode } from './nodes/ecs_mapping';
|
||||
import { getFilterIndexPatternsNode } from './nodes/filter_index_patterns';
|
||||
import { getFixQueryErrorsNode } from './nodes/fix_query_errors';
|
||||
|
@ -67,7 +67,7 @@ export function getTranslateRuleGraph({
|
|||
const validationRouter = (state: TranslateRuleState) => {
|
||||
if (
|
||||
state.validation_errors.iterations <= MAX_VALIDATION_ITERATIONS &&
|
||||
state.translation_result === SiemMigrationRuleTranslationResult.FULL
|
||||
state.translation_result === RuleTranslationResult.FULL
|
||||
) {
|
||||
if (!isEmpty(state.validation_errors?.esql_errors)) {
|
||||
return 'fixQueryErrors';
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
import type { InferenceClient } from '@kbn/inference-plugin/server';
|
||||
import { SiemMigrationRuleTranslationResult } from '../../../../../../../../../../common/siem_migrations/constants';
|
||||
import { RuleTranslationResult } from '../../../../../../../../../../common/siem_migrations/constants';
|
||||
import { getEsqlKnowledgeBase } from '../../../../../util/esql_knowledge_base_caller';
|
||||
import type { GraphNode } from '../../types';
|
||||
import { SIEM_RULE_MIGRATION_CIM_ECS_MAP } from './cim_ecs_map';
|
||||
|
@ -58,9 +58,9 @@ export const getEcsMappingNode = ({
|
|||
};
|
||||
};
|
||||
|
||||
const getTranslationResult = (esqlQuery: string): SiemMigrationRuleTranslationResult => {
|
||||
const getTranslationResult = (esqlQuery: string): RuleTranslationResult => {
|
||||
if (esqlQuery.match(/\[(macro):[\s\S]*\]/)) {
|
||||
return SiemMigrationRuleTranslationResult.PARTIAL;
|
||||
return RuleTranslationResult.PARTIAL;
|
||||
}
|
||||
return SiemMigrationRuleTranslationResult.FULL;
|
||||
return RuleTranslationResult.FULL;
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
import { SiemMigrationRuleTranslationResult } from '../../../../../../../../../../common/siem_migrations/constants';
|
||||
import { RuleTranslationResult } from '../../../../../../../../../../common/siem_migrations/constants';
|
||||
import type { GraphNode } from '../../types';
|
||||
|
||||
interface GetFilterIndexPatternsNodeParams {
|
||||
|
@ -30,7 +30,7 @@ export const getFilterIndexPatternsNode = ({
|
|||
elastic_rule: {
|
||||
...state.elastic_rule,
|
||||
query: newQuery,
|
||||
translation_result: SiemMigrationRuleTranslationResult.PARTIAL,
|
||||
translation_result: RuleTranslationResult.PARTIAL,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
import type { InferenceClient } from '@kbn/inference-plugin/server';
|
||||
import { SiemMigrationRuleTranslationResult } from '../../../../../../../../../../common/siem_migrations/constants';
|
||||
import { RuleTranslationResult } from '../../../../../../../../../../common/siem_migrations/constants';
|
||||
import { getEsqlKnowledgeBase } from '../../../../../util/esql_knowledge_base_caller';
|
||||
import type { GraphNode } from '../../types';
|
||||
import { ESQL_SYNTAX_TRANSLATION_PROMPT } from './prompts';
|
||||
|
@ -63,9 +63,9 @@ export const getTranslateRuleNode = ({
|
|||
};
|
||||
};
|
||||
|
||||
const getTranslationResult = (esqlQuery: string): SiemMigrationRuleTranslationResult => {
|
||||
const getTranslationResult = (esqlQuery: string): RuleTranslationResult => {
|
||||
if (esqlQuery.match(/\[(macro):[\s\S]*\]/)) {
|
||||
return SiemMigrationRuleTranslationResult.PARTIAL;
|
||||
return RuleTranslationResult.PARTIAL;
|
||||
}
|
||||
return SiemMigrationRuleTranslationResult.FULL;
|
||||
return RuleTranslationResult.FULL;
|
||||
};
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import type { BaseMessage } from '@langchain/core/messages';
|
||||
import { Annotation, messagesStateReducer } from '@langchain/langgraph';
|
||||
import { SiemMigrationRuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants';
|
||||
import { RuleTranslationResult } from '../../../../../../../../common/siem_migrations/constants';
|
||||
import type {
|
||||
ElasticRule,
|
||||
OriginalRule,
|
||||
|
@ -46,9 +46,9 @@ export const translateRuleState = Annotation.Root({
|
|||
reducer: (current, value) => value ?? current,
|
||||
default: () => ({ iterations: 0 } as TranslateRuleValidationErrors),
|
||||
}),
|
||||
translation_result: Annotation<SiemMigrationRuleTranslationResult>({
|
||||
translation_result: Annotation<RuleTranslationResult>({
|
||||
reducer: (current, value) => value ?? current,
|
||||
default: () => SiemMigrationRuleTranslationResult.UNTRANSLATABLE,
|
||||
default: () => RuleTranslationResult.UNTRANSLATABLE,
|
||||
}),
|
||||
comments: Annotation<RuleMigration['comments']>({
|
||||
reducer: (current, value) => (value ? (current ?? []).concat(value) : current),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue