Skip to content

Commit

Permalink
[Rules migration] Implement workflow tour - Rule Translation (#11384) (
Browse files Browse the repository at this point in the history
…#207425)

## Summary

[Internal link](elastic/security-team#10820)
to the feature details

This PR adds the SIEM Migration tour guide on the Translated Rules page.
[Figma
link](https://www.figma.com/design/BD9GZZz6y8pfSbubAt5H2W/%5B8.18%5D-GenAI-Powered-SIEM-Migration%3A-Rule-translation?node-id=2652-255044&t=hpdjyIzzG4XcwLSU-4)

The tour will be on when there are at least one translated rule in the
opened migration (rules table is not empty).


https://github.com/user-attachments/assets/8b29d8d5-43ce-4f4e-9821-595899ba3d25

Delete `securitySolution.siemMigrations.ruleTranslationGuide.v8.18` in
the local storage to reset the tour.

> [!NOTE]  
> This feature needs `siemMigrationsEnabled` experimental flag enabled
to work.
  • Loading branch information
e40pud authored Jan 27, 2025
1 parent 68beb8f commit ef58b46
Show file tree
Hide file tree
Showing 9 changed files with 333 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -270,12 +270,12 @@ const SolutionSideNavItem: React.FC<SolutionSideNavItemProps> = React.memo(
return (
<EuiFlexGroup alignItems="center" gutterSize="none">
<EuiFlexItem>{label}</EuiFlexItem>
<EuiFlexItem grow={0}>
<EuiFlexItem grow={0} id={`solutionSideNavCustomIconItem-${id}`}>
<EuiIcon type={iconType} color="text" />
</EuiFlexItem>
</EuiFlexGroup>
);
}, [label, iconType]);
}, [iconType, label, id]);

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ export const NEW_FEATURES_TOUR_STORAGE_KEYS = {
RULE_MANAGEMENT_PAGE: 'securitySolution.rulesManagementPage.newFeaturesTour.v8.13',
TIMELINES: 'securitySolution.security.timelineFlyoutHeader.saveTimelineTour',
SIEM_MAIN_LANDING_PAGE: 'securitySolution.siemMigrations.setupGuide.v8.18',
SIEM_RULE_TRANSLATION_PAGE: 'securitySolution.siemMigrations.ruleTranslationGuide.v8.18',
};

export const RULE_DETAILS_EXECUTION_LOG_TABLE_SHOW_METRIC_COLUMNS_STORAGE_KEY =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { EuiComboBox, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@el
import * as i18n from './translations';
import type { RuleMigrationStats } from '../../types';

export const SIEM_MIGRATIONS_SELECT_MIGRATION_BUTTON_ID = 'siemMigrationsSelectMigrationButton';

export interface HeaderButtonsProps {
/**
* Available rule migrations stats
Expand Down Expand Up @@ -72,6 +74,7 @@ export const HeaderButtons: React.FC<HeaderButtonsProps> = React.memo(
</EuiTitle>
<EuiSpacer size="xs" />
<EuiComboBox
id={SIEM_MIGRATIONS_SELECT_MIGRATION_BUTTON_ID}
aria-label={i18n.SIEM_MIGRATIONS_OPTION_AREAL_LABEL}
onChange={onChange}
options={migrationOptions}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { useStartMigration } from '../../service/hooks/use_start_migration';
import type { FilterOptions } from '../../types';
import { MigrationRulesFilter } from './filters';
import { convertFilterOptions } from './utils/filters';
import { SiemTranslatedRulesTour } from '../tours/translation_guide';

const DEFAULT_PAGE_SIZE = 10;
const DEFAULT_SORT_FIELD = 'translation_result';
Expand Down Expand Up @@ -299,6 +300,8 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem

return (
<>
{!isStatsLoading && translationStats?.rules.total && <SiemTranslatedRulesTour />}

<EuiSkeletonLoading
isLoading={isStatsLoading}
loadingContent={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,22 @@ import React from 'react';
import { EuiToolTip, EuiIcon } from '@elastic/eui';

interface TableHeaderProps {
id?: string;
title: string;
tooltipContent?: React.ReactNode;
}

export const TableHeader: React.FC<TableHeaderProps> = React.memo(({ title, tooltipContent }) => {
return (
<EuiToolTip content={tooltipContent}>
<>
{title}
&nbsp;
<EuiIcon size="s" type="questionInCircle" color="subdued" className="eui-alignTop" />
</>
</EuiToolTip>
);
});
export const TableHeader: React.FC<TableHeaderProps> = React.memo(
({ id, title, tooltipContent }) => {
return (
<EuiToolTip content={tooltipContent}>
<div id={id}>
{title}
&nbsp;
<EuiIcon size="s" type="questionInCircle" color="subdued" className="eui-alignTop" />
</div>
</EuiToolTip>
);
}
);
TableHeader.displayName = 'TableHeader';
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ import { StatusBadge } from '../status_badge';
import { TableHeader } from './header';
import { convertTranslationResultIntoText } from '../../utils/translation_results';

export const SIEM_MIGRATIONS_STATUS_HEADER_ID = 'siemMigrationsStatusHeader';

export const createStatusColumn = (): TableColumn => {
return {
field: 'translation_result',
name: (
<TableHeader
id={SIEM_MIGRATIONS_STATUS_HEADER_ID}
title={i18n.COLUMN_STATUS}
tooltipContent={
<FormattedMessage
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* 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, useMemo, useState } from 'react';
import { EuiButtonEmpty, EuiTourStep } from '@elastic/eui';
import { noop } from 'lodash';
import { useIsElementMounted } from '../../../../../detection_engine/rule_management_ui/components/rules_table/rules_table/guided_onboarding/use_is_element_mounted';
import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../../../../../../common/constants';
import { useKibana } from '../../../../../common/lib/kibana';
import { TourSteps, tourConfig, tourSteps } from './step_config';
import * as i18n from './translations';

export const SiemTranslatedRulesTour: React.FC = React.memo(() => {
const { siemMigrations, storage } = useKibana().services;

const stepsCount = Object.keys(tourSteps).length;
const selectMigrationStepData = tourSteps[TourSteps.MIGRATION_SELECTION];
const statusHeaderStepData = tourSteps[TourSteps.MIGRATION_RULE_STATUS];
const getStartedStepData = tourSteps[TourSteps.MIGRATION_ONBOARDING_HUB];

const isSelectMigrationAnchorMounted = useIsElementMounted(selectMigrationStepData.anchorId);
const isStatusHeaderAnchorMounted = useIsElementMounted(statusHeaderStepData.anchorId);
const isGetStartedNavigationAnchorMounted = useIsElementMounted(getStartedStepData.anchorId);

const [tourState, setTourState] = useState(() => {
const restoredTourState = storage.get(
NEW_FEATURES_TOUR_STORAGE_KEYS.SIEM_RULE_TRANSLATION_PAGE
);
if (restoredTourState != null) {
return restoredTourState;
}
return tourConfig;
});

const onTourFinished = useCallback(() => {
setTourState({
...tourState,
isTourActive: false,
});
}, [tourState]);

const onTourNext = useCallback(() => {
setTourState({
...tourState,
currentTourStep: tourState.currentTourStep + 1,
});
}, [tourState]);

useEffect(() => {
storage.set(NEW_FEATURES_TOUR_STORAGE_KEYS.SIEM_RULE_TRANSLATION_PAGE, tourState);
}, [tourState, storage]);

const isTourActive = useMemo(() => {
return siemMigrations.rules.isAvailable() && tourState.isTourActive;
}, [siemMigrations.rules, tourState]);

return (
<>
{isSelectMigrationAnchorMounted && (
<EuiTourStep
title={selectMigrationStepData.title}
content={selectMigrationStepData.content}
onFinish={noop}
step={selectMigrationStepData.step}
stepsTotal={stepsCount}
isStepOpen={isTourActive && tourState.currentTourStep === selectMigrationStepData.step}
anchor={`#${selectMigrationStepData.anchorId}`}
anchorPosition={selectMigrationStepData.anchorPosition}
maxWidth={tourState.tourPopoverWidth}
footerAction={
<EuiButtonEmpty size="xs" color="text" flush="right" onClick={onTourNext}>
{i18n.NEXT_TOUR_STEP_BUTTON}
</EuiButtonEmpty>
}
/>
)}
{isStatusHeaderAnchorMounted && (
<EuiTourStep
title={statusHeaderStepData.title}
content={statusHeaderStepData.content}
onFinish={noop}
step={statusHeaderStepData.step}
stepsTotal={stepsCount}
isStepOpen={isTourActive && tourState.currentTourStep === statusHeaderStepData.step}
anchor={`#${statusHeaderStepData.anchorId}`}
anchorPosition={statusHeaderStepData.anchorPosition}
maxWidth={tourState.tourPopoverWidth}
footerAction={
<EuiButtonEmpty size="xs" color="text" flush="right" onClick={onTourNext}>
{i18n.NEXT_TOUR_STEP_BUTTON}
</EuiButtonEmpty>
}
/>
)}
{isGetStartedNavigationAnchorMounted && (
<EuiTourStep
title={getStartedStepData.title}
content={getStartedStepData.content}
onFinish={noop}
step={getStartedStepData.step}
stepsTotal={stepsCount}
isStepOpen={isTourActive && tourState.currentTourStep === getStartedStepData.step}
anchor={`#${getStartedStepData.anchorId}`}
anchorPosition={getStartedStepData.anchorPosition}
maxWidth={tourState.tourPopoverWidth}
footerAction={
<EuiButtonEmpty size="xs" color="text" flush="right" onClick={onTourFinished}>
{i18n.FINISH_TOUR_BUTTON}
</EuiButtonEmpty>
}
/>
)}
</>
);
});
SiemTranslatedRulesTour.displayName = 'SiemTranslatedRulesTour';
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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 { FormattedMessage } from '@kbn/i18n-react';
import type { PopoverAnchorPosition } from '@elastic/eui';
import { DocLink } from '../../../../../common/components/links_to_docs/doc_link';
import { SecurityPageName } from '../../../../../../common/constants';
import { SIEM_MIGRATIONS_SELECT_MIGRATION_BUTTON_ID } from '../../header_buttons';
import { SIEM_MIGRATIONS_STATUS_HEADER_ID } from '../../rules_table_columns';
import * as i18n from './translations';

export const SECURITY_GET_STARTED_BUTTON_ANCHOR = `solutionSideNavCustomIconItem-${SecurityPageName.landing}`;

export enum TourSteps {
MIGRATION_SELECTION,
MIGRATION_RULE_STATUS,
MIGRATION_ONBOARDING_HUB,
}

export const tourSteps: {
[key in TourSteps]: {
step: number;
title: string;
content: React.ReactNode;
anchorId: string;
anchorPosition: PopoverAnchorPosition;
};
} = {
[TourSteps.MIGRATION_SELECTION]: {
step: 1,
title: i18n.MIGRATION_RULES_SELECTOR_TOUR_STEP_TITLE,
content: i18n.MIGRATION_RULES_SELECTOR_TOUR_STEP_CONTENT,
anchorId: SIEM_MIGRATIONS_SELECT_MIGRATION_BUTTON_ID,
anchorPosition: 'downCenter',
},
[TourSteps.MIGRATION_RULE_STATUS]: {
step: 2,
title: i18n.TRANSLATION_STATUS_TOUR_STEP_TITLE,
content: (
<FormattedMessage
id="xpack.securitySolution.siemMigrations.rules.tour.statusStepContent"
defaultMessage="{installed} rules have a check mark. Click {view} to access rule details. {translated} rules are ready to {install}, or for your to {edit}. Rules with errors can be {reprocessed}. Learn more about our AI Translations here.
{lineBreak}{lineBreak}
Learn more about our {link}"
values={{
lineBreak: <br />,
install: <b>{i18n.INSTALL_LABEL}</b>,
installed: <b>{i18n.INSTALLED_LABEL}</b>,
view: <b>{i18n.VIEW_LABEL}</b>,
edit: <b>{i18n.EDIT_LABEL}</b>,
translated: <b>{i18n.TRANSLATED_LABEL}</b>,
reprocessed: <b>{i18n.REPROCESSED_LABEL}</b>,
// TODO: Update doc path once available
link: <DocLink docPath="index.html" linkText={i18n.SIEM_MIGRATIONS_LINK_LABEL} />,
}}
/>
),
anchorId: SIEM_MIGRATIONS_STATUS_HEADER_ID,
anchorPosition: 'rightCenter',
},
[TourSteps.MIGRATION_ONBOARDING_HUB]: {
step: 3,
title: i18n.MIGRATION_GUIDE_TOUR_STEP_TITLE,
content: i18n.MIGRATION_GUIDE_TOUR_STEP_CONTENT,
anchorId: SECURITY_GET_STARTED_BUTTON_ANCHOR,
anchorPosition: 'rightCenter',
},
};

export const tourConfig = {
currentTourStep: tourSteps[TourSteps.MIGRATION_SELECTION].step,
isTourActive: true,
tourPopoverWidth: 360,
};
Loading

0 comments on commit ef58b46

Please sign in to comment.