diff --git a/package-lock.json b/package-lock.json index 1a10f197185..dc29161bea3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6732,6 +6732,28 @@ "@leafygreen-ui/leafygreen-provider": "^4.0.2" } }, + "node_modules/@leafygreen-ui/copyable": { + "version": "10.0.14", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/copyable/-/copyable-10.0.14.tgz", + "integrity": "sha512-O4dstObiN04Zjrd4Z10ratWZAi7pnb6gpML/HQnkAxR+0OwzKOvrR6XOQ2/3IzlLfIiY1TUHIbjavpHy/ppqVw==", + "license": "Apache-2.0", + "dependencies": { + "@leafygreen-ui/button": "^23.1.6", + "@leafygreen-ui/emotion": "^4.1.1", + "@leafygreen-ui/hooks": "^8.4.1", + "@leafygreen-ui/icon": "^13.4.0", + "@leafygreen-ui/lib": "^14.2.0", + "@leafygreen-ui/palette": "^4.1.4", + "@leafygreen-ui/tokens": "^2.12.2", + "@leafygreen-ui/tooltip": "^13.0.13", + "@leafygreen-ui/typography": "^20.1.9", + "clipboard": "^2.0.6", + "polished": "^4.2.2" + }, + "peerDependencies": { + "@leafygreen-ui/leafygreen-provider": "^4.0.7" + } + }, "node_modules/@leafygreen-ui/descendants": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@leafygreen-ui/descendants/-/descendants-2.1.5.tgz", @@ -47813,6 +47835,7 @@ "@leafygreen-ui/code": "^16.0.2", "@leafygreen-ui/combobox": "^11.0.2", "@leafygreen-ui/confirmation-modal": "^6.0.2", + "@leafygreen-ui/copyable": "^10.0.14", "@leafygreen-ui/descendants": "^2.1.0", "@leafygreen-ui/emotion": "^4.0.9", "@leafygreen-ui/guide-cue": "^7.0.2", @@ -58479,6 +58502,24 @@ "@leafygreen-ui/typography": "^20.0.2" } }, + "@leafygreen-ui/copyable": { + "version": "10.0.14", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/copyable/-/copyable-10.0.14.tgz", + "integrity": "sha512-O4dstObiN04Zjrd4Z10ratWZAi7pnb6gpML/HQnkAxR+0OwzKOvrR6XOQ2/3IzlLfIiY1TUHIbjavpHy/ppqVw==", + "requires": { + "@leafygreen-ui/button": "^22.0.2", + "@leafygreen-ui/emotion": "^4.0.9", + "@leafygreen-ui/hooks": "^8.3.4", + "@leafygreen-ui/icon": "^13.1.2", + "@leafygreen-ui/lib": "^15.2.0", + "@leafygreen-ui/palette": "^4.1.3", + "@leafygreen-ui/tokens": "^2.11.3", + "@leafygreen-ui/tooltip": "^13.0.13", + "@leafygreen-ui/typography": "^20.0.2", + "clipboard": "^2.0.6", + "polished": "^4.2.2" + } + }, "@leafygreen-ui/descendants": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@leafygreen-ui/descendants/-/descendants-2.1.5.tgz", @@ -61267,6 +61308,7 @@ "@leafygreen-ui/code": "^16.0.2", "@leafygreen-ui/combobox": "^11.0.2", "@leafygreen-ui/confirmation-modal": "^6.0.2", + "@leafygreen-ui/copyable": "^10.0.14", "@leafygreen-ui/descendants": "^2.1.0", "@leafygreen-ui/emotion": "^4.0.9", "@leafygreen-ui/guide-cue": "^7.0.2", diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx index 9d0dd164906..ddd38a9fdd7 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx @@ -2,7 +2,7 @@ import { expect } from 'chai'; import React from 'react'; import { screen, - render, + renderWithActiveConnection, waitFor, userEvent, } from '@mongodb-js/testing-library-compass'; @@ -14,12 +14,19 @@ import { MockDataGeneratorStep } from './types'; import { StepButtonLabelMap } from './constants'; import type { CollectionState } from '../../modules/collection-tab'; import { default as collectionTabReducer } from '../../modules/collection-tab'; +import type { ConnectionInfo } from '@mongodb-js/connection-info'; describe('MockDataGeneratorModal', () => { - function renderModal({ + async function renderModal({ isOpen = true, currentStep = MockDataGeneratorStep.SCHEMA_CONFIRMATION, mockServices = createMockServices(), + connectionInfo, + }: { + isOpen?: boolean; + currentStep?: MockDataGeneratorStep; + mockServices?: any; + connectionInfo?: ConnectionInfo; } = {}) { const initialState: CollectionState = { workspaceTabId: 'test-workspace-tab-id', @@ -52,10 +59,11 @@ describe('MockDataGeneratorModal', () => { applyMiddleware(thunk.withExtraArgument(mockServices)) ); - return render( + return await renderWithActiveConnection( - + , + connectionInfo ); } @@ -89,20 +97,20 @@ describe('MockDataGeneratorModal', () => { } describe('generally', () => { - it('renders the modal when isOpen is true', () => { - renderModal(); + it('renders the modal when isOpen is true', async () => { + await renderModal(); expect(screen.getByTestId('generate-mock-data-modal')).to.exist; }); - it('does not render the modal when isOpen is false', () => { - renderModal({ isOpen: false }); + it('does not render the modal when isOpen is false', async () => { + await renderModal({ isOpen: false }); expect(screen.queryByTestId('generate-mock-data-modal')).to.not.exist; }); it('closes the modal when the close button is clicked', async () => { - renderModal(); + await renderModal(); expect(screen.getByTestId('generate-mock-data-modal')).to.exist; userEvent.click(screen.getByLabelText('Close modal')); @@ -113,7 +121,7 @@ describe('MockDataGeneratorModal', () => { }); it('closes the modal when the cancel button is clicked', async () => { - renderModal(); + await renderModal(); expect(screen.getByTestId('generate-mock-data-modal')).to.exist; userEvent.click(screen.getByText('Cancel')); @@ -154,7 +162,7 @@ describe('MockDataGeneratorModal', () => { it('cancels in-flight faker mapping requests when the cancel button is clicked', async () => { const mockServices = createMockServicesWithSlowAiRequest(); - renderModal({ mockServices: mockServices as any }); + await renderModal({ mockServices: mockServices as any }); expect(screen.getByTestId('raw-schema-confirmation')).to.exist; userEvent.click(screen.getByText('Confirm')); @@ -170,7 +178,7 @@ describe('MockDataGeneratorModal', () => { it('cancels in-flight faker mapping requests when the back button is clicked after schema confirmation', async () => { const mockServices = createMockServicesWithSlowAiRequest(); - renderModal({ mockServices: mockServices as any }); + await renderModal({ mockServices: mockServices as any }); expect(screen.getByTestId('raw-schema-confirmation')).to.exist; userEvent.click(screen.getByText('Confirm')); @@ -186,8 +194,8 @@ describe('MockDataGeneratorModal', () => { }); describe('on the schema confirmation step', () => { - it('disables the Back button', () => { - renderModal(); + it('disables the Back button', async () => { + await renderModal(); expect( screen @@ -197,7 +205,7 @@ describe('MockDataGeneratorModal', () => { }); it('renders the faker schema editor when the confirm button is clicked', async () => { - renderModal(); + await renderModal(); expect(screen.getByTestId('raw-schema-confirmation')).to.exist; expect(screen.queryByTestId('faker-schema-editor')).to.not.exist; @@ -212,7 +220,7 @@ describe('MockDataGeneratorModal', () => { const mockServices = createMockServices(); mockServices.atlasAiService.getMockDataSchema = () => Promise.reject('faker schema generation failed'); - renderModal({ mockServices }); + await renderModal({ mockServices }); expect(screen.getByTestId('raw-schema-confirmation')).to.exist; expect(screen.queryByTestId('faker-schema-editor')).to.not.exist; @@ -228,6 +236,92 @@ describe('MockDataGeneratorModal', () => { // todo: assert that closing then re-opening the modal after an LLM err removes the err message }); + describe('on the generate data step', () => { + it('enables the Back button', async () => { + await renderModal({ currentStep: MockDataGeneratorStep.GENERATE_DATA }); + + expect( + screen + .getByRole('button', { name: 'Back' }) + .getAttribute('aria-disabled') + ).to.not.equal('true'); + }); + + it('renders the main sections: Prerequisites, steps, and Resources', async () => { + await renderModal({ currentStep: MockDataGeneratorStep.GENERATE_DATA }); + + expect(screen.getByText('Prerequisites')).to.exist; + expect(screen.getByText('1. Create a .js file with the following script')) + .to.exist; + expect(screen.getByText('2. Run the script with')).to.exist; + expect(screen.getByText('Resources')).to.exist; + }); + + it('closes the modal when the Done button is clicked', async () => { + await renderModal({ currentStep: MockDataGeneratorStep.GENERATE_DATA }); + + expect(screen.getByTestId('generate-mock-data-modal')).to.exist; + userEvent.click(screen.getByText('Done')); + await waitFor( + () => + expect(screen.queryByTestId('generate-mock-data-modal')).to.not.exist + ); + }); + + it('renders the Database Users link with correct URL when projectId is available', async () => { + const atlasConnectionInfo: ConnectionInfo = { + id: 'test-atlas-connection', + connectionOptions: { connectionString: 'mongodb://localhost:27017' }, + atlasMetadata: { + orgId: 'test-org', + projectId: 'test-project-123', + clusterName: 'test-cluster', + clusterUniqueId: 'test-cluster-unique-id', + clusterType: 'REPLICASET' as const, + clusterState: 'IDLE' as const, + metricsId: 'test-metrics-id', + metricsType: 'replicaSet' as const, + regionalBaseUrl: null, + instanceSize: 'M10', + supports: { + globalWrites: false, + rollingIndexes: true, + }, + }, + }; + + await renderModal({ + currentStep: MockDataGeneratorStep.GENERATE_DATA, + connectionInfo: atlasConnectionInfo, + }); + + const databaseUsersLink = screen.getByRole('link', { + name: 'Access your Database Users', + }); + expect(databaseUsersLink.getAttribute('href')).to.equal( + '/v2/test-project-123#/security/database/users' + ); + }); + + it('does not render the Database Users link when projectId is not available', async () => { + const nonAtlasConnectionInfo: ConnectionInfo = { + id: 'test-local-connection', + connectionOptions: { connectionString: 'mongodb://localhost:27017' }, + // No atlasMetadata means no projectId + }; + + await renderModal({ + currentStep: MockDataGeneratorStep.GENERATE_DATA, + connectionInfo: nonAtlasConnectionInfo, + }); + + expect(screen.queryByRole('link', { name: 'Access your Database Users' })) + .to.not.exist; + }); + + // todo: assert that the generated script is displayed in the code block + }); + describe('when rendering the modal in a specific step', () => { const steps = Object.keys( StepButtonLabelMap @@ -235,8 +329,8 @@ describe('MockDataGeneratorModal', () => { // note: these tests can be removed after every modal step is implemented steps.forEach((currentStep) => { - it(`renders the button with the correct label when the user is in step "${currentStep}"`, () => { - renderModal({ currentStep }); + it(`renders the button with the correct label when the user is in step "${currentStep}"`, async () => { + await renderModal({ currentStep }); expect(screen.getByTestId('next-step-button')).to.have.text( StepButtonLabelMap[currentStep] ); diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.tsx index d09861a6b42..eadb8b13e19 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.tsx @@ -23,6 +23,15 @@ import { } from '../../modules/collection-tab'; import { default as SchemaConfirmationScreen } from './raw-schema-confirmation'; import FakerSchemaEditor from './faker-schema-editor'; +import ScriptScreen from './script-screen'; + +const STEP_TO_STEP_CONTENT: Record = { + [MockDataGeneratorStep.SCHEMA_CONFIRMATION]: , + [MockDataGeneratorStep.SCHEMA_EDITOR]: , + [MockDataGeneratorStep.DOCUMENT_COUNT]: <>>, // TODO: Implement as part of CLOUDP-333856 + [MockDataGeneratorStep.PREVIEW_DATA]: <>>, // TODO: Implement as part of CLOUDP-333857 + [MockDataGeneratorStep.GENERATE_DATA]: , +}; const footerStyles = css` flex-direction: row; @@ -62,18 +71,9 @@ const MockDataGeneratorModal = ({ } }; - let stepContent: React.ReactNode; - - if (currentStep === MockDataGeneratorStep.SCHEMA_CONFIRMATION) { - stepContent = ; - } - - if (currentStep === MockDataGeneratorStep.SCHEMA_EDITOR) { - stepContent = ; - } - return ( { if (!open) { @@ -84,8 +84,9 @@ const MockDataGeneratorModal = ({ > - {stepContent} - + + {STEP_TO_STEP_CONTENT[currentStep]} + .mongodb.net/" \\ + --username \\ + --password "" \\ + mockdatascript.js +`; + +const outerSectionStyles = css({ + display: 'flex', + flexDirection: 'column', + gap: spacing[400], +}); + +const listStyles = css({ + listStylePosition: 'inside', + listStyleType: 'disc', + marginLeft: spacing[200], +}); + +const copyableStyles = css({ + marginLeft: spacing[400], +}); + +const sectionInstructionStyles = css({ + margin: `${spacing[200]}px 0`, +}); + +const resourceSectionStyles = css({ + padding: `${spacing[400]}px ${spacing[800]}px`, + borderRadius: spacing[400], +}); + +const resourceSectionLightStyles = css({ + backgroundColor: palette.gray.light3, +}); + +const resourceSectionDarkStyles = css({ + backgroundColor: palette.gray.dark3, +}); + +const resourceSectionHeader = css({ + marginBottom: spacing[300], +}); + +const ScriptScreen = () => { + const isDarkMode = useDarkMode(); + const connectionInfo = useConnectionInfo(); + + return ( + + + + Prerequisites + + + To run the generated script, you must: + + + + Install{' '} + + mongosh + + + + Install{' '} + faker.js + + npm install @faker-js/faker + + + + + + + 1. Create a .js file with the following script + + + In the directory that you created, create a file named + mockdatascript.js (or any name you'd like). + + {/* TODO: CLOUDP-333860: Hook up to the code generated as part script generation */} + + TK + + + + + 2. Run the script with mongosh + + + In the same working directory run the command below. Please{' '} + paste in your username and password where there are + placeholders.{' '} + + Note that this will add data to your cluster and will not be + reversible. + + + + {RUN_SCRIPT_COMMAND} + + + + Resources + + + + Generating Synthetic Data with MongoDB + + + + + Learn About the MongoDB Shell + + + {connectionInfo.atlasMetadata && + connectionInfo.atlasMetadata.projectId && ( + + + Access your Database Users + + + )} + + + + ); +}; + +export default ScriptScreen; diff --git a/packages/compass-components/package.json b/packages/compass-components/package.json index 136efecf118..2ef150acde9 100644 --- a/packages/compass-components/package.json +++ b/packages/compass-components/package.json @@ -45,6 +45,7 @@ "@leafygreen-ui/code": "^16.0.2", "@leafygreen-ui/combobox": "^11.0.2", "@leafygreen-ui/confirmation-modal": "^6.0.2", + "@leafygreen-ui/copyable": "^10.0.14", "@leafygreen-ui/descendants": "^2.1.0", "@leafygreen-ui/emotion": "^4.0.9", "@leafygreen-ui/guide-cue": "^7.0.2", diff --git a/packages/compass-components/src/components/leafygreen.tsx b/packages/compass-components/src/components/leafygreen.tsx index c15ab5e0f08..0a00f00c7d2 100644 --- a/packages/compass-components/src/components/leafygreen.tsx +++ b/packages/compass-components/src/components/leafygreen.tsx @@ -3,6 +3,7 @@ import React, { useState } from 'react'; // This file exports `@leafygreen-ui` components and wraps some of them. // 1. Import the components we use from leafygreen. +import { default as Copyable } from '@leafygreen-ui/copyable'; import { default as Badge } from '@leafygreen-ui/badge'; import { default as Banner } from '@leafygreen-ui/banner'; import Checkbox from '@leafygreen-ui/checkbox'; @@ -146,6 +147,7 @@ export { Chip, Code, ConfirmationModal, + Copyable, ExpandedContent, HeaderCell, HeaderRow,
+ TK +
+ {RUN_SCRIPT_COMMAND} +