Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
111 changes: 111 additions & 0 deletions packages/compass-components/src/components/bson-value.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import BSONValue from './bson-value';
import { expect } from 'chai';
import { render, cleanup, screen } from '@mongodb-js/testing-library-compass';
import { LegacyUUIDDisplayContext } from './document-list/legacy-uuid-format-context';

describe('BSONValue', function () {
afterEach(cleanup);
Expand Down Expand Up @@ -45,6 +46,11 @@ describe('BSONValue', function () {
value: Binary.createFromHexString('3132303d', Binary.SUBTYPE_UUID),
expected: "UUID('3132303d')",
},
{
type: 'Binary',
value: Binary.createFromBase64('dGVzdA==', Binary.SUBTYPE_UUID_OLD),
expected: "Binary.createFromBase64('dGVzdA==', 3)",
},
{
type: 'Binary',
value: Binary.fromInt8Array(new Int8Array([1, 2, 3])),
Expand Down Expand Up @@ -159,4 +165,109 @@ describe('BSONValue', function () {
expect(await screen.findByTestId('bson-value-in-use-encryption-docs-link'))
.to.be.visible;
});

describe('Legacy UUID display formats', function () {
const legacyUuidBinary = Binary.createFromHexString(
'0123456789abcdef0123456789abcdef',
Binary.SUBTYPE_UUID_OLD
);

it('should render Legacy UUID without encoding (raw format)', function () {
const { container } = render(
<LegacyUUIDDisplayContext.Provider value="">
<BSONValue type="Binary" value={legacyUuidBinary} />
</LegacyUUIDDisplayContext.Provider>
);

expect(container.querySelector('.element-value')?.textContent).to.include(
"Binary.createFromBase64('ASNFZ4mrze8BI0VniavN7w==', 3)"
);
});

it('should render Legacy UUID in Java format', function () {
const { container } = render(
<LegacyUUIDDisplayContext.Provider value="LegacyJavaUUID">
<BSONValue type="Binary" value={legacyUuidBinary} />
</LegacyUUIDDisplayContext.Provider>
);

expect(container.querySelector('.element-value')?.textContent).to.eq(
'LegacyJavaUUID("efcdab89-6745-2301-efcd-ab8967452301")'
);
});

it('should render Legacy UUID in C# format', function () {
const { container } = render(
<LegacyUUIDDisplayContext.Provider value="LegacyCSharpUUID">
<BSONValue type="Binary" value={legacyUuidBinary} />
</LegacyUUIDDisplayContext.Provider>
);

expect(container.querySelector('.element-value')?.textContent).to.eq(
'LegacyCSharpUUID("67452301-ab89-efcd-0123-456789abcdef")'
);
});

it('should render Legacy UUID in Python format', function () {
const { container } = render(
<LegacyUUIDDisplayContext.Provider value="LegacyPythonUUID">
<BSONValue type="Binary" value={legacyUuidBinary} />
</LegacyUUIDDisplayContext.Provider>
);

expect(container.querySelector('.element-value')?.textContent).to.eq(
'LegacyPythonUUID("01234567-89ab-cdef-0123-456789abcdef")'
);
});

it('should fallback to raw format if UUID conversion fails', function () {
// Create an invalid UUID binary that will cause conversion to fail.
const invalidUuidBinary = new Binary(
Buffer.from('invalid'),
Binary.SUBTYPE_UUID_OLD
);

const { container } = render(
<LegacyUUIDDisplayContext.Provider value="LegacyJavaUUID">
<BSONValue type="Binary" value={invalidUuidBinary} />
</LegacyUUIDDisplayContext.Provider>
);

expect(container.querySelector('.element-value')?.textContent).to.include(
'Binary.createFromBase64('
);
});

it('should fallback to raw format for all Legacy UUID formats on error', function () {
const invalidUuidBinary = new Binary(
Buffer.from('invalid'),
Binary.SUBTYPE_UUID_OLD
);

const formats = [
'LegacyJavaUUID',
'LegacyCSharpUUID',
'LegacyPythonUUID',
] as const;

formats.forEach((format) => {
const { container } = render(
<LegacyUUIDDisplayContext.Provider value={format}>
<BSONValue type="Binary" value={invalidUuidBinary} />
</LegacyUUIDDisplayContext.Provider>
);

expect(
container.querySelector('.element-value')?.textContent
).to.include(
'Binary.createFromBase64(',
`${format} should fallback to raw format`
);
expect(
container.querySelector('.element-value')?.textContent
).to.include(', 3)', `${format} should show subtype 3`);
cleanup();
});
});
});
});
110 changes: 109 additions & 1 deletion packages/compass-components/src/components/bson-value.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { spacing } from '@leafygreen-ui/tokens';
import { css, cx } from '@leafygreen-ui/emotion';
import type { Theme } from '../hooks/use-theme';
import { Themes, useDarkMode } from '../hooks/use-theme';
import { useLegacyUUIDDisplayContext } from './document-list/legacy-uuid-format-context';

type ValueProps =
| {
Expand Down Expand Up @@ -124,6 +125,108 @@ const ObjectIdValue: React.FunctionComponent<PropsByValueType<'ObjectId'>> = ({
);
};

const toUUIDWithHyphens = (hex: string): string => {
return (
hex.substring(0, 8) +
'-' +
hex.substring(8, 12) +
'-' +
hex.substring(12, 16) +
'-' +
hex.substring(16, 20) +
'-' +
hex.substring(20, 32)
);
};

const toLegacyJavaUUID = ({ value }: PropsByValueType<'Binary'>) => {
// Get the hex representation from the buffer.
const hex = Buffer.from(value.buffer).toString('hex');
// Reverse byte order for Java legacy UUID format (reverse all bytes).
let msb = hex.substring(0, 16);
let lsb = hex.substring(16, 32);
// Reverse pairs of hex characters (bytes).
msb =
msb.substring(14, 16) +
msb.substring(12, 14) +
msb.substring(10, 12) +
msb.substring(8, 10) +
msb.substring(6, 8) +
msb.substring(4, 6) +
msb.substring(2, 4) +
msb.substring(0, 2);
lsb =
lsb.substring(14, 16) +
lsb.substring(12, 14) +
lsb.substring(10, 12) +
lsb.substring(8, 10) +
lsb.substring(6, 8) +
lsb.substring(4, 6) +
lsb.substring(2, 4) +
lsb.substring(0, 2);
const uuid = msb + lsb;
return 'LegacyJavaUUID("' + toUUIDWithHyphens(uuid) + '")';
};

const toLegacyCSharpUUID = ({ value }: PropsByValueType<'Binary'>) => {
// Get the hex representation from the buffer.
const hex = Buffer.from(value.buffer).toString('hex');
// Reverse byte order for C# legacy UUID format (first 3 groups only).
const a =
hex.substring(6, 8) +
hex.substring(4, 6) +
hex.substring(2, 4) +
hex.substring(0, 2);
const b = hex.substring(10, 12) + hex.substring(8, 10);
const c = hex.substring(14, 16) + hex.substring(12, 14);
const d = hex.substring(16, 32);
const uuid = a + b + c + d;
return 'LegacyCSharpUUID("' + toUUIDWithHyphens(uuid) + '")';
};

const toLegacyPythonUUID = ({ value }: PropsByValueType<'Binary'>) => {
// Get the hex representation from the buffer.
const hex = Buffer.from(value.buffer).toString('hex');
return 'LegacyPythonUUID("' + toUUIDWithHyphens(hex) + '")';
};

// Binary sub_type 3.
const LegacyUUIDValue: React.FunctionComponent<PropsByValueType<'Binary'>> = (
bsonValue
) => {
const legacyUUIDDisplayEncoding = useLegacyUUIDDisplayContext();

const stringifiedValue = useMemo(() => {
// UUID must be exactly 16 bytes.
if (bsonValue.value.buffer.length === 16) {
try {
if (legacyUUIDDisplayEncoding === 'LegacyJavaUUID') {
return toLegacyJavaUUID(bsonValue);
} else if (legacyUUIDDisplayEncoding === 'LegacyCSharpUUID') {
return toLegacyCSharpUUID(bsonValue);
} else if (legacyUUIDDisplayEncoding === 'LegacyPythonUUID') {
return toLegacyPythonUUID(bsonValue);
}
} catch {
// Ignore errors and fallback to the raw representation.
// The UUID conversion can fail if the binary data is not a valid UUID.
}
}

// Raw, no encoding.
return `Binary.createFromBase64('${truncate(
bsonValue.value.toString('base64'),
100
)}', ${bsonValue.value.sub_type})`;
}, [legacyUUIDDisplayEncoding, bsonValue]);

return (
<BSONValueContainer type="Binary" title={stringifiedValue}>
{stringifiedValue}
</BSONValueContainer>
);
};

const BinaryValue: React.FunctionComponent<PropsByValueType<'Binary'>> = ({
value,
}) => {
Expand Down Expand Up @@ -242,7 +345,9 @@ const DateValue: React.FunctionComponent<PropsByValueType<'Date'>> = ({
};

const NumberValue: React.FunctionComponent<
PropsByValueType<'Int32' | 'Double'> & { type: 'Int32' | 'Double' }
PropsByValueType<'Int32' | 'Double' | 'Int64' | 'Decimal128'> & {
type: 'Int32' | 'Double' | 'Int64' | 'Decimal128';
}
> = ({ type, value }) => {
const stringifiedValue = useMemo(() => {
return String(value.valueOf());
Expand Down Expand Up @@ -377,6 +482,9 @@ const BSONValue: React.FunctionComponent<ValueProps> = (props) => {
case 'Date':
return <DateValue value={props.value}></DateValue>;
case 'Binary':
if (props.value.sub_type === Binary.SUBTYPE_UUID_OLD) {
return <LegacyUUIDValue value={props.value}></LegacyUUIDValue>;
}
return <BinaryValue value={props.value}></BinaryValue>;
case 'Int32':
case 'Double':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import {
} from './context-menu';
import { DrawerContentProvider } from './drawer-portal';
import { CopyPasteContextMenu } from '../hooks/use-copy-paste-context-menu';
import {
type LegacyUUIDDisplay,
LegacyUUIDDisplayContext,
} from './document-list/legacy-uuid-format-context';

type GuideCueProviderProps = React.ComponentProps<typeof GuideCueProvider>;

Expand All @@ -22,6 +26,7 @@ type CompassComponentsProviderProps = {
* value will be derived from the system settings
*/
darkMode?: boolean;
legacyUUIDDisplayEncoding?: LegacyUUIDDisplay;
popoverPortalContainer?: HTMLElement;
/**
* Either React children or a render callback that will get the darkMode
Expand Down Expand Up @@ -124,6 +129,7 @@ function useDarkMode(_darkMode?: boolean) {
export const CompassComponentsProvider = ({
darkMode: _darkMode,
children,
legacyUUIDDisplayEncoding,
onNextGuideGue,
onNextGuideCueGroup,
onContextMenuOpen,
Expand Down Expand Up @@ -161,45 +167,49 @@ export const CompassComponentsProvider = ({
darkMode={darkMode}
popoverPortalContainer={popoverPortalContainer}
>
<DrawerContentProvider
onDrawerSectionOpen={onDrawerSectionOpen}
onDrawerSectionHide={onDrawerSectionHide}
<LegacyUUIDDisplayContext.Provider
value={legacyUUIDDisplayEncoding ?? ''}
>
<StackedComponentProvider zIndex={stackedElementsZIndex}>
<RequiredURLSearchParamsProvider
utmSource={utmSource}
utmMedium={utmMedium}
>
<GuideCueProvider
onNext={onNextGuideGue}
onNextGroup={onNextGuideCueGroup}
disabled={disableGuideCues}
<DrawerContentProvider
onDrawerSectionOpen={onDrawerSectionOpen}
onDrawerSectionHide={onDrawerSectionHide}
>
<StackedComponentProvider zIndex={stackedElementsZIndex}>
<RequiredURLSearchParamsProvider
utmSource={utmSource}
utmMedium={utmMedium}
>
<SignalHooksProvider {...signalHooksProviderProps}>
<ConfirmationModalArea>
<ContextMenuProvider
disabled={disableContextMenus}
onContextMenuOpen={onContextMenuOpen}
onContextMenuItemClick={onContextMenuItemClick}
>
<CopyPasteContextMenu>
<ToastArea>
{typeof children === 'function'
? children({
darkMode,
portalContainerRef: setPortalContainer,
scrollContainerRef: setScrollContainer,
})
: children}
</ToastArea>
</CopyPasteContextMenu>
</ContextMenuProvider>
</ConfirmationModalArea>
</SignalHooksProvider>
</GuideCueProvider>
</RequiredURLSearchParamsProvider>
</StackedComponentProvider>
</DrawerContentProvider>
<GuideCueProvider
onNext={onNextGuideGue}
onNextGroup={onNextGuideCueGroup}
disabled={disableGuideCues}
>
<SignalHooksProvider {...signalHooksProviderProps}>
<ConfirmationModalArea>
<ContextMenuProvider
disabled={disableContextMenus}
onContextMenuOpen={onContextMenuOpen}
onContextMenuItemClick={onContextMenuItemClick}
>
<CopyPasteContextMenu>
<ToastArea>
{typeof children === 'function'
? children({
darkMode,
portalContainerRef: setPortalContainer,
scrollContainerRef: setScrollContainer,
})
: children}
</ToastArea>
</CopyPasteContextMenu>
</ContextMenuProvider>
</ConfirmationModalArea>
</SignalHooksProvider>
</GuideCueProvider>
</RequiredURLSearchParamsProvider>
</StackedComponentProvider>
</DrawerContentProvider>
</LegacyUUIDDisplayContext.Provider>
</LeafyGreenProvider>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createContext, useContext } from 'react';

export type LegacyUUIDDisplay =
| ''
| 'LegacyJavaUUID'
| 'LegacyCSharpUUID'
| 'LegacyPythonUUID';

export const LegacyUUIDDisplayContext = createContext<LegacyUUIDDisplay>('');

export function useLegacyUUIDDisplayContext(): LegacyUUIDDisplay {
return useContext(LegacyUUIDDisplayContext);
}
Loading
Loading