Skip to content

Commit

Permalink
[Rules migration] Add possibility to navigate to a specific migration (
Browse files Browse the repository at this point in the history
…elastic#11264) (elastic#201597)

## Summary

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

With these changes we:
* allow user to navigate to a specific migration by its id
* handle different possible states on migrations rules page:
* `no migrations`: if there are no existing migrations we will redirect
user to the landing page
* `unknown selected migration`: if unknown migration id is specified in
the URL, then "Unknown Migration" page will be shown
* `no selected migration`: if user lands on the root "SIEM migrations
rules" page, then most recent migration will be shown
  * `show existing migration`: selected migration will be shown

### Screenshots

**Unknown migration**

<img width="1312" alt="Screenshot 2024-11-25 at 14 46 56"
src="https://github.com/user-attachments/assets/45f51489-e4f8-496f-86e6-d19130bd6769">

**Show existing migration**

<img width="1312" alt="Screenshot 2024-11-25 at 15 03 53"
src="https://github.com/user-attachments/assets/ca866432-5a61-44c7-8bec-7aa95ba73156">
  • Loading branch information
e40pud authored Nov 26, 2024
1 parent f0c8fde commit 17410c3
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 156 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import React from 'react';
import { Routes, Route } from '@kbn/shared-ux-router';

import type { SecuritySubPluginRoutes } from '../app/types';
import { SIEM_MIGRATIONS_RULES_PATH, SecurityPageName } from '../../common/constants';
Expand All @@ -17,7 +18,9 @@ export const RulesRoutes = () => {
return (
<PluginTemplateWrapper>
<SecurityRoutePageWrapper pageName={SecurityPageName.siemMigrationsRules}>
<RulesPage />
<Routes>
<Route path={`${SIEM_MIGRATIONS_RULES_PATH}/:migrationId?`} component={RulesPage} />
</Routes>
</SecurityRoutePageWrapper>
</PluginTemplateWrapper>
);
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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 React from 'react';
import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import * as i18n from './translations';

const UnknownMigrationComponent = () => {
return (
<EuiFlexGroup
alignItems="center"
gutterSize="s"
responsive={false}
direction="column"
wrap={true}
>
<EuiFlexItem grow={false}>
<EuiEmptyPrompt
title={<h2>{i18n.UNKNOWN_MIGRATION}</h2>}
titleSize="s"
body={i18n.UNKNOWN_MIGRATION_BODY}
data-test-subj="noMigrationsAvailable"
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};

export const UnknownMigration = React.memo(UnknownMigrationComponent);
UnknownMigration.displayName = 'UnknownMigration';
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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 UNKNOWN_MIGRATION = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.unknownMigrationTitle',
{
defaultMessage: 'Unknown migration',
}
);

export const UNKNOWN_MIGRATION_BODY = i18n.translate(
'xpack.securitySolution.siemMigrations.rules.unknownMigrationBodyTitle',
{
defaultMessage:
'Selected migration does not exist. Please select one of the available migraitons',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -5,93 +5,113 @@
* 2.0.
*/

import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo } from 'react';

import { EuiSkeletonLoading, EuiSkeletonText, EuiSkeletonTitle } from '@elastic/eui';
import type { RouteComponentProps } from 'react-router-dom';
import { useNavigation } from '../../../common/lib/kibana';
import type { RuleMigration } from '../../../../common/siem_migrations/model/rule_migration.gen';
import { HeaderPage } from '../../../common/components/header_page';
import { SecuritySolutionPageWrapper } from '../../../common/components/page_wrapper';
import { SecurityPageName } from '../../../app/types';

import * as i18n from './translations';
import { RulesTable } from '../components/rules_table';
import { NeedAdminForUpdateRulesCallOut } from '../../../detections/components/callouts/need_admin_for_update_callout';
import { MissingPrivilegesCallOut } from '../../../detections/components/callouts/missing_privileges_callout';
import { HeaderButtons } from '../components/header_buttons';
import { useRulePreviewFlyout } from '../hooks/use_rule_preview_flyout';
import { NoMigrations } from '../components/no_migrations';
import { UnknownMigration } from '../components/unknown_migration';
import { useLatestStats } from '../hooks/use_latest_stats';

export const RulesPage = React.memo(() => {
const { data: ruleMigrationsStatsAll, isLoading: isLoadingMigrationsStats } = useLatestStats();

const migrationsIds = useMemo(() => {
if (isLoadingMigrationsStats || !ruleMigrationsStatsAll?.length) {
return [];
}
return ruleMigrationsStatsAll
.filter((migration) => migration.status === 'finished')
.map((migration) => migration.id);
}, [isLoadingMigrationsStats, ruleMigrationsStatsAll]);

const [selectedMigrationId, setSelectedMigrationId] = useState<string | undefined>();
const onMigrationIdChange = (selectedId?: string) => {
setSelectedMigrationId(selectedId);
};

useEffect(() => {
if (!migrationsIds.length) {
return;
}
const index = migrationsIds.findIndex((id) => id === selectedMigrationId);
if (index === -1) {
setSelectedMigrationId(migrationsIds[0]);
}
}, [migrationsIds, selectedMigrationId]);

const ruleActionsFactory = useCallback(
(ruleMigration: RuleMigration, closeRulePreview: () => void) => {
// TODO: Add flyout action buttons
return null;
type RulesMigrationPageProps = RouteComponentProps<{ migrationId?: string }>;

export const RulesPage: React.FC<RulesMigrationPageProps> = React.memo(
({
match: {
params: { migrationId },
},
[]
);

const { rulePreviewFlyout, openRulePreview } = useRulePreviewFlyout({
ruleActionsFactory,
});

return (
<>
<NeedAdminForUpdateRulesCallOut />
<MissingPrivilegesCallOut />

<SecuritySolutionPageWrapper>
<HeaderPage title={i18n.PAGE_TITLE}>
<HeaderButtons
migrationsIds={migrationsIds}
selectedMigrationId={selectedMigrationId}
onMigrationIdChange={onMigrationIdChange}
}) => {
const { navigateTo } = useNavigation();

const { data: ruleMigrationsStatsAll, isLoading: isLoadingMigrationsStats } = useLatestStats();

const migrationsIds = useMemo(() => {
if (isLoadingMigrationsStats || !ruleMigrationsStatsAll?.length) {
return [];
}
return ruleMigrationsStatsAll
.filter((migration) => migration.status === 'finished')
.map((migration) => migration.id);
}, [isLoadingMigrationsStats, ruleMigrationsStatsAll]);

useEffect(() => {
if (isLoadingMigrationsStats) {
return;
}

// Navigate to landing page if there are no migrations
if (!migrationsIds.length) {
navigateTo({ deepLinkId: SecurityPageName.landing, path: 'siem_migrations' });
return;
}

// Navigate to the most recent migration if none is selected
if (!migrationId) {
navigateTo({ deepLinkId: SecurityPageName.siemMigrationsRules, path: migrationsIds[0] });
}
}, [isLoadingMigrationsStats, migrationId, migrationsIds, navigateTo]);

const onMigrationIdChange = (selectedId?: string) => {
navigateTo({ deepLinkId: SecurityPageName.siemMigrationsRules, path: selectedId });
};

const ruleActionsFactory = useCallback(
(ruleMigration: RuleMigration, closeRulePreview: () => void) => {
// TODO: Add flyout action buttons
return null;
},
[]
);

const { rulePreviewFlyout, openRulePreview } = useRulePreviewFlyout({
ruleActionsFactory,
});

const content = useMemo(() => {
if (!migrationId || !migrationsIds.includes(migrationId)) {
return <UnknownMigration />;
}
return <RulesTable migrationId={migrationId} openRulePreview={openRulePreview} />;
}, [migrationId, migrationsIds, openRulePreview]);

return (
<>
<NeedAdminForUpdateRulesCallOut />
<MissingPrivilegesCallOut />

<SecuritySolutionPageWrapper>
<HeaderPage title={i18n.PAGE_TITLE}>
<HeaderButtons
migrationsIds={migrationsIds}
selectedMigrationId={migrationId}
onMigrationIdChange={onMigrationIdChange}
/>
</HeaderPage>
<EuiSkeletonLoading
isLoading={isLoadingMigrationsStats}
loadingContent={
<>
<EuiSkeletonTitle />
<EuiSkeletonText />
</>
}
loadedContent={content}
/>
</HeaderPage>
<EuiSkeletonLoading
isLoading={isLoadingMigrationsStats}
loadingContent={
<>
<EuiSkeletonTitle />
<EuiSkeletonText />
</>
}
loadedContent={
selectedMigrationId ? (
<RulesTable migrationId={selectedMigrationId} openRulePreview={openRulePreview} />
) : (
<NoMigrations />
)
}
/>
{rulePreviewFlyout}
</SecuritySolutionPageWrapper>
</>
);
});
{rulePreviewFlyout}
</SecuritySolutionPageWrapper>
</>
);
}
);
RulesPage.displayName = 'RulesPage';

0 comments on commit 17410c3

Please sign in to comment.