Skip to content

Commit

Permalink
[8.18] [Security Solution] Disable update button when editing field i…
Browse files Browse the repository at this point in the history
…n rule update flyout (#205999) (#209401)

# Backport

This will backport the following commits from `main` to `8.18`:
- [[Security Solution] Disable update button when editing field in rule
update flyout (#205999)](#205999)

<!--- Backport version: 9.4.3 -->

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

<!--BACKPORT [{"author":{"name":"Jacek
Kolezynski","email":"jacek.kolezynski@elastic.co"},"sourceCommit":{"committedDate":"2025-02-03T18:41:24Z","message":"[Security
Solution] Disable update button when editing field in rule update flyout
(#205999)\n\n**Resolves: #203912**\n\n## Summary\n\nWhen editing a field
in Rule Update flyout, the `Update` button should\nbe temporarily
disabled. Currently it is enabled all the time which
is\ncounterintuitive and may lead to errors.\n\n##
BEFORE\n\n\nhttps://github.com/user-attachments/assets/ef2c7580-247f-4eb1-96aa-43c9454e0e94\n\n##
AFTER\n\n\nhttps://github.com/user-attachments/assets/02c6a3a8-861c-4d41-8dbd-52defb63f642\n\n---------\n\nCo-authored-by:
Nikita Indik
<nikita.indik@elastic.co>","sha":"9891bbdd61e1f2f0d7203b0be27544d4f8f3ef06","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:skip","v9.0.0","Team:Detections
and Resp","Team: SecuritySolution","Team:Detection Rule
Management","Feature:Prebuilt Detection
Rules","backport:version","v8.18.0","v9.1.0"],"title":"[Security
Solution] Disable update button when editing field in rule update
flyout","number":205999,"url":"https://github.com/elastic/kibana/pull/205999","mergeCommit":{"message":"[Security
Solution] Disable update button when editing field in rule update flyout
(#205999)\n\n**Resolves: #203912**\n\n## Summary\n\nWhen editing a field
in Rule Update flyout, the `Update` button should\nbe temporarily
disabled. Currently it is enabled all the time which
is\ncounterintuitive and may lead to errors.\n\n##
BEFORE\n\n\nhttps://github.com/user-attachments/assets/ef2c7580-247f-4eb1-96aa-43c9454e0e94\n\n##
AFTER\n\n\nhttps://github.com/user-attachments/assets/02c6a3a8-861c-4d41-8dbd-52defb63f642\n\n---------\n\nCo-authored-by:
Nikita Indik
<nikita.indik@elastic.co>","sha":"9891bbdd61e1f2f0d7203b0be27544d4f8f3ef06"}},"sourceBranch":"main","suggestedTargetBranches":["9.0","8.18"],"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/205999","number":205999,"mergeCommit":{"message":"[Security
Solution] Disable update button when editing field in rule update flyout
(#205999)\n\n**Resolves: #203912**\n\n## Summary\n\nWhen editing a field
in Rule Update flyout, the `Update` button should\nbe temporarily
disabled. Currently it is enabled all the time which
is\ncounterintuitive and may lead to errors.\n\n##
BEFORE\n\n\nhttps://github.com/user-attachments/assets/ef2c7580-247f-4eb1-96aa-43c9454e0e94\n\n##
AFTER\n\n\nhttps://github.com/user-attachments/assets/02c6a3a8-861c-4d41-8dbd-52defb63f642\n\n---------\n\nCo-authored-by:
Nikita Indik
<nikita.indik@elastic.co>","sha":"9891bbdd61e1f2f0d7203b0be27544d4f8f3ef06"}}]}]
BACKPORT-->

Co-authored-by: Jacek Kolezynski <jacek.kolezynski@elastic.co>
  • Loading branch information
kibanamachine and jkelas authored Feb 3, 2025
1 parent 21be576 commit 0dea724
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
* 2.0.
*/

import React, { createContext, useContext, useMemo } from 'react';
import React, { createContext, useContext, useEffect, useMemo } from 'react';
import { isEqual } from 'lodash';
import { useBoolean } from '@kbn/react-hooks';
import { useRulePreviewContext } from '../../../../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/rule_preview_context';
import { assertUnreachable } from '../../../../../../../common/utility_types';
import {
ThreeWayDiffOutcome,
Expand Down Expand Up @@ -102,6 +103,16 @@ export function FieldUpgradeContextProvider({
initialRightSideMode === FieldFinalSideMode.Edit
);

const { setFieldEditing, setFieldReadonly } = useRulePreviewContext();

useEffect(() => {
if (editing) {
setFieldEditing(fieldName);
} else {
setFieldReadonly(fieldName);
}
}, [setFieldEditing, setFieldReadonly, editing, fieldName]);

invariant(fieldDiff, `Field diff is not found for ${fieldName}.`);

const finalDiffableRule = calcFinalDiffableRule(ruleUpgradeState);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,11 @@ export const AllRules = React.memo(() => {
);
} else {
return (
<>
<UpgradePrebuiltRulesTableContextProvider>
<RulesTableToolbar />
<EuiSpacer />
<UpgradePrebuiltRulesTable />
</UpgradePrebuiltRulesTableContextProvider>
</>
<UpgradePrebuiltRulesTableContextProvider>
<RulesTableToolbar />
<EuiSpacer />
<UpgradePrebuiltRulesTable />
</UpgradePrebuiltRulesTableContextProvider>
);
}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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 { invariant } from '@formatjs/intl-utils';
import useSet from 'react-use/lib/useSet';
import React, { createContext, useContext, useEffect, useMemo } from 'react';
import usePrevious from 'react-use/lib/usePrevious';

export interface RulePreviewContextType {
/**
* Sets the rule is being edited in the rule upgrade flyout
*/
setFieldEditing: (fieldName: string) => void;

/**
* Sets the rule is not being edited in the rule upgrade flyout
*/
setFieldReadonly: (fieldName: string) => void;

/**
* Returns whether the rule is being edited in the rule upgrade flyout
*/
isEditingRule: boolean;
}

const RulePreviewContext = createContext<RulePreviewContextType | null>(null);

interface RulePreviewContextProviderProps {
children: React.ReactNode;
ruleId: string | undefined;
}

export function RulePreviewContextProvider({ children, ruleId }: RulePreviewContextProviderProps) {
const [editedFields, { add, remove, reset }] = useSet<string>();
const prevRuleId = usePrevious(ruleId);

useEffect(() => {
if (ruleId !== prevRuleId) {
reset();
}
}, [reset, ruleId, prevRuleId]);

const isEditingRule = editedFields.size > 0;

const contextValue: RulePreviewContextType = useMemo(
() => ({
isEditingRule,
setFieldEditing: add,
setFieldReadonly: remove,
}),
[isEditingRule, add, remove]
);

return <RulePreviewContext.Provider value={contextValue}>{children}</RulePreviewContext.Provider>;
}

export function useRulePreviewContext() {
const context = useContext(RulePreviewContext);

invariant(
context !== null,
'useRulePreviewContext must be used inside a RulePreviewContextProvider'
);

return context;
}
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
[rulesUpgradeState]
);
const ruleActionsFactory = useCallback(
(rule: RuleResponse, closeRulePreview: () => void) => {
(rule: RuleResponse, closeRulePreview: () => void, isEditingRule: boolean) => {
const ruleUpgradeState = rulesUpgradeState[rule.rule_id];
if (!ruleUpgradeState) {
return null;
Expand All @@ -282,7 +282,8 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
loadingRules.includes(rule.rule_id) ||
isRefetching ||
isUpgradingSecurityPackages ||
(ruleUpgradeState.hasUnresolvedConflicts && !hasRuleTypeChange)
(ruleUpgradeState.hasUnresolvedConflicts && !hasRuleTypeChange) ||
isEditingRule
}
onClick={() => {
if (hasRuleTypeChange || isRulesCustomizationEnabled === false) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,31 @@
*/

import type { ReactNode } from 'react';
import React, { useCallback, useState, useMemo } from 'react';
import React, { useCallback, useState, useMemo, memo } from 'react';
import type { EuiTabbedContentTab } from '@elastic/eui';
import { invariant } from '../../../../../common/utils/invariant';
import type { RuleSignatureId } from '../../../../../common/api/detection_engine';
import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema';
import { RuleDetailsFlyout } from '../../../rule_management/components/rule_details/rule_details_flyout';

interface UseRulePreviewFlyoutParams {
rules: RuleResponse[];
ruleActionsFactory: (rule: RuleResponse, closeRulePreview: () => void) => ReactNode;
import {
RulePreviewContextProvider,
useRulePreviewContext,
} from './upgrade_prebuilt_rules_table/rule_preview_context';
interface UseRulePreviewFlyoutBaseParams {
ruleActionsFactory: (
rule: RuleResponse,
closeRulePreview: () => void,
isEditingRule: boolean
) => ReactNode;
extraTabsFactory?: (rule: RuleResponse) => EuiTabbedContentTab[];
subHeaderFactory?: (rule: RuleResponse) => ReactNode;
flyoutProps: RulePreviewFlyoutProps;
}

interface UseRulePreviewFlyoutParams extends UseRulePreviewFlyoutBaseParams {
rules: RuleResponse[];
}

interface RulePreviewFlyoutProps {
/**
* Rule preview flyout unique id used in HTML
Expand All @@ -44,41 +54,76 @@ export function useRulePreviewFlyout({
}: UseRulePreviewFlyoutParams): UseRulePreviewFlyoutResult {
const [rule, setRuleForPreview] = useState<RuleResponse | undefined>();
const closeRulePreview = useCallback(() => setRuleForPreview(undefined), []);
const subHeader = useMemo(
() => (rule ? subHeaderFactory?.(rule) : null),
[subHeaderFactory, rule]
const openRulePreview = useCallback(
(ruleId: RuleSignatureId) => {
const ruleToShowInFlyout = rules.find((x) => x.rule_id === ruleId);

invariant(ruleToShowInFlyout, `Rule with rule_id ${ruleId} not found`);
setRuleForPreview(ruleToShowInFlyout);
},
[rules, setRuleForPreview]
);
const rulePreviewFlyout = (
<RulePreviewContextProvider ruleId={rule?.rule_id}>
<RulePreviewFlyoutInternal
rule={rule}
closeRulePreview={closeRulePreview}
extraTabsFactory={extraTabsFactory}
ruleActionsFactory={ruleActionsFactory}
subHeaderFactory={subHeaderFactory}
flyoutProps={flyoutProps}
/>
</RulePreviewContextProvider>
);

return {
rulePreviewFlyout,
openRulePreview,
closeRulePreview,
};
}

const RulePreviewFlyoutInternal = memo(function RulePreviewFlyoutInternal({
rule,
closeRulePreview,
extraTabsFactory,
ruleActionsFactory,
subHeaderFactory,
flyoutProps,
}: UseRulePreviewFlyoutBaseParams & {
rule: RuleResponse | undefined;
closeRulePreview: () => void;
}) {
const { isEditingRule } = useRulePreviewContext();

const ruleActions = useMemo(
() => rule && ruleActionsFactory(rule, closeRulePreview),
[rule, ruleActionsFactory, closeRulePreview]
() => rule && ruleActionsFactory(rule, closeRulePreview, isEditingRule),
[rule, ruleActionsFactory, closeRulePreview, isEditingRule]
);
const extraTabs = useMemo(
() => (rule && extraTabsFactory ? extraTabsFactory(rule) : []),
[rule, extraTabsFactory]
);

return {
rulePreviewFlyout: rule && (
<RuleDetailsFlyout
rule={rule}
size="l"
id={flyoutProps.id}
dataTestSubj={flyoutProps.dataTestSubj}
closeFlyout={closeRulePreview}
ruleActions={ruleActions}
extraTabs={extraTabs}
subHeader={subHeader}
/>
),
openRulePreview: useCallback(
(ruleId: RuleSignatureId) => {
const ruleToShowInFlyout = rules.find((x) => x.rule_id === ruleId);
const subHeader = useMemo(
() => (rule ? subHeaderFactory?.(rule) : null),
[subHeaderFactory, rule]
);

invariant(ruleToShowInFlyout, `Rule with rule_id ${ruleId} not found`);
setRuleForPreview(ruleToShowInFlyout);
},
[rules, setRuleForPreview]
),
closeRulePreview,
};
}
if (!rule) {
return null;
}

return (
<RuleDetailsFlyout
rule={rule}
size="l"
id={flyoutProps.id}
dataTestSubj={flyoutProps.dataTestSubj}
closeFlyout={closeRulePreview}
ruleActions={ruleActions}
extraTabs={extraTabs}
subHeader={subHeader}
/>
);
});

0 comments on commit 0dea724

Please sign in to comment.