[Rules migration] UI updates (#207789)

## Summary

[Internal link](https://github.com/elastic/security-team/issues/10820)
to the feature details

This PR includes next improvements and fixes

### Improvements

1. Updated copies
2. Added `Last updated` label in Translation Rule details flyout

<img width="1274" alt="Screenshot 2025-01-22 at 14 15 24 copy"
src="https://github.com/user-attachments/assets/6974698f-3a26-48f1-96fc-d5458aa81a0a"
/>

3. Added rule installation information callout in Translation Rule
details flyout

<img width="1274" alt="Screenshot 2025-01-22 at 14 15 24"
src="https://github.com/user-attachments/assets/c350f0c2-8acf-4821-99a8-f6b510ae8dc5"
/>

4. Added horizontal line underneath the Translation Rules header

> [!NOTE]  
> This feature needs `siemMigrationsEnabled` experimental flag enabled
to work.
This commit is contained in:
Ievgen Sorokopud 2025-01-23 18:45:01 +01:00 committed by GitHub
parent 0a577beec5
commit 835141857a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 158 additions and 27 deletions

View file

@ -19,7 +19,8 @@ export const siemMigrationsLinks: LinkItem = {
id: SecurityPageName.siemMigrationsRules,
title: SIEM_MIGRATIONS_RULES,
description: i18n.translate('xpack.securitySolution.appLinks.siemMigrationsRulesDescription', {
defaultMessage: 'SIEM Rule Migrations.',
defaultMessage:
'Our generative AI powered SIEM migration tool automates some of the most time consuming migrations tasks and processed.',
}),
landingIcon: SiemMigrationsIcon,
path: SIEM_MIGRATIONS_RULES_PATH,

View file

@ -48,6 +48,7 @@ import {
isMigrationCustomRule,
} from '../../../../../common/siem_migrations/rules/utils';
import { useUpdateMigrationRules } from '../../logic/use_update_migration_rules';
import { UpdatedByLabel } from './updated_by';
/*
* Fixes tabs to the top and allows the content to scroll.
@ -244,7 +245,8 @@ export const MigrationRuleDetailsFlyout: React.FC<MigrationRuleDetailsFlyoutProp
i18n.UNKNOWN_MIGRATION_RULE_TITLE}
</h2>
</EuiTitle>
<EuiSpacer size="l" />
<EuiSpacer size="s" />
<UpdatedByLabel ruleMigration={ruleMigration} />
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiSkeletonLoading

View file

@ -10,29 +10,37 @@ import React from 'react';
import type { IconType } from '@elastic/eui';
import { EuiCallOut } from '@elastic/eui';
import {
RuleMigrationTranslationResultEnum,
type RuleMigration,
type RuleMigrationTranslationResult,
} from '../../../../../../../common/siem_migrations/model/rule_migration.gen';
import * as i18n from './translations';
type RuleMigrationTranslationCallOutMode = RuleMigrationTranslationResult | 'mapped';
const getCallOutInfo = (
translationResult: RuleMigrationTranslationResult
mode: RuleMigrationTranslationCallOutMode
): { title: string; message?: string; icon: IconType; color: 'success' | 'warning' | 'danger' } => {
switch (translationResult) {
case RuleMigrationTranslationResultEnum.full:
switch (mode) {
case 'mapped':
return {
title: i18n.CALLOUT_MAPPED_TRANSLATED_RULE_TITLE,
icon: 'checkInCircleFilled',
color: 'success',
};
case 'full':
return {
title: i18n.CALLOUT_TRANSLATED_RULE_TITLE,
icon: 'checkInCircleFilled',
color: 'success',
};
case RuleMigrationTranslationResultEnum.partial:
case 'partial':
return {
title: i18n.CALLOUT_PARTIALLY_TRANSLATED_RULE_TITLE,
message: i18n.CALLOUT_PARTIALLY_TRANSLATED_RULE_DESCRIPTION,
icon: 'warningFilled',
color: 'warning',
};
case RuleMigrationTranslationResultEnum.untranslatable:
case 'untranslatable':
return {
title: i18n.CALLOUT_NOT_TRANSLATED_RULE_TITLE,
message: i18n.CALLOUT_NOT_TRANSLATED_RULE_DESCRIPTION,
@ -43,23 +51,29 @@ const getCallOutInfo = (
};
export interface TranslationCallOutProps {
translationResult: RuleMigrationTranslationResult;
ruleMigration: RuleMigration;
}
export const TranslationCallOut: FC<TranslationCallOutProps> = React.memo(
({ translationResult }) => {
const { title, message, icon, color } = getCallOutInfo(translationResult);
return (
<EuiCallOut
color={color}
title={title}
iconType={icon}
data-test-subj={`ruleMigrationCallOut-${translationResult}`}
>
{message}
</EuiCallOut>
);
export const TranslationCallOut: FC<TranslationCallOutProps> = React.memo(({ ruleMigration }) => {
if (!ruleMigration.translation_result) {
return null;
}
);
const mode = ruleMigration.elastic_rule?.prebuilt_rule_id
? 'mapped'
: ruleMigration.translation_result;
const { title, message, icon, color } = getCallOutInfo(mode);
return (
<EuiCallOut
color={color}
title={title}
iconType={icon}
size={'s'}
data-test-subj={`ruleMigrationCallOut-${mode}`}
>
{message}
</EuiCallOut>
);
});
TranslationCallOut.displayName = 'TranslationCallOut';

View file

@ -9,6 +9,7 @@ import React, { useMemo } from 'react';
import {
EuiAccordion,
EuiBadge,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
@ -18,6 +19,7 @@ import {
} from '@elastic/eui';
import { css } from '@emotion/css';
import { FormattedMessage } from '@kbn/i18n-react';
import { RuleTranslationResult } from '../../../../../../../common/siem_migrations/constants';
import type { RuleResponse } from '../../../../../../../common/api/detection_engine';
import type { RuleMigration } from '../../../../../../../common/siem_migrations/model/rule_migration.gen';
import { TranslationTabHeader } from './header';
@ -78,7 +80,7 @@ export const TranslationTab: React.FC<TranslationTabProps> = React.memo(
<EuiSpacer size="m" />
{ruleMigration.translation_result && !isInstalled && (
<>
<TranslationCallOut translationResult={ruleMigration.translation_result} />
<TranslationCallOut ruleMigration={ruleMigration} />
<EuiSpacer size="m" />
</>
)}
@ -131,6 +133,19 @@ export const TranslationTab: React.FC<TranslationTabProps> = React.memo(
</EuiSplitPanel.Outer>
</EuiFlexItem>
</EuiAccordion>
{ruleMigration.translation_result === RuleTranslationResult.FULL && (
<>
<EuiSpacer size="m" />
<EuiCallOut
color={'primary'}
title={i18n.CALLOUT_TRANSLATED_RULE_INFO_TITLE}
iconType={'iInCircle'}
size={'s'}
>
{i18n.CALLOUT_TRANSLATED_RULE_INFO_DESCRIPTION}
</EuiCallOut>
</>
)}
</>
);
}

View file

@ -85,6 +85,14 @@ export const CALLOUT_TRANSLATED_RULE_TITLE = i18n.translate(
}
);
export const CALLOUT_MAPPED_TRANSLATED_RULE_TITLE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.mappedTranslatedRuleCalloutTitle',
{
defaultMessage:
'This rule was mapped to an Elastic authored rule. Click Install & enable rule to complete migration. You can fine-tune it later.',
}
);
export const CALLOUT_PARTIALLY_TRANSLATED_RULE_TITLE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.partiallyTranslatedRuleCalloutTitle',
{
@ -114,3 +122,18 @@ export const CALLOUT_NOT_TRANSLATED_RULE_DESCRIPTION = i18n.translate(
'This might be caused by feature differences between SIEM products. If possible, update the rule manually.',
}
);
export const CALLOUT_TRANSLATED_RULE_INFO_TITLE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.partiallyTranslatedRuleCalloutTitle',
{
defaultMessage: 'Translation successful. Install the rule to customize it.',
}
);
export const CALLOUT_TRANSLATED_RULE_INFO_DESCRIPTION = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.translationDetails.translationTab.partiallyTranslatedRuleCalloutDescription',
{
defaultMessage:
'After you install the rule, you can modify or update it with full access to all features.',
}
);

View file

@ -41,3 +41,10 @@ export const CLOSE_BUTTON_LABEL = i18n.translate(
defaultMessage: 'Close',
}
);
export const LAST_UPDATED_LABEL = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.translationDetails.lastUpdatedLabel',
{
defaultMessage: 'Last updated',
}
);

View file

@ -0,0 +1,54 @@
/*
* 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 { FormattedMessage } from '@kbn/i18n-react';
import { EuiText } from '@elastic/eui';
import { FormattedDate } from '../../../../../common/components/formatted_date';
import { useBulkGetUserProfiles } from '../../../../../common/components/user_profiles/use_bulk_get_user_profiles';
import type { RuleMigration } from '../../../../../../common/siem_migrations/model/rule_migration.gen';
import * as i18n from './translations';
interface UpdatedByLabelProps {
ruleMigration: RuleMigration;
}
export const UpdatedByLabel: React.FC<UpdatedByLabelProps> = React.memo(
({ ruleMigration }: UpdatedByLabelProps) => {
const userProfileId = useMemo(
() => new Set([ruleMigration.updated_by ?? ruleMigration.created_by]),
[ruleMigration.created_by, ruleMigration.updated_by]
);
const { isLoading: isLoadingUserProfiles, data: userProfiles } = useBulkGetUserProfiles({
uids: userProfileId,
});
if (isLoadingUserProfiles || !userProfiles?.length) {
return null;
}
const userProfile = userProfiles[0];
const updatedBy = userProfile.user.full_name ?? userProfile.user.username;
const updatedAt = ruleMigration.updated_at ?? ruleMigration['@timestamp'];
return (
<EuiText size="xs">
<FormattedMessage
id="xpack.securitySolution.siemMigrations.rules.translationDetails.updatedByLabel"
defaultMessage="{updated}: {by} on {date}"
values={{
updated: <b>{i18n.LAST_UPDATED_LABEL}</b>,
by: updatedBy,
date: <FormattedDate value={updatedAt} fieldName="updated_at" />,
}}
/>
</EuiText>
);
}
);
UpdatedByLabel.displayName = 'UpdatedByLabel';

View file

@ -0,0 +1,15 @@
/*
* 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 LAST_UPDATED_LABEL = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.translationDetails.lastUpdated.label',
{
defaultMessage: 'Last updated',
}
);

View file

@ -135,7 +135,7 @@ export const MigrationRulesPage: React.FC<MigrationRulesPageProps> = React.memo(
<MissingPrivilegesCallOut />
<SecuritySolutionPageWrapper>
<HeaderPage title={pageTitle}>
<HeaderPage title={pageTitle} border>
<HeaderButtons
ruleMigrationsStats={ruleMigrationsStats}
selectedMigrationId={migrationId}

View file

@ -30,7 +30,7 @@ export const PageTitle: React.FC = React.memo(() => {
<EuiFlexItem
css={css`
margin: ${euiTheme.size.m} 0 0 ${euiTheme.size.m};
margin: ${euiTheme.size.s} 0 0 ${euiTheme.size.m};
`}
grow={false}
>