From 7d221a2fd943d32566f1852be7e84ed355c57d15 Mon Sep 17 00:00:00 2001 From: Rhys Howell Date: Wed, 3 Dec 2025 10:12:46 -0800 Subject: [PATCH 1/4] feat(settings, components): add a preference for showing legacy UUIDs in various encodings COMPASS-9690 --- .../src/components/bson-value.spec.tsx | 111 +++++++++++++++++ .../src/components/bson-value.tsx | 117 +++++++++++++++++- .../compass-components-provider.tsx | 84 +++++++------ .../legacy-uuid-format-context.tsx | 13 ++ .../src/preferences-schema.tsx | 45 +++++++ .../compass-preferences-model/src/provider.ts | 6 +- .../src/components/settings/general.spec.tsx | 13 ++ .../src/components/settings/general.tsx | 1 + .../src/components/settings/settings-list.tsx | 42 ++++--- packages/compass-web/src/entrypoint.tsx | 11 +- packages/compass/src/app/components/home.tsx | 18 ++- packages/hadron-document/src/element.ts | 1 + .../hadron-type-checker/src/type-checker.ts | 5 + 13 files changed, 404 insertions(+), 63 deletions(-) create mode 100644 packages/compass-components/src/components/document-list/legacy-uuid-format-context.tsx diff --git a/packages/compass-components/src/components/bson-value.spec.tsx b/packages/compass-components/src/components/bson-value.spec.tsx index 38677522158..5bd81d5ec79 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("0123456789abcdef0123456789abcdef")' + ); + }); + + 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 bc0f8585357..fff6a2c371e 100644 --- a/packages/compass-components/src/components/bson-value.tsx +++ b/packages/compass-components/src/components/bson-value.tsx @@ -8,6 +8,7 @@ import { Icon, Link } from './leafygreen'; import { spacing } from '@leafygreen-ui/tokens'; import { css, cx } from '@leafygreen-ui/emotion'; import { Theme, useDarkMode } from '../hooks/use-theme'; +import { useLegacyUUIDDisplayContext } from './document-list/legacy-uuid-format-context'; type ValueProps = | { @@ -123,6 +124,115 @@ const ObjectIdValue: React.FunctionComponent> = ({ ); }; +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 reversed = msb + lsb; + const uuid = + reversed.substring(0, 8) + + '-' + + reversed.substring(8, 12) + + '-' + + reversed.substring(12, 16) + + '-' + + reversed.substring(16, 20) + + '-' + + reversed.substring(20, 32); + return 'LegacyJavaUUID("' + 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 reversed = a + b + c + d; + const uuid = + reversed.substring(0, 8) + + '-' + + reversed.substring(8, 12) + + '-' + + reversed.substring(12, 16) + + '-' + + reversed.substring(16, 20) + + '-' + + reversed.substring(20, 32); + return 'LegacyCSharpUUID("' + uuid + '")'; +}; + +const toLegacyPythonUUID = ({ value }: PropsByValueType<'Binary'>) => { + // Get the hex representation from the buffer. + const hex = Buffer.from(value.buffer).toString('hex'); + // Python format uses the raw hex like UUID in subtype 4. + return 'LegacyPythonUUID("' + 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, }) => { @@ -241,7 +351,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()); @@ -376,6 +488,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 (