diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/constants.ts b/packages/compass-collection/src/components/mock-data-generator-modal/constants.ts index 10c3c707087..9d4ad07fe29 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/constants.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/constants.ts @@ -1,3 +1,4 @@ +import type { MongoDBFieldType } from '@mongodb-js/compass-generative-ai'; import { MockDataGeneratorStep } from './types'; export const StepButtonLabelMap = { @@ -10,3 +11,72 @@ export const StepButtonLabelMap = { export const DEFAULT_DOCUMENT_COUNT = 1000; export const MAX_DOCUMENT_COUNT = 100000; + +/** + * Map of MongoDB types to available Faker methods. + * Not all Faker methods are included here. + * More can be found in the Faker.js API: https://fakerjs.dev/api/ + */ +export const MONGO_TYPE_TO_FAKER_METHODS: Record< + MongoDBFieldType, + Array +> = { + String: [ + 'lorem.word', + 'lorem.words', + 'lorem.sentence', + 'lorem.paragraph', + 'person.firstName', + 'person.lastName', + 'person.fullName', + 'person.jobTitle', + 'internet.email', + 'internet.url', + 'internet.domainName', + 'internet.userName', + 'phone.number', + 'location.city', + 'location.country', + 'location.streetAddress', + 'location.zipCode', + 'location.state', + 'company.name', + 'company.catchPhrase', + 'color.human', + 'commerce.productName', + 'commerce.department', + 'finance.accountName', + 'finance.currencyCode', + 'git.commitSha', + 'string.uuid', + 'string.alpha', + 'string.alphanumeric', + ], + Number: [ + 'number.int', + 'number.float', + 'finance.amount', + 'location.latitude', + 'location.longitude', + ], + Int32: ['number.int', 'finance.amount'], + Long: ['number.int', 'number.bigInt'], + Decimal128: ['number.float', 'finance.amount'], + Boolean: ['datatype.boolean'], + Date: [ + 'date.recent', + 'date.past', + 'date.future', + 'date.anytime', + 'date.birthdate', + ], + Timestamp: ['date.recent', 'date.past', 'date.future', 'date.anytime'], + ObjectId: ['database.mongodbObjectId'], + Binary: ['string.hexadecimal', 'string.binary'], + RegExp: ['lorem.word', 'string.alpha'], + Code: ['lorem.sentence', 'lorem.paragraph', 'git.commitMessage'], + MinKey: ['number.int'], + MaxKey: ['number.int'], + Symbol: ['lorem.word', 'string.symbol'], + DBRef: ['database.mongodbObjectId'], +}; diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/faker-mapping-selector.spec.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/faker-mapping-selector.spec.tsx new file mode 100644 index 00000000000..5b455ba74a3 --- /dev/null +++ b/packages/compass-collection/src/components/mock-data-generator-modal/faker-mapping-selector.spec.tsx @@ -0,0 +1,152 @@ +import { expect } from 'chai'; +import React from 'react'; +import { + screen, + render, + cleanup, + waitFor, + userEvent, +} from '@mongodb-js/testing-library-compass'; +import sinon from 'sinon'; +import FakerMappingSelector from './faker-mapping-selector'; +import { UNRECOGNIZED_FAKER_METHOD } from '../../modules/collection-tab'; +import { MONGO_TYPE_TO_FAKER_METHODS } from './constants'; +import { MongoDBFieldTypeValues } from '@mongodb-js/compass-generative-ai'; + +const mockActiveJsonType = MongoDBFieldTypeValues.String; +const mockActiveFakerFunction = 'lorem.word'; +const onJsonTypeSelectStub = sinon.stub(); +const onFakerFunctionSelectStub = sinon.stub(); + +describe('FakerMappingSelector', () => { + afterEach(() => { + cleanup(); + }); + + it('should display all MongoDB types in the dropdown', async () => { + // Check that all MongoDB types from the constant are present + const mongoTypes = Object.keys(MongoDBFieldTypeValues); + + render( + + ); + + const jsonTypeSelect = screen.getByLabelText('JSON Type'); + userEvent.click(jsonTypeSelect); + + for (const type of mongoTypes) { + await waitFor(() => { + expect(screen.getByRole('option', { name: type })).to.exist; + }); + } + }); + + describe('should display faker methods for each MongoDB type', () => { + for (const [mongoType, methods] of Object.entries( + MONGO_TYPE_TO_FAKER_METHODS + )) { + it(`should display faker methods for ${mongoType}`, () => { + const firstMethod = methods[0]; + + render( + + ); + + const fakerFunctionSelect = screen.getByLabelText('Faker Function'); + userEvent.click(fakerFunctionSelect); + + methods.forEach((method) => { + expect(screen.getByRole('option', { name: method })).to.exist; + }); + }); + } + }); + + it('should call onJsonTypeSelect when MongoDB type changes', async () => { + render( + + ); + + const jsonTypeSelect = screen.getByLabelText('JSON Type'); + userEvent.click(jsonTypeSelect); + + const numberOption = await screen.findByRole('option', { name: 'Number' }); + userEvent.click(numberOption); + + expect(onJsonTypeSelectStub).to.have.been.calledOnceWith('Number'); + }); + + it('should call onFakerFunctionSelect when faker function changes', async () => { + render( + + ); + + const fakerFunctionSelect = screen.getByLabelText('Faker Function'); + userEvent.click(fakerFunctionSelect); + + const emailOption = await screen.findByRole('option', { + name: 'internet.email', + }); + userEvent.click(emailOption); + + expect(onFakerFunctionSelectStub).to.have.been.calledOnceWith( + 'internet.email' + ); + }); + + it('should show warning banner when faker method is unrecognized', () => { + render( + + ); + + expect( + screen.getByText( + /Please select a function or we will default fill this field/ + ) + ).to.exist; + }); + + it('should not show warning banner when faker method is recognized', () => { + render( + + ); + + expect( + screen.queryByText( + /Please select a function or we will default fill this field/ + ) + ).to.not.exist; + }); +}); diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/faker-mapping-selector.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/faker-mapping-selector.tsx index b6c1aa726b0..38868371a81 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/faker-mapping-selector.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/faker-mapping-selector.tsx @@ -8,9 +8,11 @@ import { Select, spacing, } from '@mongodb-js/compass-components'; -import React from 'react'; +import React, { useMemo } from 'react'; import { UNRECOGNIZED_FAKER_METHOD } from '../../modules/collection-tab'; import type { MongoDBFieldType } from '@mongodb-js/compass-generative-ai'; +import { MongoDBFieldTypeValues } from '@mongodb-js/compass-generative-ai'; +import { MONGO_TYPE_TO_FAKER_METHODS } from './constants'; const fieldMappingSelectorsStyles = css({ width: '50%', @@ -25,7 +27,7 @@ const labelStyles = css({ }); interface Props { - activeJsonType: string; + activeJsonType: MongoDBFieldType; activeFakerFunction: string; onJsonTypeSelect: (jsonType: MongoDBFieldType) => void; onFakerFunctionSelect: (fakerFunction: string) => void; @@ -37,6 +39,16 @@ const FakerMappingSelector = ({ onJsonTypeSelect, onFakerFunctionSelect, }: Props) => { + const fakerMethodOptions = useMemo(() => { + const methods = MONGO_TYPE_TO_FAKER_METHODS[activeJsonType] || []; + + if (methods.includes(activeFakerFunction)) { + return methods; + } + + return [activeFakerFunction, ...methods]; + }, [activeJsonType, activeFakerFunction]); + return (
Mapping @@ -46,8 +58,7 @@ const FakerMappingSelector = ({ value={activeJsonType} onChange={(value) => onJsonTypeSelect(value as MongoDBFieldType)} > - {/* TODO(CLOUDP-344400) : Make the select input editable and render other options depending on the JSON type selected */} - {[activeJsonType].map((type) => ( + {Object.values(MongoDBFieldTypeValues).map((type) => ( @@ -59,10 +70,9 @@ const FakerMappingSelector = ({ value={activeFakerFunction} onChange={onFakerFunctionSelect} > - {/* TODO(CLOUDP-344400): Make the select input editable and render other JSON types */} - {[activeFakerFunction].map((field) => ( - ))} diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/faker-schema-editor-screen.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/faker-schema-editor-screen.tsx index 39b6a5d7a27..f5b24852699 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/faker-schema-editor-screen.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/faker-schema-editor-screen.tsx @@ -12,8 +12,13 @@ import { import React from 'react'; import FieldSelector from './schema-field-selector'; import FakerMappingSelector from './faker-mapping-selector'; -import type { FakerSchema, MockDataGeneratorState } from './types'; +import type { + FakerSchema, + FakerFieldMapping, + MockDataGeneratorState, +} from './types'; import type { MongoDBFieldType } from '@mongodb-js/compass-generative-ai'; +import { getDefaultFakerMethod } from './script-generation-utils'; const containerStyles = css({ display: 'flex', @@ -59,6 +64,20 @@ const FakerSchemaEditorContent = ({ const [fakerSchemaFormValues, setFakerSchemaFormValues] = React.useState(fakerSchema); + // Store original LLM mappings to restore when reselecting original methods + const originalLlmMappings = React.useRef>( + Object.fromEntries( + Object.entries(fakerSchema).map(([field, mapping]) => [ + field, + { + mongoType: mapping.mongoType, + fakerMethod: mapping.fakerMethod, + fakerArgs: mapping.fakerArgs, + }, + ]) + ) + ); + const fieldPaths = Object.keys(fakerSchemaFormValues); const [activeField, setActiveField] = React.useState(fieldPaths[0]); @@ -71,13 +90,25 @@ const FakerSchemaEditorContent = ({ const onJsonTypeSelect = (newJsonType: MongoDBFieldType) => { const currentMapping = fakerSchemaFormValues[activeField]; + const originalLlmMapping = originalLlmMappings.current[activeField]; + if (currentMapping) { + const isSwitchingToOriginalType = + originalLlmMapping && newJsonType === originalLlmMapping.mongoType; + + const newMapping = + isSwitchingToOriginalType && originalLlmMapping + ? { ...originalLlmMapping } + : { + ...currentMapping, + mongoType: newJsonType, + fakerMethod: getDefaultFakerMethod(newJsonType), + fakerArgs: [], + }; + setFakerSchemaFormValues({ ...fakerSchemaFormValues, - [activeField]: { - ...currentMapping, - mongoType: newJsonType, - }, + [activeField]: newMapping, }); resetIsSchemaConfirmed(); } @@ -85,13 +116,26 @@ const FakerSchemaEditorContent = ({ const onFakerFunctionSelect = (newFakerFunction: string) => { const currentMapping = fakerSchemaFormValues[activeField]; + const originalLlmMapping = originalLlmMappings.current[activeField]; + if (currentMapping) { + const isSwitchingToLlmSuggestion = + originalLlmMapping && + currentMapping.mongoType === originalLlmMapping.mongoType && + newFakerFunction === originalLlmMapping.fakerMethod; + + const newMapping = + isSwitchingToLlmSuggestion && originalLlmMapping + ? { ...originalLlmMapping } + : { + ...currentMapping, + fakerMethod: newFakerFunction, + fakerArgs: [], + }; + setFakerSchemaFormValues({ ...fakerSchemaFormValues, - [activeField]: { - ...currentMapping, - fakerMethod: newFakerFunction, - }, + [activeField]: newMapping, }); resetIsSchemaConfirmed(); } diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/raw-schema-confirmation-screen.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/raw-schema-confirmation-screen.tsx index e55182e192c..788e09ea23c 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/raw-schema-confirmation-screen.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/raw-schema-confirmation-screen.tsx @@ -9,6 +9,8 @@ import { BannerVariant, Body, DocumentList, + useDarkMode, + cx, } from '@mongodb-js/compass-components'; import { usePreference } from 'compass-preferences-model/provider'; @@ -28,6 +30,11 @@ const documentContainerStyles = css({ border: `1px solid ${palette.gray.light2}`, borderRadius: spacing[400], }); +const documentContainerDarkStyles = css({ + backgroundColor: palette.gray.dark3, + border: `1px solid ${palette.gray.dark2}`, + borderRadius: spacing[400], +}); const documentStyles = css({ padding: `${spacing[400]}px ${spacing[900]}px`, @@ -52,6 +59,7 @@ const RawSchemaConfirmationScreen = ({ const enableSampleDocumentPassing = usePreference( 'enableGenAISampleDocumentPassing' ); + const isDarkMode = useDarkMode(); const subtitleText = enableSampleDocumentPassing ? 'Sample Documents Collected' @@ -69,7 +77,12 @@ const RawSchemaConfirmationScreen = ({ {subtitleText} {descriptionText} -
+
{ const schema = { unknownField: { mongoType: 'String' as const, - fakerMethod: 'unrecognized', + fakerMethod: UNRECOGNIZED_FAKER_METHOD, fakerArgs: [], }, }; @@ -803,32 +804,32 @@ describe('Script Generation', () => { const schema = { unknownNumber: { mongoType: 'Number' as const, - fakerMethod: 'unrecognized', + fakerMethod: UNRECOGNIZED_FAKER_METHOD, fakerArgs: [], }, unknownInt: { mongoType: 'Int32' as const, - fakerMethod: 'unrecognized', + fakerMethod: UNRECOGNIZED_FAKER_METHOD, fakerArgs: [], }, unknownInt32: { mongoType: 'Int32' as const, - fakerMethod: 'unrecognized', + fakerMethod: UNRECOGNIZED_FAKER_METHOD, fakerArgs: [], }, unknownInt64: { mongoType: 'Long' as const, - fakerMethod: 'unrecognized', + fakerMethod: UNRECOGNIZED_FAKER_METHOD, fakerArgs: [], }, unknownLong: { mongoType: 'Long' as const, - fakerMethod: 'unrecognized', + fakerMethod: UNRECOGNIZED_FAKER_METHOD, fakerArgs: [], }, unknownDecimal128: { mongoType: 'Decimal128' as const, - fakerMethod: 'unrecognized', + fakerMethod: UNRECOGNIZED_FAKER_METHOD, fakerArgs: [], }, }; @@ -879,7 +880,7 @@ describe('Script Generation', () => { const schema = { unknownDate: { mongoType: 'Date' as const, - fakerMethod: 'unrecognized', + fakerMethod: UNRECOGNIZED_FAKER_METHOD, fakerArgs: [], }, }; @@ -903,7 +904,7 @@ describe('Script Generation', () => { const schema = { unknownBool: { mongoType: 'Boolean' as const, - fakerMethod: 'unrecognized', + fakerMethod: UNRECOGNIZED_FAKER_METHOD, fakerArgs: [], }, }; @@ -927,7 +928,7 @@ describe('Script Generation', () => { const schema = { unknownId: { mongoType: 'ObjectId' as const, - fakerMethod: 'unrecognized', + fakerMethod: UNRECOGNIZED_FAKER_METHOD, fakerArgs: [], }, }; @@ -951,7 +952,7 @@ describe('Script Generation', () => { const schema = { unknownType: { mongoType: 'String' as const, - fakerMethod: 'unrecognized', + fakerMethod: UNRECOGNIZED_FAKER_METHOD, fakerArgs: [], }, }; @@ -975,7 +976,7 @@ describe('Script Generation', () => { const schema = { timestampField: { mongoType: 'Timestamp' as const, - fakerMethod: 'unrecognized', + fakerMethod: UNRECOGNIZED_FAKER_METHOD, fakerArgs: [], }, }; @@ -999,7 +1000,7 @@ describe('Script Generation', () => { const schema = { regexField: { mongoType: 'RegExp' as const, - fakerMethod: 'unrecognized', + fakerMethod: UNRECOGNIZED_FAKER_METHOD, fakerArgs: [], }, }; @@ -1023,7 +1024,7 @@ describe('Script Generation', () => { const schema = { jsField: { mongoType: 'Code' as const, - fakerMethod: 'unrecognized', + fakerMethod: UNRECOGNIZED_FAKER_METHOD, fakerArgs: [], }, }; @@ -1399,7 +1400,7 @@ describe('Script Generation', () => { const schema = { unknownField: { mongoType: 'String' as const, - fakerMethod: 'unrecognized', + fakerMethod: UNRECOGNIZED_FAKER_METHOD, fakerArgs: [], probability: 0.5, }, diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts index 15687a4b76d..46424fcc9cb 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts @@ -1,6 +1,7 @@ import type { MongoDBFieldType } from '@mongodb-js/compass-generative-ai'; import type { FakerFieldMapping } from './types'; import { prettify } from '@mongodb-js/compass-editor'; +import { UNRECOGNIZED_FAKER_METHOD } from '../../modules/collection-tab'; export type FakerArg = string | number | boolean | { json: string }; @@ -446,7 +447,7 @@ function renderArrayCode( */ function generateFakerCall(mapping: FakerFieldMapping): string { const method = - mapping.fakerMethod === 'unrecognized' + mapping.fakerMethod === UNRECOGNIZED_FAKER_METHOD ? getDefaultFakerMethod(mapping.mongoType) : mapping.fakerMethod; diff --git a/packages/compass-generative-ai/src/atlas-ai-service.ts b/packages/compass-generative-ai/src/atlas-ai-service.ts index a490d5119a1..cb19dae3d1a 100644 --- a/packages/compass-generative-ai/src/atlas-ai-service.ts +++ b/packages/compass-generative-ai/src/atlas-ai-service.ts @@ -235,8 +235,7 @@ export interface MockDataSchemaRequest { */ export type MongoDBFieldType = PrimitiveSchemaType['name']; -// TODO(CLOUDP-346699): Export this from mongodb-schema -enum MongoDBFieldTypeValues { +export enum MongoDBFieldTypeValues { String = 'String', Number = 'Number', Boolean = 'Boolean', diff --git a/packages/compass-generative-ai/src/index.ts b/packages/compass-generative-ai/src/index.ts index 26218310a07..0089fe592d8 100644 --- a/packages/compass-generative-ai/src/index.ts +++ b/packages/compass-generative-ai/src/index.ts @@ -37,3 +37,5 @@ export type { MockDataSchemaResponse, MongoDBFieldType, } from './atlas-ai-service'; + +export { MongoDBFieldTypeValues } from './atlas-ai-service';