[9.0] [Siem Migrations] Add Translated Rules Empty Page (#213438) (#214170)

# Backport

This will backport the following commits from `main` to `9.0`:
- [[Siem Migrations] Add Translated Rules Empty Page
(#213438)](https://github.com/elastic/kibana/pull/213438)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Jatin
Kathuria","email":"jatin.kathuria@elastic.co"},"sourceCommit":{"committedDate":"2025-03-12T13:59:12Z","message":"[Siem
Migrations] Add Translated Rules Empty Page (#213438)\n\n##
Summary\n\nThis PR adds empty state for Translated Rules
Page.\n\n\n\nhttps://github.com/user-attachments/assets/b8222151-526c-435e-b9bb-403e1097c056\n\n\n\n\n\n###
Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers
should verify this PR satisfies this list as well.\n\n- [x] Any text
added follows [EUI's
writing\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\nsentence case text and includes
[i18n\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\n-
[x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios","sha":"bbc91a26917e0ad5257bfd879a0f1931d3442f25","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Feature:ExpressionLanguage","release_note:skip","v9.0.0","Team:Threat
Hunting","backport:version","v8.18.0","v9.1.0","v8.19.0"],"title":"[Siem
Migrations] Add Translated Rules Empty
Page","number":213438,"url":"https://github.com/elastic/kibana/pull/213438","mergeCommit":{"message":"[Siem
Migrations] Add Translated Rules Empty Page (#213438)\n\n##
Summary\n\nThis PR adds empty state for Translated Rules
Page.\n\n\n\nhttps://github.com/user-attachments/assets/b8222151-526c-435e-b9bb-403e1097c056\n\n\n\n\n\n###
Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers
should verify this PR satisfies this list as well.\n\n- [x] Any text
added follows [EUI's
writing\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\nsentence case text and includes
[i18n\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\n-
[x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios","sha":"bbc91a26917e0ad5257bfd879a0f1931d3442f25"}},"sourceBranch":"main","suggestedTargetBranches":["9.0","8.18","8.x"],"targetPullRequestStates":[{"branch":"9.0","label":"v9.0.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.18","label":"v8.18.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/213438","number":213438,"mergeCommit":{"message":"[Siem
Migrations] Add Translated Rules Empty Page (#213438)\n\n##
Summary\n\nThis PR adds empty state for Translated Rules
Page.\n\n\n\nhttps://github.com/user-attachments/assets/b8222151-526c-435e-b9bb-403e1097c056\n\n\n\n\n\n###
Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers
should verify this PR satisfies this list as well.\n\n- [x] Any text
added follows [EUI's
writing\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\nsentence case text and includes
[i18n\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\n-
[x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios","sha":"bbc91a26917e0ad5257bfd879a0f1931d3442f25"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Jatin Kathuria <jatin.kathuria@elastic.co>
This commit is contained in:
Kibana Machine 2025-03-13 02:56:20 +11:00 committed by GitHub
parent 873f281cd3
commit 95824add22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 495 additions and 11 deletions

View file

@ -0,0 +1,146 @@
/*
* 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 { v4 as uuidv4 } from 'uuid';
import { SiemMigrationTaskStatus } from '../../../../common/siem_migrations/constants';
import type {
GetRuleMigrationResponse,
GetRuleMigrationTranslationStatsResponse,
} from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen';
import type { RuleMigrationStats } from '../../rules/types';
const getMockMigrationResultRule = ({
migrationId,
translationResult = 'full',
status = 'completed',
}: {
migrationId: string;
status?: GetRuleMigrationResponse['data'][number]['status'];
translationResult?: GetRuleMigrationResponse['data'][number]['translation_result'];
}): GetRuleMigrationResponse['data'][number] => {
const ruleId = uuidv4();
return {
migration_id: migrationId,
original_rule: {
id: ruleId,
vendor: 'splunk',
title: ` 'Rule Title - ${ruleId}'`,
description: `Rule Title Description - ${ruleId}`,
query: 'some query',
query_language: 'spl',
},
'@timestamp': '2025-03-06T15:00:01.036Z',
status,
created_by: 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0',
updated_by: 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0',
updated_at: '2025-03-06T15:01:37.321Z',
comments: [
{
created_at: '2025-03-06T15:01:06.390Z',
message: '## Prebuilt Rule Matching Summary\nNo related prebuilt rule found.',
created_by: 'assistant',
},
],
translation_result: translationResult,
elastic_rule: {
severity: 'low',
risk_score: 21,
query:
'FROM index metadata _id,_version,_index\n| WHERE MATCH(host.hostname, "vendor_sales")',
description: `Converted Splunk Rule to Elastic Rule - ${ruleId}`,
query_language: 'esql',
title: `Converted Splunk Rule - ${ruleId}`,
integration_ids: ['endpoint'],
},
id: ruleId,
};
};
export const mockedMigrationLatestStatsData: RuleMigrationStats[] = [
{
id: '1',
number: 1,
status: SiemMigrationTaskStatus.FINISHED,
rules: {
total: 1,
pending: 0,
processing: 0,
completed: 1,
failed: 0,
},
last_updated_at: '2025-03-06T15:01:37.321Z',
created_at: '2025-03-06T15:01:37.321Z',
},
{
id: '2',
number: 2,
status: SiemMigrationTaskStatus.FINISHED,
rules: {
total: 2,
pending: 0,
processing: 0,
completed: 2,
failed: 0,
},
created_at: '2025-03-06T15:01:37.321Z',
last_updated_at: '2025-03-06T15:01:37.321Z',
},
];
export const mockedMigrationResultsObj: Record<string, GetRuleMigrationResponse> = {
'1': {
total: 2,
data: [
getMockMigrationResultRule({
migrationId: '1',
translationResult: 'full',
status: 'completed',
}),
getMockMigrationResultRule({
migrationId: '1',
translationResult: 'untranslatable',
status: 'failed',
}),
],
},
};
export const mockedMigrationTranslationStats: Record<
string,
GetRuleMigrationTranslationStatsResponse
> = {
'1': {
id: '1',
rules: {
total: 2,
success: {
total: 2,
result: {
full: 1,
partial: 0,
untranslatable: 1,
},
installable: 1,
prebuilt: 0,
},
failed: 0,
},
},
};
const mockRefreshStats = jest.fn();
export const mockedLatestStatsEmpty = {
data: [],
isLoading: false,
refreshStats: mockRefreshStats,
};
export const mockedLatestStats = {
...mockedLatestStatsEmpty,
data: mockedMigrationLatestStatsData,
};

View file

@ -320,7 +320,12 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
<EmptyMigration />
) : (
<>
<EuiFlexGroup gutterSize="m" justifyContent="flexEnd" wrap>
<EuiFlexGroup
data-test-subj="siemMigrationsRulesTable"
gutterSize="m"
justifyContent="flexEnd"
wrap
>
<EuiFlexItem>
<SearchField initialValue={searchTerm} onSearch={handleOnSearch} />
</EuiFlexItem>

View file

@ -20,7 +20,7 @@ interface NameProps {
const Name = ({ rule, openMigrationRuleDetails }: NameProps) => {
if (rule.status === SiemMigrationStatus.FAILED) {
return (
<EuiText color="danger" size="s">
<EuiText data-test-subj="ruleName" color="danger" size="s">
{rule.original_rule.title}
</EuiText>
);

View file

@ -12,6 +12,7 @@ import * as i18n from './translations';
export const UnknownMigration: React.FC = React.memo(() => {
return (
<EuiFlexGroup
data-test-subj="siemMigrationsUnknown"
alignItems="center"
gutterSize="s"
responsive={false}

View file

@ -0,0 +1,57 @@
/*
* 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 { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import { SecurityPageName } from '@kbn/deeplinks-security';
import { css } from '@emotion/react';
import { SecuritySolutionLinkButton } from '../../../common/components/links';
import * as i18n from './translations';
import { OnboardingCardId, OnboardingTopicId } from '../../../onboarding/constants';
export const EmptyMigrationRulesPage = () => {
return (
<KibanaPageTemplate.Section color="plain" paddingSize="none">
<EuiFlexGroup
css={css`
/**
* 240px compensates for the kibana header, action bar and page header.
* It also compensates for the extra margin that header introduces
*/
min-height: calc(100vh - 240px);
`}
justifyContent="center"
alignItems="center"
>
<EuiFlexItem>
<EuiEmptyPrompt
title={
<span data-test-subj="siemMigrationsTranslatedRulesEmptyPageHeader">
{i18n.TRANSLATED_RULES_EMPTY_PAGE_TITLE}
</span>
}
actions={
<SecuritySolutionLinkButton
deepLinkId={SecurityPageName.landing}
path={`${OnboardingTopicId.siemMigrations}#${OnboardingCardId.siemMigrationsRules}`}
>
{i18n.TRANSLATED_RULES_EMPTY_PAGE_CTA}
</SecuritySolutionLinkButton>
}
iconType={'logoSecurity'}
body={
<span data-test-subj="siemMigrationsTranslatedRulesEmptyPageMessage">
{i18n.TRANSLATED_RULES_EMPTY_PAGE_MESSAGE}
</span>
}
/>
</EuiFlexItem>
</EuiFlexGroup>
</KibanaPageTemplate.Section>
);
};

View file

@ -0,0 +1,254 @@
/*
* 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 hash from 'object-hash';
import { createMemoryHistory } from 'history';
import { render, screen, waitFor } from '@testing-library/react';
import { MigrationRulesPage, type MigrationRulesPageProps } from '.';
import * as useLatestStatsModule from '../service/hooks/use_latest_stats';
import * as useNavigationModule from '@kbn/security-solution-navigation/src/navigation';
import * as useGetIntegrationsModule from '../service/hooks/use_get_integrations';
import * as useGetMigrationRulesModule from '../logic/use_get_migration_rules';
import * as useGetMigrationTranslationStatsModule from '../logic/use_get_migration_translation_stats';
import * as useMissingPrivilegesModule from '../../../detections/components/callouts/missing_privileges_callout/use_missing_privileges';
import * as useGetMigrationMissingPrivilegesModule from '../logic/use_get_migration_privileges';
import * as useCallOutStorageModule from '../../../common/components/callouts/use_callout_storage';
import { TestProviders } from '../../../common/mock';
import {
mockedLatestStats,
mockedLatestStatsEmpty,
mockedMigrationResultsObj,
mockedMigrationTranslationStats,
} from '../../common/mocks/migration_result.data';
import * as useGetMissingResourcesModule from '../service/hooks/use_get_missing_resources';
jest.mock('../../../common/components/page_wrapper', () => {
return {
SecuritySolutionPageWrapper: jest.fn(({ children }) => {
return <div data-test-subj="SecuritySolutionPageWrapper">{children}</div>;
}),
};
});
const useLatestStatsSpy = jest.spyOn(useLatestStatsModule, 'useLatestStats');
const useNavigationSpy = jest.spyOn(useNavigationModule, 'useNavigation');
const useGetIntegrationsSpy = jest.spyOn(useGetIntegrationsModule, 'useGetIntegrations');
const useInvalidateGetMigrationRulesSpy = jest.spyOn(
useGetMigrationRulesModule,
'useInvalidateGetMigrationRules'
);
const useInvalidateGetMigrationTranslationStatsSpy = jest.spyOn(
useGetMigrationTranslationStatsModule,
'useInvalidateGetMigrationTranslationStats'
);
const useGetMigrationRulesSpy = jest.spyOn(useGetMigrationRulesModule, 'useGetMigrationRules');
// missing detection privileges
const useMissingPrivilegesSpy = jest.spyOn(useMissingPrivilegesModule, 'useMissingPrivileges');
// missing migration privileges
const useGetMigrationMissingPrivilegesSpy = jest.spyOn(
useGetMigrationMissingPrivilegesModule,
'useGetMigrationMissingPrivileges'
);
const useCalloutStorageSpy = jest.spyOn(useCallOutStorageModule, 'useCallOutStorage');
const useGetMissingResourcesSpy = jest.spyOn(
useGetMissingResourcesModule,
'useGetMissingResources'
);
const useGetMigrationTranslationStatsSpy = jest.spyOn(
useGetMigrationTranslationStatsModule,
'useGetMigrationTranslationStats'
);
const defaultProps: MigrationRulesPageProps = {
history: createMemoryHistory(),
location: createMemoryHistory().location,
match: {
isExact: true,
path: '',
url: '',
params: {
migrationId: undefined,
},
},
};
const mockNavigateTo = jest.fn();
const mockGetIntegrations = jest.fn();
const mockMissingDetectionsPrivileges: useMissingPrivilegesModule.MissingPrivileges = {
featurePrivileges: [],
indexPrivileges: [],
};
const mockGetMigrationMissingPrivileges = {
data: [],
};
const mockVisibleCallStorageResult = {
isVisible: () => true,
dismiss: jest.fn(),
getVisibleMessageIds: jest.fn(() => []),
};
const mockHiddenCallStorageResult = {
isVisible: () => false,
dismiss: jest.fn(),
getVisibleMessageIds: jest.fn(() => []),
};
function renderTestComponent(args?: { migrationId?: string; wrapper?: React.ComponentType }) {
const finalProps = {
...defaultProps,
match: {
...defaultProps.match,
params: {
migrationId: args?.migrationId ?? defaultProps.match.params.migrationId,
},
},
};
return render(<MigrationRulesPage {...finalProps} />, {
wrapper: args?.wrapper,
});
}
const mockUseMigrationRuleTransationStats: typeof useGetMigrationTranslationStatsModule.useGetMigrationTranslationStats =
jest.fn((migrationId: string) => {
const result = structuredClone(mockedMigrationTranslationStats)[migrationId];
return {
data: result,
isLoading: false,
} as unknown as ReturnType<
typeof useGetMigrationTranslationStatsModule.useGetMigrationTranslationStats
>;
});
const mockUseGetMigrationRules: typeof useGetMigrationRulesModule.useGetMigrationRules = jest.fn(
({ migrationId }) => {
const { data, total } = mockedMigrationResultsObj[migrationId];
return {
data: {
ruleMigrations: data,
total,
},
isLoading: false,
} as unknown as ReturnType<typeof useGetMigrationRulesModule.useGetMigrationRules>;
}
);
describe('Migrations: Translated Rules Page', () => {
beforeEach(() => {
useLatestStatsSpy.mockReturnValue(mockedLatestStatsEmpty);
useNavigationSpy.mockReturnValue({ navigateTo: mockNavigateTo, getAppUrl: jest.fn() });
useGetIntegrationsSpy.mockReturnValue({
getIntegrations: mockGetIntegrations,
isLoading: false,
error: null,
});
useInvalidateGetMigrationTranslationStatsSpy.mockReturnValue(jest.fn());
useInvalidateGetMigrationRulesSpy.mockReturnValue(jest.fn());
useMissingPrivilegesSpy.mockReturnValue(mockMissingDetectionsPrivileges);
useGetMigrationMissingPrivilegesSpy.mockReturnValue(
mockGetMigrationMissingPrivileges as unknown as ReturnType<
typeof useGetMigrationMissingPrivilegesModule.useGetMigrationMissingPrivileges
>
);
useCalloutStorageSpy.mockReturnValue(mockHiddenCallStorageResult);
useGetMigrationRulesSpy.mockImplementation(mockUseGetMigrationRules);
useGetMissingResourcesSpy.mockReturnValue({
getMissingResources: jest.fn(() => []),
isLoading: false,
} as unknown as ReturnType<typeof useGetMissingResourcesModule.useGetMissingResources>);
useGetMigrationTranslationStatsSpy.mockImplementation(mockUseMigrationRuleTransationStats);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('With No MigrationId', () => {
test('should render empty page when no translated rules are available', () => {
renderTestComponent({
wrapper: TestProviders,
});
expect(screen.getByTestId('siemMigrationsTranslatedRulesEmptyPageHeader')).toBeVisible();
expect(screen.queryByTestId('siemMigrationsRulesTable')).toBeNull();
});
test('should render skeleton when loading', () => {
useLatestStatsSpy.mockReturnValue({ ...mockedLatestStatsEmpty, isLoading: true });
renderTestComponent();
expect(screen.getByTestId('migrationRulesPageLoading')).toBeVisible();
});
test('should redirect to the most recent migration when no migrationId is provided and migrations exists', () => {
useLatestStatsSpy.mockReturnValue(mockedLatestStats);
renderTestComponent();
expect(mockNavigateTo).toHaveBeenCalledWith({
deepLinkId: 'siem_migrations-rules',
path: '2',
});
});
test('should render missing privileges panel', () => {
const mockMissingPrivileges: useMissingPrivilegesModule.MissingPrivileges = {
...mockMissingDetectionsPrivileges,
indexPrivileges: [['index', ['privilege1', 'privilege2']]],
};
useLatestStatsSpy.mockReturnValue(mockedLatestStats);
useMissingPrivilegesSpy.mockReturnValue(mockMissingPrivileges);
useCalloutStorageSpy.mockReturnValue(mockVisibleCallStorageResult);
const missingPrivilegesHash = hash(mockMissingPrivileges);
renderTestComponent({
wrapper: TestProviders,
});
expect(useCalloutStorageSpy).toHaveBeenCalled();
expect(
screen.getByTestId(`callout-missing-siem-migrations-privileges-${missingPrivilegesHash}`)
).toBeVisible();
expect(
screen.getByTestId(`callout-missing-siem-migrations-privileges-${missingPrivilegesHash}`)
).toHaveTextContent(/Insufficient privileges/);
});
test('should render unknown migration state if no migrationId is provided', () => {
useLatestStatsSpy.mockReturnValue(mockedLatestStats);
renderTestComponent();
expect(screen.getByTestId('siemMigrationsUnknown')).toBeVisible();
});
});
describe('With MigrationId', () => {
test('should render migration table successfully if migrationId is provided', async () => {
useLatestStatsSpy.mockReturnValue(mockedLatestStats);
renderTestComponent({
migrationId: '1',
wrapper: TestProviders,
});
await waitFor(() => {
expect(screen.getByTestId('siemMigrationsRulesTable')).toBeVisible();
});
expect(screen.getAllByTestId(/ruleName/)).toHaveLength(2);
expect(screen.getAllByTestId(/ruleName/)[0]).toHaveTextContent(/Converted Splunk Rule -/);
// only successful is selectable
expect(screen.getAllByTitle(/Select row 1/)).toHaveLength(1);
expect(screen.getByTitle(/Not fully translated migration rule/)).toBeDisabled();
});
});
});

View file

@ -30,8 +30,9 @@ import { useInvalidateGetMigrationTranslationStats } from '../logic/use_get_migr
import { useGetIntegrations } from '../service/hooks/use_get_integrations';
import { PageTitle } from './page_title';
import { RuleMigrationsUploadMissingPanel } from '../components/migration_status_panels/upload_missing_panel';
import { EmptyMigrationRulesPage } from './empty';
type MigrationRulesPageProps = RouteComponentProps<{ migrationId?: string }>;
export type MigrationRulesPageProps = RouteComponentProps<{ migrationId?: string }>;
export const MigrationRulesPage: React.FC<MigrationRulesPageProps> = React.memo(
({
@ -54,13 +55,7 @@ export const MigrationRulesPage: React.FC<MigrationRulesPageProps> = React.memo(
}, [getIntegrations]);
useEffect(() => {
if (isLoading) {
return;
}
// Navigate to landing page if there are no migrations
if (!ruleMigrationsStats.length) {
navigateTo({ deepLinkId: SecurityPageName.landing, path: 'siem_migrations' });
if (isLoading || ruleMigrationsStats.length === 0) {
return;
}
@ -96,6 +91,9 @@ export const MigrationRulesPage: React.FC<MigrationRulesPageProps> = React.memo(
const pageTitle = useMemo(() => <PageTitle />, []);
const content = useMemo(() => {
if (ruleMigrationsStats.length === 0 && !migrationId) {
return <EmptyMigrationRulesPage />;
}
const migrationStats = ruleMigrationsStats.find((stats) => stats.id === migrationId);
if (!migrationId || !migrationStats) {
return <UnknownMigration />;
@ -141,6 +139,7 @@ export const MigrationRulesPage: React.FC<MigrationRulesPageProps> = React.memo(
<NeedAdminForUpdateRulesCallOut />
<MissingPrivilegesCallOut />
<EuiSkeletonLoading
data-test-subj="migrationRulesPageLoading"
isLoading={isLoading}
loadingContent={
<>

View file

@ -9,7 +9,7 @@ import { EuiBetaBadge, EuiFlexGroup, EuiFlexItem, EuiTitle, useEuiTheme } from '
import { css } from '@emotion/react';
import React from 'react';
import * as i18n from './translations';
import * as i18n from '../translations';
export const PageTitle: React.FC = React.memo(() => {
const { euiTheme } = useEuiTheme();

View file

@ -25,3 +25,25 @@ export const BETA_TOOLTIP = i18n.translate(
'This functionality is in technical preview and is subject to change. Please use SIEM Migrations with caution in production environments.',
}
);
export const TRANSLATED_RULES_EMPTY_PAGE_MESSAGE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.emptyPageMessage',
{
defaultMessage:
'Translate your existing Splunk Rules with Elastic Automatic Migration. Got to SIEM rule migration for step-by-step guidance.',
}
);
export const TRANSLATED_RULES_EMPTY_PAGE_TITLE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.emptyPageTitle',
{
defaultMessage: 'No migrations to View',
}
);
export const TRANSLATED_RULES_EMPTY_PAGE_CTA = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.emptyPageCta',
{
defaultMessage: 'Start SIEM rule Migration',
}
);