Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Expand empties
  • Loading branch information
nikitaindik committed Feb 10, 2026
commit 1c8d481bcec66abc383aa258734f98d3a653cf2b
Original file line number Diff line number Diff line change
Expand Up @@ -253,11 +253,11 @@ describe('Rule upgrade workflow: viewing rule changes in JSON diff view', () =>

renderRuleDiffComponent({ oldRule, newRule });
expect(screen.queryAllByText('"author":', { exact: false })).toHaveLength(0);
expect(screen.queryAllByText('Expand 35 unchanged lines')).toHaveLength(1);
expect(screen.queryAllByText('Expand 44 unchanged lines')).toHaveLength(1);

await userEvent.click(screen.getByText('Expand 35 unchanged lines'));
await userEvent.click(screen.getByText('Expand 44 unchanged lines'));

expect(screen.queryAllByText('Expand 35 unchanged lines')).toHaveLength(0);
expect(screen.queryAllByText('Expand 44 unchanged lines')).toHaveLength(0);
expect(screen.queryAllByText('"author":', { exact: false })).toHaveLength(2);
});

Expand Down Expand Up @@ -296,7 +296,7 @@ describe('Rule upgrade workflow: viewing rule changes in JSON diff view', () =>
const arePropertiesSortedInConciseView = checkRenderedPropertyNamesAreSorted();
expect(arePropertiesSortedInConciseView).toBe(true);

await userEvent.click(screen.getByText('Expand 35 unchanged lines'));
await userEvent.click(screen.getByText('Expand 44 unchanged lines'));
const arePropertiesSortedInExpandedView = checkRenderedPropertyNamesAreSorted();
expect(arePropertiesSortedInExpandedView).toBe(true);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
* 2.0.
*/

import { stableStringify } from '@kbn/std';
import type {
RuleSchedule,
SimpleRuleSchedule,
Expand All @@ -20,6 +19,7 @@ import type {
ThreeWayDiff,
} from '../../../../../../common/api/detection_engine';
import type { FieldDiff } from '../../../model/rule_details/rule_field_diff';
import { stringifyWithExpandedEmpties } from '../three_way_diff/comparison_side/utils';

export const sortAndStringifyJson = (fieldValue: unknown): string => {
if (!fieldValue) {
Expand All @@ -29,7 +29,7 @@ export const sortAndStringifyJson = (fieldValue: unknown): string => {
if (typeof fieldValue === 'string') {
return fieldValue;
}
return stableStringify(fieldValue, { space: 2 });
return stringifyWithExpandedEmpties(fieldValue);
};

export const getFieldDiffsForDataSource = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import React, { useMemo } from 'react';
import { omit, pick } from 'lodash';
import { stableStringify } from '@kbn/std';
import {
EuiSpacer,
EuiPanel,
Expand All @@ -21,6 +20,7 @@ import { normalizeMachineLearningJobIds } from '../../../../../common/detection_
import { filterEmptyThreats } from '../../../rule_creation_ui/pages/rule_creation/helpers';
import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema/rule_schemas.gen';
import { DiffView } from './json_diff/diff_view';
import { stringifyWithExpandedEmpties } from './three_way_diff/comparison_side/utils';

/* Inclding these properties in diff display might be confusing to users. */
const HIDDEN_PROPERTIES: Array<keyof RuleResponse> = [
Expand Down Expand Up @@ -60,9 +60,6 @@ const HIDDEN_PROPERTIES: Array<keyof RuleResponse> = [
'execution_summary',
];

const sortAndStringifyJson = (jsObject: Record<string, unknown>): string =>
stableStringify(jsObject, { space: 2 });

/**
* Normalizes the rule object, making it suitable for comparison with another normalized rule.
*
Expand Down Expand Up @@ -144,8 +141,8 @@ export const RuleDiffTab = ({
);

return [
sortAndStringifyJson(visibleOldRuleProperties),
sortAndStringifyJson(visibleNewRuleProperties),
stringifyWithExpandedEmpties(visibleOldRuleProperties),
stringifyWithExpandedEmpties(visibleNewRuleProperties),
];
}, [oldRule, newRule]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,48 @@ export const stringifyToSortedJson = (fieldValue: unknown): string => {
return fieldValue;
}

return stableStringify(fieldValue, { space: 2 });
return stringifyWithExpandedEmpties(fieldValue);
};

/**
* Stringifies a value to pretty-printed JSON with sorted keys,
* then expands inline empty arrays (`[]`) and objects (`{}`) to multi-line format.
*
* This expansion is needed because the diff view uses a line-based diff algorithm.
* Without it, a change from `[]` to a non-empty array (or object) shows as a full replacement
* (delete + insert) instead of a clean insertion, since the algorithm can't match
* the single-line `[]` against the opening `[` of a multi-line array.
*
* @param value - The value to stringify.
* @returns A pretty-printed JSON string with empty collections expanded to multi-line.
*
* @example
* // Input: { "author": [], "name": "Test" }
* // Output:
* // {
* // "author": [
* // ],
* // "name": "Test"
* // }
*/
export const stringifyWithExpandedEmpties = (value: unknown): string => {
const jsonString = stableStringify(value, { space: 2 });

if (jsonString === '[]') {
return '[\n]';
}

if (jsonString === '{}') {
return '{\n}';
}

const expanded = jsonString
// Expand nested empty arrays from "[]" to "[\n]"
.replace(/^(\s*)(.*): \[\]/gm, '$1$2: [\n$1]')
// Expand nested empty objects from "{}" to "{\n}"
.replace(/^(\s*)(.*): \{\}/gm, '$1$2: {\n$1}');

return expanded;
};

interface OptionDetails {
Expand Down
Loading