Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Rules migration] Implement workflow tour - Rule Translation (#11384) #207425

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,182 @@
/*
* 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 type { PopoverAnchorPosition } from '@elastic/eui';
import { EuiButtonEmpty, EuiTourStep } from '@elastic/eui';
import { noop } from 'lodash';
import { FormattedMessage } from '@kbn/i18n-react';
import { DocLink } from '../../../../../common/components/links_to_docs/doc_link';
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,
SecurityPageName,
} from '../../../../../../common/constants';
import { useKibana } from '../../../../../common/lib/kibana';
import { SIEM_MIGRATIONS_STATUS_HEADER_ID } from '../../rules_table_columns';
import { SIEM_MIGRATIONS_SELECT_MIGRATION_BUTTON_ID } from '../../header_buttons';
import * as i18n from './translations';

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

const tourConfig = {
currentTourStep: 1,
isTourActive: true,
tourPopoverWidth: 360,
};

const tourSteps: Array<{
step: number;
title: string;
content: React.ReactNode;
anchorPosition: PopoverAnchorPosition;
}> = [
{
step: 1,
title: i18n.MIGRATION_RULES_SELECTOR_TOUR_STEP_TITLE,
content: i18n.MIGRATION_RULES_SELECTOR_TOUR_STEP_CONTENT,
anchorPosition: 'downCenter',
},
Copy link
Member

@KDKHD KDKHD Jan 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tiny nit/question: How come you don't store the anchors in here too

Suggested change
{
step: 1,
title: i18n.MIGRATION_RULES_SELECTOR_TOUR_STEP_TITLE,
content: i18n.MIGRATION_RULES_SELECTOR_TOUR_STEP_CONTENT,
anchorPosition: 'downCenter',
},
{
step: 1,
title: i18n.MIGRATION_RULES_SELECTOR_TOUR_STEP_TITLE,
content: i18n.MIGRATION_RULES_SELECTOR_TOUR_STEP_CONTENT,
anchorPosition: 'downCenter',
anchor: `#${SIEM_MIGRATIONS_SELECT_MIGRATION_BUTTON_ID}`
},

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! Addressed in 37d5846

{
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} />,
}}
/>
),
anchorPosition: 'rightCenter',
},
{
step: 3,
title: i18n.MIGRATION_GUIDE_TOUR_STEP_TITLE,
content: i18n.MIGRATION_GUIDE_TOUR_STEP_CONTENT,
anchorPosition: 'rightCenter',
},
];

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

const isSelectMigrationAnchorMounted = useIsElementMounted(
SIEM_MIGRATIONS_SELECT_MIGRATION_BUTTON_ID
);
const isStatusHeaderAnchorMounted = useIsElementMounted(SIEM_MIGRATIONS_STATUS_HEADER_ID);
const isGetStartedNavigationAnchorMounted = useIsElementMounted(
SECURITY_GET_STARTED_BUTTON_ANCHOR
);

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]);

const selectMigrationStepData = tourSteps[0];
const statusHeaderStepData = tourSteps[1];
const getStartedStepData = tourSteps[2];

return (
<>
{isSelectMigrationAnchorMounted && (
<EuiTourStep
title={selectMigrationStepData.title}
content={selectMigrationStepData.content}
onFinish={noop}
step={1}
stepsTotal={tourSteps.length}
isStepOpen={isTourActive && tourState.currentTourStep === 1}
anchor={`#${SIEM_MIGRATIONS_SELECT_MIGRATION_BUTTON_ID}`}
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={2}
stepsTotal={tourSteps.length}
isStepOpen={isTourActive && tourState.currentTourStep === 2}
anchor={`#${SIEM_MIGRATIONS_STATUS_HEADER_ID}`}
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={3}
stepsTotal={tourSteps.length}
isStepOpen={isTourActive && tourState.currentTourStep === 3}
anchor={`#${SECURITY_GET_STARTED_BUTTON_ANCHOR}`}
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,108 @@
/*
* 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 MIGRATION_RULES_SELECTOR_TOUR_STEP_TITLE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.title',
{
defaultMessage: 'Translated rules in one place',
}
);

export const MIGRATION_RULES_SELECTOR_TOUR_STEP_CONTENT = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.content',
{
defaultMessage:
'Each migration’s translated rules appear on its SIEM rule translations page. Switch between your migrations using this dropdown. Start a new migration by clicking “Upload more rules for translation”.',
}
);

export const TRANSLATION_STATUS_TOUR_STEP_TITLE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.statusStepTitle',
{
defaultMessage: 'Translation status',
}
);

export const MIGRATION_GUIDE_TOUR_STEP_TITLE = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.migrationGuideStepTitle',
{
defaultMessage: 'SIEM Rule Migration guide',
}
);

export const MIGRATION_GUIDE_TOUR_STEP_CONTENT = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.migrationGuideStepContent',
{
defaultMessage:
'Your guide and migrated rules can always be found in the Onboarding Hub. Use it to review previous rule migrations or start a new one.',
}
);

export const NEXT_TOUR_STEP_BUTTON = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.nextStepButton',
{
defaultMessage: 'Next',
}
);

export const FINISH_TOUR_BUTTON = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.finishButton',
{
defaultMessage: 'OK',
}
);

export const INSTALL_LABEL = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.installLabel',
{
defaultMessage: 'Install',
}
);

export const INSTALLED_LABEL = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.installedLabel',
{
defaultMessage: 'Installed',
}
);

export const VIEW_LABEL = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.viewLabel',
{
defaultMessage: 'View',
}
);

export const EDIT_LABEL = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.editLabel',
{
defaultMessage: 'Edit',
}
);

export const TRANSLATED_LABEL = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.translatedLabel',
{
defaultMessage: 'Translated',
}
);

export const REPROCESSED_LABEL = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.reprocessedLabel',
{
defaultMessage: 'Reprocessed',
}
);

export const SIEM_MIGRATIONS_LINK_LABEL = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.tour.translationRuleGuide.siemMigrationsLinkLabel',
{
defaultMessage: 'AI Translations here',
}
);