diff --git a/packages/compass-components/src/components/bson-value.spec.tsx b/packages/compass-components/src/components/bson-value.spec.tsx
index 38677522158..06bf64564bf 100644
--- a/packages/compass-components/src/components/bson-value.spec.tsx
+++ b/packages/compass-components/src/components/bson-value.spec.tsx
@@ -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);
@@ -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])),
@@ -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(
+
+
+
+ );
+
+ expect(container.querySelector('.element-value')?.textContent).to.include(
+ "Binary.createFromBase64('ASNFZ4mrze8BI0VniavN7w==', 3)"
+ );
+ });
+
+ it('should render Legacy UUID in Java format', function () {
+ const { container } = render(
+
+
+
+ );
+
+ 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(
+
+
+
+ );
+
+ 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(
+
+
+
+ );
+
+ 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(
+
+
+
+ );
+
+ 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(
+
+
+
+ );
+
+ 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();
+ });
+ });
+ });
});
diff --git a/packages/compass-components/src/components/bson-value.tsx b/packages/compass-components/src/components/bson-value.tsx
index efd7fc5241e..5e78c6f3c8d 100644
--- a/packages/compass-components/src/components/bson-value.tsx
+++ b/packages/compass-components/src/components/bson-value.tsx
@@ -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 =
| {
@@ -124,6 +125,108 @@ const ObjectIdValue: React.FunctionComponent> = ({
);
};
+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> = (
+ 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 (
+
+ {stringifiedValue}
+
+ );
+};
+
const BinaryValue: React.FunctionComponent> = ({
value,
}) => {
@@ -242,7 +345,9 @@ const DateValue: React.FunctionComponent> = ({
};
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());
@@ -377,6 +482,9 @@ const BSONValue: React.FunctionComponent = (props) => {
case 'Date':
return ;
case 'Binary':
+ if (props.value.sub_type === Binary.SUBTYPE_UUID_OLD) {
+ return ;
+ }
return ;
case 'Int32':
case 'Double':
diff --git a/packages/compass-components/src/components/compass-components-provider.tsx b/packages/compass-components/src/components/compass-components-provider.tsx
index a3fe6132a8d..e24472845a6 100644
--- a/packages/compass-components/src/components/compass-components-provider.tsx
+++ b/packages/compass-components/src/components/compass-components-provider.tsx
@@ -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;
@@ -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
@@ -124,6 +129,7 @@ function useDarkMode(_darkMode?: boolean) {
export const CompassComponentsProvider = ({
darkMode: _darkMode,
children,
+ legacyUUIDDisplayEncoding,
onNextGuideGue,
onNextGuideCueGroup,
onContextMenuOpen,
@@ -161,45 +167,49 @@ export const CompassComponentsProvider = ({
darkMode={darkMode}
popoverPortalContainer={popoverPortalContainer}
>
-
-
-
-
+
+
-
-
-
-
-
- {typeof children === 'function'
- ? children({
- darkMode,
- portalContainerRef: setPortalContainer,
- scrollContainerRef: setScrollContainer,
- })
- : children}
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+ {typeof children === 'function'
+ ? children({
+ darkMode,
+ portalContainerRef: setPortalContainer,
+ scrollContainerRef: setScrollContainer,
+ })
+ : children}
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/packages/compass-components/src/components/document-list/legacy-uuid-format-context.tsx b/packages/compass-components/src/components/document-list/legacy-uuid-format-context.tsx
new file mode 100644
index 00000000000..f45c0f21f8c
--- /dev/null
+++ b/packages/compass-components/src/components/document-list/legacy-uuid-format-context.tsx
@@ -0,0 +1,13 @@
+import { createContext, useContext } from 'react';
+
+export type LegacyUUIDDisplay =
+ | ''
+ | 'LegacyJavaUUID'
+ | 'LegacyCSharpUUID'
+ | 'LegacyPythonUUID';
+
+export const LegacyUUIDDisplayContext = createContext('');
+
+export function useLegacyUUIDDisplayContext(): LegacyUUIDDisplay {
+ return useContext(LegacyUUIDDisplayContext);
+}
diff --git a/packages/compass-preferences-model/src/preferences-schema.tsx b/packages/compass-preferences-model/src/preferences-schema.tsx
index 728e7707a14..49f4926d7c6 100644
--- a/packages/compass-preferences-model/src/preferences-schema.tsx
+++ b/packages/compass-preferences-model/src/preferences-schema.tsx
@@ -41,6 +41,14 @@ export const SORT_ORDER_VALUES = [
export type SORT_ORDERS = (typeof SORT_ORDER_VALUES)[number];
+export const LEGACY_UUID_ENCODINGS = [
+ '',
+ 'LegacyJavaUUID',
+ 'LegacyCSharpUUID',
+ 'LegacyPythonUUID',
+] as const;
+export type LEGACY_UUID_ENCODINGS = (typeof LEGACY_UUID_ENCODINGS)[number];
+
export type PermanentFeatureFlags = {
showDevFeatureFlags?: boolean;
enableDebugUseCsfleSchemaMap?: boolean;
@@ -72,6 +80,7 @@ export type UserConfigurablePreferences = PermanentFeatureFlags &
maxTimeMS?: number;
installURLHandlers: boolean;
protectConnectionStringsForNewConnections: boolean;
+ legacyUUIDDisplayEncoding: LEGACY_UUID_ENCODINGS;
// This preference is not a great fit for user preferences, but everything
// except for user preferences doesn't allow required preferences to be
// defined, so we are sticking it here
@@ -1101,6 +1110,42 @@ export const storedUserPreferencesProps: Required<{
type: 'number',
},
+ // There are a good amount of folks who still use the legacy UUID
+ // binary subtype 3, so we provide an option to control how those
+ // values are displayed in Compass.
+ legacyUUIDDisplayEncoding: {
+ ui: true,
+ cli: true,
+ global: true,
+ description: {
+ short: 'Encoding for Displaying Legacy UUID Values',
+ long: 'Select the encoding to be used when displaying legacy UUID of the binary subtype 3.',
+ options: {
+ '': {
+ label: 'Raw data (no encoding)',
+ description: 'Display legacy UUIDs as raw binary data',
+ },
+ LegacyJavaUUID: {
+ label: 'Legacy Java UUID',
+ description:
+ 'Display legacy UUIDs using Java UUID encoding. LegacyJavaUUID("UUID_STRING")',
+ },
+ LegacyCSharpUUID: {
+ label: 'Legacy C# UUID',
+ description:
+ 'Display legacy UUIDs using C# UUID encoding. LegacyCSharpUUID("UUID_STRING")',
+ },
+ LegacyPythonUUID: {
+ label: 'Legacy Python UUID',
+ description:
+ 'Display legacy UUIDs using Python UUID encoding. LegacyPythonUUID("UUID_STRING")',
+ },
+ },
+ },
+ validator: z.enum(LEGACY_UUID_ENCODINGS).default(''),
+ type: 'string',
+ },
+
...allFeatureFlagsProps,
};
diff --git a/packages/compass-preferences-model/src/provider.ts b/packages/compass-preferences-model/src/provider.ts
index 7fc9b4136c9..ed7f77e1ef2 100644
--- a/packages/compass-preferences-model/src/provider.ts
+++ b/packages/compass-preferences-model/src/provider.ts
@@ -11,7 +11,11 @@ export {
export { capMaxTimeMSAtPreferenceLimit } from './maxtimems';
export { FEATURE_FLAG_DEFINITIONS as featureFlags } from './feature-flags';
export type * from './feature-flags';
-export { getSettingDescription, SORT_ORDER_VALUES } from './preferences-schema';
+export {
+ getSettingDescription,
+ SORT_ORDER_VALUES,
+ LEGACY_UUID_ENCODINGS,
+} from './preferences-schema';
export type * from './preferences-schema';
export type { DevtoolsProxyOptions } from '@mongodb-js/devtools-proxy-support';
export type { ParsedGlobalPreferencesResult } from './global-config';
diff --git a/packages/compass-settings/src/components/settings/general.spec.tsx b/packages/compass-settings/src/components/settings/general.spec.tsx
index abbc9932e1f..38d7126e16a 100644
--- a/packages/compass-settings/src/components/settings/general.spec.tsx
+++ b/packages/compass-settings/src/components/settings/general.spec.tsx
@@ -66,6 +66,19 @@ describe('GeneralSettings', function () {
expect(getSettings()).to.have.property('defaultSortOrder', '{ _id: 1 }');
});
+ it('renders legacyUUIDDisplayEncoding', function () {
+ expect(within(container).getByTestId('legacyUUIDDisplayEncoding')).to.exist;
+ });
+
+ it('changes legacyUUIDDisplayEncoding value when selecting an option', function () {
+ within(container).getByTestId('legacyUUIDDisplayEncoding').click();
+ within(container).getByText('Legacy Java UUID').click();
+ expect(getSettings()).to.have.property(
+ 'legacyUUIDDisplayEncoding',
+ 'LegacyJavaUUID'
+ );
+ });
+
['maxTimeMS'].forEach((option) => {
it(`renders ${option}`, function () {
expect(within(container).getByTestId(option)).to.exist;
diff --git a/packages/compass-settings/src/components/settings/general.tsx b/packages/compass-settings/src/components/settings/general.tsx
index 5b9375c1c35..c09595e308a 100644
--- a/packages/compass-settings/src/components/settings/general.tsx
+++ b/packages/compass-settings/src/components/settings/general.tsx
@@ -15,6 +15,7 @@ const generalFields = [
'enableShowDialogOnQuit',
'enableDbAndCollStats',
'inferNamespacesFromPrivileges',
+ 'legacyUUIDDisplayEncoding',
] as const;
export const GeneralSettings: React.FunctionComponent = () => {
diff --git a/packages/compass-settings/src/components/settings/settings-list.tsx b/packages/compass-settings/src/components/settings/settings-list.tsx
index 6a8048caf41..afb5441f287 100644
--- a/packages/compass-settings/src/components/settings/settings-list.tsx
+++ b/packages/compass-settings/src/components/settings/settings-list.tsx
@@ -4,7 +4,10 @@ import {
getSettingDescription,
featureFlags,
} from 'compass-preferences-model/provider';
-import { SORT_ORDER_VALUES } from 'compass-preferences-model/provider';
+import {
+ SORT_ORDER_VALUES,
+ LEGACY_UUID_ENCODINGS,
+} from 'compass-preferences-model/provider';
import { settingStateLabels } from './state-labels';
import {
Checkbox,
@@ -22,6 +25,11 @@ import { changeFieldValue } from '../../stores/settings';
import type { RootState } from '../../stores';
import { connect } from 'react-redux';
+const ENUM_PREFERENCE_CONFIG = {
+ defaultSortOrder: SORT_ORDER_VALUES,
+ legacyUUIDDisplayEncoding: LEGACY_UUID_ENCODINGS,
+} as const;
+
type KeysMatching = keyof {
[P in keyof T as T[P] extends V ? P : never]: P;
};
@@ -38,6 +46,7 @@ type StringPreferences = KeysMatching<
UserConfigurablePreferences,
string | undefined
>;
+type StringEnumPreferences = keyof typeof ENUM_PREFERENCE_CONFIG;
type SupportedPreferences =
| BooleanPreferences
| NumericPreferences
@@ -163,7 +172,7 @@ function NumericSetting({
);
}
-function DefaultSortOrderSetting({
+function StringEnumSetting({
name,
onChange,
value,
@@ -175,6 +184,11 @@ function DefaultSortOrderSetting({
disabled: boolean;
}) {
const optionDescriptions = getSettingDescription(name).description.options;
+
+ if (!optionDescriptions) {
+ throw new Error(`No option descriptions found for preference ${name}`);
+ }
+
const onChangeCallback = useCallback(
(value: string) => {
onChange(name, value as UserConfigurablePreferences[PreferenceName]);
@@ -196,15 +210,9 @@ function DefaultSortOrderSetting({
onChange={onChangeCallback}
disabled={disabled}
>
- {SORT_ORDER_VALUES.map((option) => (
-
))}
@@ -271,6 +279,10 @@ type SettingsInputProps = AnySetting & {
required?: boolean;
};
+function isStringEnumPreference(name: string): name is StringEnumPreferences {
+ return name in ENUM_PREFERENCE_CONFIG;
+}
+
function isSupported(props: AnySetting): props is
| {
name: StringPreferences;
@@ -301,7 +313,9 @@ function SettingsInput({
}: SettingsInputProps): React.ReactElement {
if (!isSupported(props)) {
throw new Error(
- `Do not know how to render type ${props.type} for preference ${props.name}`
+ `Do not know how to render type ${String(props.type)} for preference ${
+ props.name
+ }`
);
}
@@ -318,9 +332,9 @@ function SettingsInput({
disabled={!!disabled}
/>
);
- } else if (type === 'string' && name === 'defaultSortOrder') {
+ } else if (type === 'string' && isStringEnumPreference(name)) {
input = (
- = ({ darkMode, children }) => {
const track = useTelemetry();
- const { enableContextMenus, enableGuideCues } = usePreferences([
- 'enableContextMenus',
- 'enableGuideCues',
- ]);
+ const { enableContextMenus, enableGuideCues, legacyUUIDDisplayEncoding } =
+ usePreferences([
+ 'enableContextMenus',
+ 'enableGuideCues',
+ 'legacyUUIDDisplayEncoding',
+ ]);
return (
{
const track = useTelemetry();
- const { enableContextMenus, showedNetworkOptIn, enableGuideCues } =
- usePreferences([
- 'enableContextMenus',
- 'showedNetworkOptIn',
- 'enableGuideCues',
- ]);
+ const {
+ enableContextMenus,
+ legacyUUIDDisplayEncoding,
+ showedNetworkOptIn,
+ enableGuideCues,
+ } = usePreferences([
+ 'enableContextMenus',
+ 'legacyUUIDDisplayEncoding',
+ 'showedNetworkOptIn',
+ 'enableGuideCues',
+ ]);
return (
{
track('Guide Cue Dismissed', {
groupId: cue.groupId,