Skip to content

Commit fe58808

Browse files
authored
feat(settings, components): add preference for legacy UUID display encoding COMPASS-9690 (#7625)
1 parent 828087f commit fe58808

File tree

11 files changed

+391
-63
lines changed

11 files changed

+391
-63
lines changed

packages/compass-components/src/components/bson-value.spec.tsx

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import BSONValue from './bson-value';
1919
import { expect } from 'chai';
2020
import { render, cleanup, screen } from '@mongodb-js/testing-library-compass';
21+
import { LegacyUUIDDisplayContext } from './document-list/legacy-uuid-format-context';
2122

2223
describe('BSONValue', function () {
2324
afterEach(cleanup);
@@ -45,6 +46,11 @@ describe('BSONValue', function () {
4546
value: Binary.createFromHexString('3132303d', Binary.SUBTYPE_UUID),
4647
expected: "UUID('3132303d')",
4748
},
49+
{
50+
type: 'Binary',
51+
value: Binary.createFromBase64('dGVzdA==', Binary.SUBTYPE_UUID_OLD),
52+
expected: "Binary.createFromBase64('dGVzdA==', 3)",
53+
},
4854
{
4955
type: 'Binary',
5056
value: Binary.fromInt8Array(new Int8Array([1, 2, 3])),
@@ -159,4 +165,109 @@ describe('BSONValue', function () {
159165
expect(await screen.findByTestId('bson-value-in-use-encryption-docs-link'))
160166
.to.be.visible;
161167
});
168+
169+
describe('Legacy UUID display formats', function () {
170+
const legacyUuidBinary = Binary.createFromHexString(
171+
'0123456789abcdef0123456789abcdef',
172+
Binary.SUBTYPE_UUID_OLD
173+
);
174+
175+
it('should render Legacy UUID without encoding (raw format)', function () {
176+
const { container } = render(
177+
<LegacyUUIDDisplayContext.Provider value="">
178+
<BSONValue type="Binary" value={legacyUuidBinary} />
179+
</LegacyUUIDDisplayContext.Provider>
180+
);
181+
182+
expect(container.querySelector('.element-value')?.textContent).to.include(
183+
"Binary.createFromBase64('ASNFZ4mrze8BI0VniavN7w==', 3)"
184+
);
185+
});
186+
187+
it('should render Legacy UUID in Java format', function () {
188+
const { container } = render(
189+
<LegacyUUIDDisplayContext.Provider value="LegacyJavaUUID">
190+
<BSONValue type="Binary" value={legacyUuidBinary} />
191+
</LegacyUUIDDisplayContext.Provider>
192+
);
193+
194+
expect(container.querySelector('.element-value')?.textContent).to.eq(
195+
'LegacyJavaUUID("efcdab89-6745-2301-efcd-ab8967452301")'
196+
);
197+
});
198+
199+
it('should render Legacy UUID in C# format', function () {
200+
const { container } = render(
201+
<LegacyUUIDDisplayContext.Provider value="LegacyCSharpUUID">
202+
<BSONValue type="Binary" value={legacyUuidBinary} />
203+
</LegacyUUIDDisplayContext.Provider>
204+
);
205+
206+
expect(container.querySelector('.element-value')?.textContent).to.eq(
207+
'LegacyCSharpUUID("67452301-ab89-efcd-0123-456789abcdef")'
208+
);
209+
});
210+
211+
it('should render Legacy UUID in Python format', function () {
212+
const { container } = render(
213+
<LegacyUUIDDisplayContext.Provider value="LegacyPythonUUID">
214+
<BSONValue type="Binary" value={legacyUuidBinary} />
215+
</LegacyUUIDDisplayContext.Provider>
216+
);
217+
218+
expect(container.querySelector('.element-value')?.textContent).to.eq(
219+
'LegacyPythonUUID("01234567-89ab-cdef-0123-456789abcdef")'
220+
);
221+
});
222+
223+
it('should fallback to raw format if UUID conversion fails', function () {
224+
// Create an invalid UUID binary that will cause conversion to fail.
225+
const invalidUuidBinary = new Binary(
226+
Buffer.from('invalid'),
227+
Binary.SUBTYPE_UUID_OLD
228+
);
229+
230+
const { container } = render(
231+
<LegacyUUIDDisplayContext.Provider value="LegacyJavaUUID">
232+
<BSONValue type="Binary" value={invalidUuidBinary} />
233+
</LegacyUUIDDisplayContext.Provider>
234+
);
235+
236+
expect(container.querySelector('.element-value')?.textContent).to.include(
237+
'Binary.createFromBase64('
238+
);
239+
});
240+
241+
it('should fallback to raw format for all Legacy UUID formats on error', function () {
242+
const invalidUuidBinary = new Binary(
243+
Buffer.from('invalid'),
244+
Binary.SUBTYPE_UUID_OLD
245+
);
246+
247+
const formats = [
248+
'LegacyJavaUUID',
249+
'LegacyCSharpUUID',
250+
'LegacyPythonUUID',
251+
] as const;
252+
253+
formats.forEach((format) => {
254+
const { container } = render(
255+
<LegacyUUIDDisplayContext.Provider value={format}>
256+
<BSONValue type="Binary" value={invalidUuidBinary} />
257+
</LegacyUUIDDisplayContext.Provider>
258+
);
259+
260+
expect(
261+
container.querySelector('.element-value')?.textContent
262+
).to.include(
263+
'Binary.createFromBase64(',
264+
`${format} should fallback to raw format`
265+
);
266+
expect(
267+
container.querySelector('.element-value')?.textContent
268+
).to.include(', 3)', `${format} should show subtype 3`);
269+
cleanup();
270+
});
271+
});
272+
});
162273
});

packages/compass-components/src/components/bson-value.tsx

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { spacing } from '@leafygreen-ui/tokens';
99
import { css, cx } from '@leafygreen-ui/emotion';
1010
import type { Theme } from '../hooks/use-theme';
1111
import { Themes, useDarkMode } from '../hooks/use-theme';
12+
import { useLegacyUUIDDisplayContext } from './document-list/legacy-uuid-format-context';
1213

1314
type ValueProps =
1415
| {
@@ -124,6 +125,108 @@ const ObjectIdValue: React.FunctionComponent<PropsByValueType<'ObjectId'>> = ({
124125
);
125126
};
126127

128+
const toUUIDWithHyphens = (hex: string): string => {
129+
return (
130+
hex.substring(0, 8) +
131+
'-' +
132+
hex.substring(8, 12) +
133+
'-' +
134+
hex.substring(12, 16) +
135+
'-' +
136+
hex.substring(16, 20) +
137+
'-' +
138+
hex.substring(20, 32)
139+
);
140+
};
141+
142+
const toLegacyJavaUUID = ({ value }: PropsByValueType<'Binary'>) => {
143+
// Get the hex representation from the buffer.
144+
const hex = Buffer.from(value.buffer).toString('hex');
145+
// Reverse byte order for Java legacy UUID format (reverse all bytes).
146+
let msb = hex.substring(0, 16);
147+
let lsb = hex.substring(16, 32);
148+
// Reverse pairs of hex characters (bytes).
149+
msb =
150+
msb.substring(14, 16) +
151+
msb.substring(12, 14) +
152+
msb.substring(10, 12) +
153+
msb.substring(8, 10) +
154+
msb.substring(6, 8) +
155+
msb.substring(4, 6) +
156+
msb.substring(2, 4) +
157+
msb.substring(0, 2);
158+
lsb =
159+
lsb.substring(14, 16) +
160+
lsb.substring(12, 14) +
161+
lsb.substring(10, 12) +
162+
lsb.substring(8, 10) +
163+
lsb.substring(6, 8) +
164+
lsb.substring(4, 6) +
165+
lsb.substring(2, 4) +
166+
lsb.substring(0, 2);
167+
const uuid = msb + lsb;
168+
return 'LegacyJavaUUID("' + toUUIDWithHyphens(uuid) + '")';
169+
};
170+
171+
const toLegacyCSharpUUID = ({ value }: PropsByValueType<'Binary'>) => {
172+
// Get the hex representation from the buffer.
173+
const hex = Buffer.from(value.buffer).toString('hex');
174+
// Reverse byte order for C# legacy UUID format (first 3 groups only).
175+
const a =
176+
hex.substring(6, 8) +
177+
hex.substring(4, 6) +
178+
hex.substring(2, 4) +
179+
hex.substring(0, 2);
180+
const b = hex.substring(10, 12) + hex.substring(8, 10);
181+
const c = hex.substring(14, 16) + hex.substring(12, 14);
182+
const d = hex.substring(16, 32);
183+
const uuid = a + b + c + d;
184+
return 'LegacyCSharpUUID("' + toUUIDWithHyphens(uuid) + '")';
185+
};
186+
187+
const toLegacyPythonUUID = ({ value }: PropsByValueType<'Binary'>) => {
188+
// Get the hex representation from the buffer.
189+
const hex = Buffer.from(value.buffer).toString('hex');
190+
return 'LegacyPythonUUID("' + toUUIDWithHyphens(hex) + '")';
191+
};
192+
193+
// Binary sub_type 3.
194+
const LegacyUUIDValue: React.FunctionComponent<PropsByValueType<'Binary'>> = (
195+
bsonValue
196+
) => {
197+
const legacyUUIDDisplayEncoding = useLegacyUUIDDisplayContext();
198+
199+
const stringifiedValue = useMemo(() => {
200+
// UUID must be exactly 16 bytes.
201+
if (bsonValue.value.buffer.length === 16) {
202+
try {
203+
if (legacyUUIDDisplayEncoding === 'LegacyJavaUUID') {
204+
return toLegacyJavaUUID(bsonValue);
205+
} else if (legacyUUIDDisplayEncoding === 'LegacyCSharpUUID') {
206+
return toLegacyCSharpUUID(bsonValue);
207+
} else if (legacyUUIDDisplayEncoding === 'LegacyPythonUUID') {
208+
return toLegacyPythonUUID(bsonValue);
209+
}
210+
} catch {
211+
// Ignore errors and fallback to the raw representation.
212+
// The UUID conversion can fail if the binary data is not a valid UUID.
213+
}
214+
}
215+
216+
// Raw, no encoding.
217+
return `Binary.createFromBase64('${truncate(
218+
bsonValue.value.toString('base64'),
219+
100
220+
)}', ${bsonValue.value.sub_type})`;
221+
}, [legacyUUIDDisplayEncoding, bsonValue]);
222+
223+
return (
224+
<BSONValueContainer type="Binary" title={stringifiedValue}>
225+
{stringifiedValue}
226+
</BSONValueContainer>
227+
);
228+
};
229+
127230
const BinaryValue: React.FunctionComponent<PropsByValueType<'Binary'>> = ({
128231
value,
129232
}) => {
@@ -242,7 +345,9 @@ const DateValue: React.FunctionComponent<PropsByValueType<'Date'>> = ({
242345
};
243346

244347
const NumberValue: React.FunctionComponent<
245-
PropsByValueType<'Int32' | 'Double'> & { type: 'Int32' | 'Double' }
348+
PropsByValueType<'Int32' | 'Double' | 'Int64' | 'Decimal128'> & {
349+
type: 'Int32' | 'Double' | 'Int64' | 'Decimal128';
350+
}
246351
> = ({ type, value }) => {
247352
const stringifiedValue = useMemo(() => {
248353
return String(value.valueOf());
@@ -377,6 +482,9 @@ const BSONValue: React.FunctionComponent<ValueProps> = (props) => {
377482
case 'Date':
378483
return <DateValue value={props.value}></DateValue>;
379484
case 'Binary':
485+
if (props.value.sub_type === Binary.SUBTYPE_UUID_OLD) {
486+
return <LegacyUUIDValue value={props.value}></LegacyUUIDValue>;
487+
}
380488
return <BinaryValue value={props.value}></BinaryValue>;
381489
case 'Int32':
382490
case 'Double':

packages/compass-components/src/components/compass-components-provider.tsx

Lines changed: 47 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import {
1313
} from './context-menu';
1414
import { DrawerContentProvider } from './drawer-portal';
1515
import { CopyPasteContextMenu } from '../hooks/use-copy-paste-context-menu';
16+
import {
17+
type LegacyUUIDDisplay,
18+
LegacyUUIDDisplayContext,
19+
} from './document-list/legacy-uuid-format-context';
1620

1721
type GuideCueProviderProps = React.ComponentProps<typeof GuideCueProvider>;
1822

@@ -22,6 +26,7 @@ type CompassComponentsProviderProps = {
2226
* value will be derived from the system settings
2327
*/
2428
darkMode?: boolean;
29+
legacyUUIDDisplayEncoding?: LegacyUUIDDisplay;
2530
popoverPortalContainer?: HTMLElement;
2631
/**
2732
* Either React children or a render callback that will get the darkMode
@@ -124,6 +129,7 @@ function useDarkMode(_darkMode?: boolean) {
124129
export const CompassComponentsProvider = ({
125130
darkMode: _darkMode,
126131
children,
132+
legacyUUIDDisplayEncoding,
127133
onNextGuideGue,
128134
onNextGuideCueGroup,
129135
onContextMenuOpen,
@@ -161,45 +167,49 @@ export const CompassComponentsProvider = ({
161167
darkMode={darkMode}
162168
popoverPortalContainer={popoverPortalContainer}
163169
>
164-
<DrawerContentProvider
165-
onDrawerSectionOpen={onDrawerSectionOpen}
166-
onDrawerSectionHide={onDrawerSectionHide}
170+
<LegacyUUIDDisplayContext.Provider
171+
value={legacyUUIDDisplayEncoding ?? ''}
167172
>
168-
<StackedComponentProvider zIndex={stackedElementsZIndex}>
169-
<RequiredURLSearchParamsProvider
170-
utmSource={utmSource}
171-
utmMedium={utmMedium}
172-
>
173-
<GuideCueProvider
174-
onNext={onNextGuideGue}
175-
onNextGroup={onNextGuideCueGroup}
176-
disabled={disableGuideCues}
173+
<DrawerContentProvider
174+
onDrawerSectionOpen={onDrawerSectionOpen}
175+
onDrawerSectionHide={onDrawerSectionHide}
176+
>
177+
<StackedComponentProvider zIndex={stackedElementsZIndex}>
178+
<RequiredURLSearchParamsProvider
179+
utmSource={utmSource}
180+
utmMedium={utmMedium}
177181
>
178-
<SignalHooksProvider {...signalHooksProviderProps}>
179-
<ConfirmationModalArea>
180-
<ContextMenuProvider
181-
disabled={disableContextMenus}
182-
onContextMenuOpen={onContextMenuOpen}
183-
onContextMenuItemClick={onContextMenuItemClick}
184-
>
185-
<CopyPasteContextMenu>
186-
<ToastArea>
187-
{typeof children === 'function'
188-
? children({
189-
darkMode,
190-
portalContainerRef: setPortalContainer,
191-
scrollContainerRef: setScrollContainer,
192-
})
193-
: children}
194-
</ToastArea>
195-
</CopyPasteContextMenu>
196-
</ContextMenuProvider>
197-
</ConfirmationModalArea>
198-
</SignalHooksProvider>
199-
</GuideCueProvider>
200-
</RequiredURLSearchParamsProvider>
201-
</StackedComponentProvider>
202-
</DrawerContentProvider>
182+
<GuideCueProvider
183+
onNext={onNextGuideGue}
184+
onNextGroup={onNextGuideCueGroup}
185+
disabled={disableGuideCues}
186+
>
187+
<SignalHooksProvider {...signalHooksProviderProps}>
188+
<ConfirmationModalArea>
189+
<ContextMenuProvider
190+
disabled={disableContextMenus}
191+
onContextMenuOpen={onContextMenuOpen}
192+
onContextMenuItemClick={onContextMenuItemClick}
193+
>
194+
<CopyPasteContextMenu>
195+
<ToastArea>
196+
{typeof children === 'function'
197+
? children({
198+
darkMode,
199+
portalContainerRef: setPortalContainer,
200+
scrollContainerRef: setScrollContainer,
201+
})
202+
: children}
203+
</ToastArea>
204+
</CopyPasteContextMenu>
205+
</ContextMenuProvider>
206+
</ConfirmationModalArea>
207+
</SignalHooksProvider>
208+
</GuideCueProvider>
209+
</RequiredURLSearchParamsProvider>
210+
</StackedComponentProvider>
211+
</DrawerContentProvider>
212+
</LegacyUUIDDisplayContext.Provider>
203213
</LeafyGreenProvider>
204214
);
205215
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { createContext, useContext } from 'react';
2+
3+
export type LegacyUUIDDisplay =
4+
| ''
5+
| 'LegacyJavaUUID'
6+
| 'LegacyCSharpUUID'
7+
| 'LegacyPythonUUID';
8+
9+
export const LegacyUUIDDisplayContext = createContext<LegacyUUIDDisplay>('');
10+
11+
export function useLegacyUUIDDisplayContext(): LegacyUUIDDisplay {
12+
return useContext(LegacyUUIDDisplayContext);
13+
}

0 commit comments

Comments
 (0)