1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

chore(1-3835): improve json diff view (#10146)

Replaces the existing JSON diff implementation we use with
`json-diff-react` (35kb unpacked, according to npm), a react-fork of the
popular `json-diff` library. The change is behind a new flag.

The new library has several advantages:
- nicer formatting (including nested objects)
- we don't need to calculate the diff manually anymore
- option to hide/reveal unchanged properties

There's still a few more things to put in place (such as handling of no
changes) and overflow handling when you have very long properties.

Here's a few comparison screenies:

Old (below) vs new (above):

![image](https://github.com/user-attachments/assets/466472cc-9499-4d22-8f62-e3f9096496d1)

Fold and unfold:

![image](https://github.com/user-attachments/assets/3d213aee-a3a6-42e2-8fbb-0133276aa790)

![image](https://github.com/user-attachments/assets/3f7d7aeb-5835-4f32-9d6e-97cf09df1c0e)

In change requests:

![image](https://github.com/user-attachments/assets/c529ff8c-05c8-4ec7-a49d-ac58a1eeea98)

Strategy re-ordering:
Folded:

![image](https://github.com/user-attachments/assets/26ef905d-c766-4982-be9a-83ff15260e23)

Unfolded:

![image](https://github.com/user-attachments/assets/d2e212c3-351c-42fe-a645-ec8e9c71146e)

Old:

![image](https://github.com/user-attachments/assets/041310b0-149b-417a-a724-8d37f1ad44f7)
This commit is contained in:
Thomas Heartman 2025-06-17 12:01:17 +02:00 committed by GitHub
parent d2b9751e01
commit c619cb9ec5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 114 additions and 8 deletions

View File

@ -160,6 +160,7 @@
"packageManager": "yarn@4.9.2",
"dependencies": {
"chartjs-plugin-datalabels": "^2.2.0",
"json-2-csv": "^5.5.5"
"json-2-csv": "^5.5.5",
"json-diff-react": "^1.0.1"
}
}

View File

@ -1,6 +1,8 @@
import { diff } from 'deep-diff';
import { useTheme } from '@mui/system';
import type { JSX, CSSProperties } from 'react';
import { type JSX, type CSSProperties, useState, type FC, useId } from 'react';
import { JsonDiffComponent, type JsonValue } from 'json-diff-react';
import { Button, styled, useTheme } from '@mui/material';
import { useUiFlag } from 'hooks/useUiFlag';
const DIFF_PREFIXES: Record<string, string> = {
A: ' ',
@ -17,10 +19,71 @@ interface IEventDiffResult {
interface IEventDiffProps {
entry: { data?: unknown; preData?: unknown };
/**
* @deprecated remove with flag improvedJsonDiff
*/
sort?: (a: IEventDiffResult, b: IEventDiffResult) => number;
}
const EventDiff = ({
const DiffStyles = styled('div')(({ theme }) => ({
color: theme.palette.text.secondary,
fontFamily: 'monospace',
whiteSpace: 'pre',
fontSize: theme.typography.body2.fontSize,
'.deletion, .addition': {
position: 'relative',
'::before': {
position: 'absolute',
left: 0,
top: 0,
marginLeft: '-10px',
},
},
'.addition': {
color: theme.palette.eventLog.diffAdd,
'::before': {
content: '"+"',
},
},
'.deletion': {
color: theme.palette.eventLog.diffSub,
'::before': {
content: '"-"',
},
},
}));
const NewEventDiff: FC<IEventDiffProps> = ({ entry }) => {
const [full, setFull] = useState(false);
const diffId = useId();
return (
<>
<Button
onClick={() => setFull(!full)}
aria-controls={diffId}
aria-expanded={full}
>
{full ? 'Show only changed properties' : 'Show all properties'}
</Button>
<DiffStyles id={diffId}>
<JsonDiffComponent
jsonA={(entry.preData ?? {}) as JsonValue}
jsonB={(entry.data ?? {}) as JsonValue}
jsonDiffOptions={{
full: full,
maxElisions: 2,
excludeKeys: ['id', 'createdAt', 'updatedAt'],
}}
/>
</DiffStyles>
</>
);
};
const OldEventDiff: FC<IEventDiffProps> = ({
entry,
sort = (a, b) => a.key.localeCompare(b.key),
}: IEventDiffProps) => {
@ -113,11 +176,18 @@ const EventDiff = ({
}
return (
// biome-ignore lint/a11y/noNoninteractiveTabindex: <explanation>
<pre style={{ overflowX: 'auto', overflowY: 'hidden' }} tabIndex={0}>
<pre style={{ overflowX: 'auto', overflowY: 'hidden' }}>
<code>{changes.length === 0 ? '(no changes)' : changes}</code>
</pre>
);
};
const EventDiff: FC<IEventDiffProps> = (props) => {
const useNewJsonDiff = useUiFlag('improvedJsonDiff');
if (useNewJsonDiff) {
return <NewEventDiff {...props} />;
}
return <OldEventDiff {...props} />;
};
export default EventDiff;

View File

@ -90,6 +90,7 @@ export type UiFlags = {
lifecycleMetrics?: boolean;
createFlagDialogCache?: boolean;
healthToTechDebt?: boolean;
improvedJsonDiff?: boolean;
};
export interface IVersionInfo {

View File

@ -1277,6 +1277,15 @@ __metadata:
languageName: node
linkType: hard
"@ewoudenberg/difflib@npm:^0.1.0":
version: 0.1.0
resolution: "@ewoudenberg/difflib@npm:0.1.0"
dependencies:
heap: "npm:>= 0.2.0"
checksum: 10c0/3060807c91f39c5c1e5421fe51573bb2bffcab48cb32e9dcc69ce0d43e658dbf5959421382541012628ca6b842d252fc157a79f70cea8088a864572fec2bd094
languageName: node
linkType: hard
"@exodus/schemasafe@npm:^1.0.0-rc.2":
version: 1.3.0
resolution: "@exodus/schemasafe@npm:1.3.0"
@ -6079,6 +6088,13 @@ __metadata:
languageName: node
linkType: hard
"heap@npm:>= 0.2.0":
version: 0.2.7
resolution: "heap@npm:0.2.7"
checksum: 10c0/341c5d51ae13dc8346c371a8a69c57c972fcb9a3233090d3dd5ba29d483d6b5b4e75492443cbfeacd46608bb30e6680f646ffb7a6205900221735587d07a79b6
languageName: node
linkType: hard
"hoist-non-react-statics@npm:^3.3.1":
version: 3.3.2
resolution: "hoist-non-react-statics@npm:3.3.2"
@ -6806,6 +6822,17 @@ __metadata:
languageName: node
linkType: hard
"json-diff-react@npm:^1.0.1":
version: 1.0.1
resolution: "json-diff-react@npm:1.0.1"
dependencies:
"@ewoudenberg/difflib": "npm:^0.1.0"
react: "npm:^18.2.0"
react-dom: "npm:^18.2.0"
checksum: 10c0/085c395f61a8fb148d60c91bb279dbc071e76a89f96634da41df03b72ae365f916f3e5a12e77d6f4f64a680572849ae61d5a5853e1b83f9d7f31e7e8aa8e6de0
languageName: node
linkType: hard
"json-parse-even-better-errors@npm:^2.3.0":
version: 2.3.1
resolution: "json-parse-even-better-errors@npm:2.3.1"
@ -8567,7 +8594,7 @@ __metadata:
languageName: node
linkType: hard
"react-dom@npm:18.3.1":
"react-dom@npm:18.3.1, react-dom@npm:^18.2.0":
version: 18.3.1
resolution: "react-dom@npm:18.3.1"
dependencies:
@ -8826,7 +8853,7 @@ __metadata:
languageName: node
linkType: hard
"react@npm:18.3.1":
"react@npm:18.3.1, react@npm:^18.2.0":
version: 18.3.1
resolution: "react@npm:18.3.1"
dependencies:
@ -10369,6 +10396,7 @@ __metadata:
immer: "npm:9.0.21"
jsdom: "npm:25.0.1"
json-2-csv: "npm:^5.5.5"
json-diff-react: "npm:^1.0.1"
lodash.clonedeep: "npm:4.5.0"
lodash.isequal: "npm:^4.5.0"
lodash.mapvalues: "npm:^4.6.0"

View File

@ -61,6 +61,7 @@ export type IFlagKey =
| 'lifecycleMetrics'
| 'customMetrics'
| 'createFlagDialogCache'
| 'improvedJsonDiff'
| 'changeRequestApproverEmails';
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
@ -286,6 +287,10 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_CHANGE_REQUEST_APPROVER_EMAILS,
false,
),
improvedJsonDiff: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_IMPROVED_JSON_DIFF,
false,
),
};
export const defaultExperimentalOptions: IExperimentalOptions = {

View File

@ -55,6 +55,7 @@ process.nextTick(async () => {
reportUnknownFlags: true,
customMetrics: true,
lifecycleMetrics: true,
improvedJsonDiff: true,
},
},
authentication: {