diff --git a/package-lock.json b/package-lock.json index 8865f53cd09..d6c490d9035 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7335,6 +7335,28 @@ "react-dom": "^17.0.0 || ^18.0.0" } }, + "node_modules/@leafygreen-ui/progress-bar": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/progress-bar/-/progress-bar-1.0.7.tgz", + "integrity": "sha512-puqjyAs+epIFa4mU9UA3c7FKn/SWPj3jPlxtQwSFJJFKqK5MVgJ1Tm/1lWyCdiD6N2EQLkmEjxeU740zPGFWhA==", + "license": "Apache-2.0", + "dependencies": { + "@leafygreen-ui/a11y": "^3.0.5", + "@leafygreen-ui/emotion": "^5.1.0", + "@leafygreen-ui/hooks": "^9.3.0", + "@leafygreen-ui/icon": "^14.7.1", + "@leafygreen-ui/lib": "^15.6.2", + "@leafygreen-ui/palette": "^5.0.2", + "@leafygreen-ui/tokens": "^4.1.0", + "@leafygreen-ui/typography": "^22.2.3", + "@lg-tools/test-harnesses": "^0.3.4", + "lodash": "^4.17.21", + "polished": "^4.2.2" + }, + "peerDependencies": { + "@leafygreen-ui/leafygreen-provider": "^5.0.0 || ^4.0.0 || ^3.2.0" + } + }, "node_modules/@leafygreen-ui/radio-box-group": { "version": "15.0.10", "resolved": "https://registry.npmjs.org/@leafygreen-ui/radio-box-group/-/radio-box-group-15.0.10.tgz", @@ -47949,6 +47971,7 @@ "@leafygreen-ui/polymorphic": "^3.1.0", "@leafygreen-ui/popover": "^14.3.1", "@leafygreen-ui/portal": "^7.1.0", + "@leafygreen-ui/progress-bar": "^1.0.7", "@leafygreen-ui/radio-box-group": "^15.0.10", "@leafygreen-ui/radio-group": "^13.0.10", "@leafygreen-ui/search-input": "^6.1.1", @@ -59032,6 +59055,24 @@ "@leafygreen-ui/lib": "^15.6.1" } }, + "@leafygreen-ui/progress-bar": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/progress-bar/-/progress-bar-1.0.7.tgz", + "integrity": "sha512-puqjyAs+epIFa4mU9UA3c7FKn/SWPj3jPlxtQwSFJJFKqK5MVgJ1Tm/1lWyCdiD6N2EQLkmEjxeU740zPGFWhA==", + "requires": { + "@leafygreen-ui/a11y": "^3.0.5", + "@leafygreen-ui/emotion": "^5.1.0", + "@leafygreen-ui/hooks": "^9.3.0", + "@leafygreen-ui/icon": "^14.7.1", + "@leafygreen-ui/lib": "^15.6.2", + "@leafygreen-ui/palette": "^5.0.2", + "@leafygreen-ui/tokens": "^4.0.0", + "@leafygreen-ui/typography": "^22.2.2", + "@lg-tools/test-harnesses": "^0.3.4", + "lodash": "^4.17.21", + "polished": "^4.2.2" + } + }, "@leafygreen-ui/radio-box-group": { "version": "15.0.10", "resolved": "https://registry.npmjs.org/@leafygreen-ui/radio-box-group/-/radio-box-group-15.0.10.tgz", @@ -61477,6 +61518,7 @@ "@leafygreen-ui/polymorphic": "^3.1.0", "@leafygreen-ui/popover": "^14.3.1", "@leafygreen-ui/portal": "^7.1.0", + "@leafygreen-ui/progress-bar": "^1.0.7", "@leafygreen-ui/radio-box-group": "^15.0.10", "@leafygreen-ui/radio-group": "^13.0.10", "@leafygreen-ui/search-input": "^6.1.1", diff --git a/packages/compass-components/package.json b/packages/compass-components/package.json index f7140299da7..214c1bc67bc 100644 --- a/packages/compass-components/package.json +++ b/packages/compass-components/package.json @@ -64,6 +64,7 @@ "@leafygreen-ui/polymorphic": "^3.1.0", "@leafygreen-ui/popover": "^14.3.1", "@leafygreen-ui/portal": "^7.1.0", + "@leafygreen-ui/progress-bar": "^1.0.7", "@leafygreen-ui/radio-box-group": "^15.0.10", "@leafygreen-ui/radio-group": "^13.0.10", "@leafygreen-ui/search-input": "^6.1.1", diff --git a/packages/compass-components/src/components/leafygreen.tsx b/packages/compass-components/src/components/leafygreen.tsx index 8f533686eac..57886bcdcd6 100644 --- a/packages/compass-components/src/components/leafygreen.tsx +++ b/packages/compass-components/src/components/leafygreen.tsx @@ -23,6 +23,7 @@ import { import { Menu, MenuSeparator, MenuItem } from '@leafygreen-ui/menu'; export type { MenuItemProps } from '@leafygreen-ui/menu'; import { InfoSprinkle } from '@leafygreen-ui/info-sprinkle'; +import { ProgressBar } from '@leafygreen-ui/progress-bar'; // If a leafygreen Menu (and therefore MenuItems) makes its way into a
, // clicking on a menu item will submit that form. This is because it uses a button @@ -191,6 +192,7 @@ export { Combobox, ComboboxGroup, ComboboxOption, + ProgressBar, }; export * as Avatar from '@leafygreen-ui/avatar'; diff --git a/packages/compass-components/src/components/loader.tsx b/packages/compass-components/src/components/loader.tsx index ffa567b569a..57c47d90189 100644 --- a/packages/compass-components/src/components/loader.tsx +++ b/packages/compass-components/src/components/loader.tsx @@ -3,9 +3,10 @@ import { palette } from '@leafygreen-ui/palette'; import { spacing } from '@leafygreen-ui/tokens'; import { css, cx, keyframes } from '@leafygreen-ui/emotion'; import { useDarkMode } from '../hooks/use-theme'; -import { Subtitle, Button } from './leafygreen'; +import { Subtitle, Button, ProgressBar } from './leafygreen'; +import type { ProgressBarProps } from '@leafygreen-ui/progress-bar'; -const containerStyles = css({ +const loaderContainerStyles = css({ display: 'flex', gap: spacing[200], flexDirection: 'column', @@ -14,6 +15,18 @@ const containerStyles = css({ maxWidth: spacing[1600] * 8, }); +const progressContainerStyles = css({ + width: '100%', + height: '100%', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + maxWidth: spacing[1600] * 8, + gap: spacing[500], + margin: '0 auto', +}); + const textStyles = css({ color: palette.green.dark2, textAlign: 'center', @@ -36,11 +49,16 @@ type SpinLoaderWithLabelProps = Omit & { ['data-testid']?: string; }; -type CancelLoaderProps = Omit & { +type CancelActionProps = { onCancel(): void; - cancelText: string; + cancelText?: string; }; +type CancelLoaderProps = Omit & + CancelActionProps; + +type ProgressLoaderWithCancelProps = CancelActionProps & ProgressBarProps; + const shellLoaderSpin = keyframes` 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } @@ -94,7 +112,10 @@ function SpinLoaderWithLabel({ const darkMode = useDarkMode(_darkMode); return ( -
+
+ + +
+ ); +} + +export { + SpinLoaderWithLabel, + SpinLoader, + CancelLoader, + ProgressLoaderWithCancel, +}; diff --git a/packages/compass-components/src/index.ts b/packages/compass-components/src/index.ts index cc6484d7b86..d9c36b124e3 100644 --- a/packages/compass-components/src/index.ts +++ b/packages/compass-components/src/index.ts @@ -40,6 +40,7 @@ export { SpinLoader, SpinLoaderWithLabel, CancelLoader, + ProgressLoaderWithCancel, } from './components/loader'; import { ResizeHandle, ResizeDirection } from './components/resize-handle'; import { Accordion } from './components/accordion'; diff --git a/packages/compass-data-modeling/src/components/analysis-progress-status.spec.tsx b/packages/compass-data-modeling/src/components/analysis-progress-status.spec.tsx new file mode 100644 index 00000000000..b7776d2413f --- /dev/null +++ b/packages/compass-data-modeling/src/components/analysis-progress-status.spec.tsx @@ -0,0 +1,139 @@ +import React from 'react'; +import { expect } from 'chai'; +import { screen, waitFor } from '@mongodb-js/testing-library-compass'; +import AnalysisProgressStatus from './analysis-progress-status'; +import { renderWithStore, testConnections } from '../../test/setup-store'; +import { + AnalysisProcessActionTypes, + startAnalysis, +} from '../store/analysis-process'; +import { createSandboxFromDefaultPreferences } from 'compass-preferences-model'; + +describe('AnalysisProgressStatus', () => { + async function renderAnalysisProgressStatus({ + automaticallyInferRelations = false, + } = {}) { + const preferences = await createSandboxFromDefaultPreferences(); + const { store } = renderWithStore(, { + services: { + preferences, + }, + }); + void store.dispatch( + startAnalysis( + 'My Diagram', + testConnections[0].id, + 'testDB', + ['coll1', 'coll2', 'coll3'], + { automaticallyInferRelations } + ) + ); + return store; + } + + it('Allows cancellation', async () => { + const store = await renderAnalysisProgressStatus(); + expect(screen.getByText('Sampling collections…')).to.be.visible; + expect(screen.getByText('Cancel')).to.be.visible; + screen.getByText('Cancel').click(); + await waitFor(() => { + expect(store.getState().analysisProgress.step).to.equal('IDLE'); + expect(store.getState().diagram).to.be.null; + }); + }); + + describe('Keeps showing progress along the way', () => { + it('Without relationship inferring', async () => { + const store = await renderAnalysisProgressStatus({ + automaticallyInferRelations: false, + }); + expect(screen.getByText('Sampling collections…')).to.be.visible; + expect(screen.getByText('0/3')).to.be.visible; + + // 2 out of 3 samples fetched, 1 analyzed + store.dispatch({ + type: AnalysisProcessActionTypes.NAMESPACE_SAMPLE_FETCHED, + }); + store.dispatch({ + type: AnalysisProcessActionTypes.NAMESPACE_SAMPLE_FETCHED, + }); + store.dispatch({ + type: AnalysisProcessActionTypes.NAMESPACE_SCHEMA_ANALYZED, + }); + + expect(screen.getByText('Sampling collections…')).to.be.visible; + expect(screen.getByText('2/3')).to.be.visible; + + // Last sample fetched + store.dispatch({ + type: AnalysisProcessActionTypes.NAMESPACE_SAMPLE_FETCHED, + }); + + expect(screen.getByText('Analyzing collection schemas…')).to.be.visible; + expect(screen.getByText('1/3')).to.be.visible; + + // Finish analyzing + store.dispatch({ + type: AnalysisProcessActionTypes.NAMESPACE_SCHEMA_ANALYZED, + }); + store.dispatch({ + type: AnalysisProcessActionTypes.NAMESPACE_SCHEMA_ANALYZED, + }); + + expect(screen.queryByText('Inferring relationships between collections…')) + .not.to.exist; + expect(screen.getByText('Preparing diagram…')).to.be.visible; + }); + + it('With relationship inferring', async () => { + const store = await renderAnalysisProgressStatus({ + automaticallyInferRelations: true, + }); + expect(screen.getByText('Sampling collections…')).to.be.visible; + expect(screen.getByText('0/3')).to.be.visible; + + // Fetch and analyze all samples + store.dispatch({ + type: AnalysisProcessActionTypes.NAMESPACE_SAMPLE_FETCHED, + }); + store.dispatch({ + type: AnalysisProcessActionTypes.NAMESPACE_SAMPLE_FETCHED, + }); + store.dispatch({ + type: AnalysisProcessActionTypes.NAMESPACE_SAMPLE_FETCHED, + }); + store.dispatch({ + type: AnalysisProcessActionTypes.NAMESPACE_SCHEMA_ANALYZED, + }); + store.dispatch({ + type: AnalysisProcessActionTypes.NAMESPACE_SCHEMA_ANALYZED, + }); + store.dispatch({ + type: AnalysisProcessActionTypes.NAMESPACE_SCHEMA_ANALYZED, + }); + + expect(screen.getByText('Inferring relationships between collections…')) + .to.be.visible; + expect(screen.queryByText('0/3')).not.to.exist; + + // Infer some relationships + store.dispatch({ + type: AnalysisProcessActionTypes.NAMESPACE_RELATIONS_INFERRED, + }); + store.dispatch({ + type: AnalysisProcessActionTypes.NAMESPACE_RELATIONS_INFERRED, + }); + + expect(screen.getByText('Inferring relationships between collections…')) + .to.be.visible; + expect(screen.queryByText('2/3')).not.to.exist; + + // Finish inferring + store.dispatch({ + type: AnalysisProcessActionTypes.NAMESPACE_RELATIONS_INFERRED, + }); + + expect(screen.getByText('Preparing diagram…')).to.be.visible; + }); + }); +}); diff --git a/packages/compass-data-modeling/src/components/analysis-progress-status.tsx b/packages/compass-data-modeling/src/components/analysis-progress-status.tsx new file mode 100644 index 00000000000..2b2e30cc967 --- /dev/null +++ b/packages/compass-data-modeling/src/components/analysis-progress-status.tsx @@ -0,0 +1,118 @@ +import { + ProgressLoaderWithCancel, + useDarkMode, +} from '@mongodb-js/compass-components'; +import React from 'react'; +import { connect } from 'react-redux'; +import type { DataModelingState } from '../store/reducer'; +import { cancelAnalysis, type AnalysisStep } from '../store/analysis-process'; + +function getProgressPropsFromStatus({ + step, + sampledCollections, + analyzedCollections, + collectionRelationsInferred, + totalCollections, +}: { + step: AnalysisStep; + sampledCollections: number; + analyzedCollections: number; + collectionRelationsInferred: number; + totalCollections: number; +}): { + label: string; +} & ( + | { + isIndeterminate: false; + maxValue: number; + value: number; + formatValue?: 'fraction'; + } + | { + isIndeterminate: true; + } +) { + if (step === 'SAMPLING') { + return { + isIndeterminate: false, + label: 'Sampling collections…', + maxValue: totalCollections, + value: sampledCollections, + formatValue: 'fraction', + }; + } + if (step === 'ANALYZING_SCHEMA') { + return { + isIndeterminate: false, + label: 'Analyzing collection schemas…', + maxValue: totalCollections, + value: analyzedCollections, + formatValue: 'fraction', + }; + } + if (step === 'INFERRING_RELATIONSHIPS') { + return { + isIndeterminate: false, + label: 'Inferring relationships between collections…', + maxValue: totalCollections, + value: collectionRelationsInferred, + formatValue: undefined, + }; + } + return { + isIndeterminate: true, + label: 'Preparing diagram…', + }; +} + +export type AnalysisProgressStatusProps = { + step: AnalysisStep; + sampledCollections: number; + analyzedCollections: number; + collectionRelationsInferred: number; + totalCollections: number; + onCancelClick: () => void; +}; + +export const AnalysisProgressStatus: React.FC = ({ + step, + sampledCollections, + analyzedCollections, + collectionRelationsInferred, + totalCollections, + onCancelClick, +}) => { + const darkMode = useDarkMode(); + return ( + + ); +}; + +export default connect( + (state: DataModelingState) => { + const analysisProgress = state.analysisProgress; + return { + step: analysisProgress.step, + sampledCollections: analysisProgress.samplesFetched, + analyzedCollections: analysisProgress.schemasAnalyzed, + collectionRelationsInferred: analysisProgress.collectionRelationsInferred, + totalCollections: + analysisProgress.currentAnalysisOptions?.collections.length ?? 0, + }; + }, + { + onCancelClick: cancelAnalysis, + } +)(AnalysisProgressStatus); diff --git a/packages/compass-data-modeling/src/components/diagram-editor-toolbar.tsx b/packages/compass-data-modeling/src/components/diagram-editor-toolbar.tsx index 31750755507..2d3df8d6cdf 100644 --- a/packages/compass-data-modeling/src/components/diagram-editor-toolbar.tsx +++ b/packages/compass-data-modeling/src/components/diagram-editor-toolbar.tsx @@ -215,7 +215,7 @@ export default connect( (state: DataModelingState) => { const { diagram, step } = state; return { - step: step, + step, hasUndo: (diagram?.edits.prev.length ?? 0) > 0, hasRedo: (diagram?.edits.next.length ?? 0) > 0, diagramName: diagram?.name, diff --git a/packages/compass-data-modeling/src/components/diagram-editor.tsx b/packages/compass-data-modeling/src/components/diagram-editor.tsx index ef08e257deb..dec3a101b7b 100644 --- a/packages/compass-data-modeling/src/components/diagram-editor.tsx +++ b/packages/compass-data-modeling/src/components/diagram-editor.tsx @@ -34,7 +34,6 @@ import type { } from '@mongodb-js/compass-components'; import { Banner, - CancelLoader, WorkspaceContainer, css, spacing, @@ -50,7 +49,7 @@ import { useFocusStateIncludingUnfocused, FocusStates, } from '@mongodb-js/compass-components'; -import { cancelAnalysis, retryAnalysis } from '../store/analysis-process'; +import { retryAnalysis } from '../store/analysis-process'; import type { FieldPath, StaticModel } from '../services/data-model-storage'; import DiagramEditorToolbar from './diagram-editor-toolbar'; import ExportDiagramModal from './export-diagram-modal'; @@ -64,15 +63,7 @@ import toNS from 'mongodb-ns'; import { FIELD_TYPES } from '../utils/field-types'; import { getNamespaceRelationships } from '../utils/utils'; import { usePreference } from 'compass-preferences-model/provider'; - -const loadingContainerStyles = css({ - width: '100%', - paddingTop: spacing[1800] * 3, -}); - -const loaderStyles = css({ - margin: '0 auto', -}); +import AnalysisProgressStatus from './analysis-progress-status'; const errorBannerStyles = css({ margin: spacing[200], @@ -585,14 +576,12 @@ const DiagramEditor: React.FunctionComponent<{ step: DataModelingState['step']; diagramId?: string; onRetryClick: () => void; - onCancelClick: () => void; onAddCollectionClick: () => void; DiagramComponent?: typeof Diagram; }> = ({ step, diagramId, onRetryClick, - onCancelClick, onAddCollectionClick, DiagramComponent = Diagram, }) => { @@ -622,16 +611,7 @@ const DiagramEditor: React.FunctionComponent<{ } if (step === 'ANALYZING') { - content = ( -
- -
- ); + content = ; } if (step === 'ANALYSIS_FAILED') { @@ -690,7 +670,6 @@ export default connect( }, { onRetryClick: retryAnalysis, - onCancelClick: cancelAnalysis, onAddCollectionClick: addCollection, } )(DiagramEditor); diff --git a/packages/compass-data-modeling/src/components/new-diagram-form.spec.tsx b/packages/compass-data-modeling/src/components/new-diagram-form.spec.tsx index 72794bd1ed3..7e5f7f9c9b9 100644 --- a/packages/compass-data-modeling/src/components/new-diagram-form.spec.tsx +++ b/packages/compass-data-modeling/src/components/new-diagram-form.spec.tsx @@ -10,6 +10,7 @@ import NewDiagramForm from './new-diagram-form'; import { changeName, createNewDiagram } from '../store/generate-diagram-wizard'; import { renderWithStore } from '../../test/setup-store'; import type { DataModelingStore } from '../../test/setup-store'; +import { createSandboxFromDefaultPreferences } from 'compass-preferences-model'; function getComboboxByTestId(testId: string) { return within(screen.getByTestId(testId)).getByRole('combobox'); @@ -282,7 +283,12 @@ describe('NewDiagramForm', function () { context('select-collections step', function () { it('shows list of collections', async function () { - const { store } = renderWithStore(); + const preferences = await createSandboxFromDefaultPreferences(); + const { store } = renderWithStore(, { + services: { + preferences, + }, + }); { // Navigate to connections step diff --git a/packages/compass-data-modeling/src/components/new-diagram-form.tsx b/packages/compass-data-modeling/src/components/new-diagram-form.tsx index b99516c1e34..4e73f830c26 100644 --- a/packages/compass-data-modeling/src/components/new-diagram-form.tsx +++ b/packages/compass-data-modeling/src/components/new-diagram-form.tsx @@ -39,6 +39,7 @@ import { Checkbox, } from '@mongodb-js/compass-components'; import { usePreference } from 'compass-preferences-model/provider'; +import { selectIsAnalysisInProgress } from '../store/analysis-process'; const footerStyles = css({ flexDirection: 'row', @@ -559,8 +560,7 @@ export default connect( collections: databaseCollections ?? [], selectedCollections: selectedCollections ?? [], error, - analysisInProgress: - state.analysisProgress.analysisProcessStatus === 'in-progress', + analysisInProgress: selectIsAnalysisInProgress(state), automaticallyInferRelationships: state.generateDiagramWizard.automaticallyInferRelations, }; diff --git a/packages/compass-data-modeling/src/store/analysis-process.ts b/packages/compass-data-modeling/src/store/analysis-process.ts index 1c1ab5e81e1..21700e1918c 100644 --- a/packages/compass-data-modeling/src/store/analysis-process.ts +++ b/packages/compass-data-modeling/src/store/analysis-process.ts @@ -16,6 +16,12 @@ import { import { inferForeignToLocalRelationshipsForCollection } from './relationships'; import { mongoLogId } from '@mongodb-js/compass-logging/provider'; +export type AnalysisStep = + | 'IDLE' + | 'SAMPLING' + | 'ANALYZING_SCHEMA' + | 'INFERRING_RELATIONSHIPS'; + export type AnalysisProcessState = { currentAnalysisOptions: | ({ @@ -25,10 +31,11 @@ export type AnalysisProcessState = { collections: string[]; } & AnalysisOptions) | null; - analysisProcessStatus: 'idle' | 'in-progress'; + step: AnalysisStep; samplesFetched: number; schemasAnalyzed: number; - relationsInferred: number; + willInferRelations: boolean; + collectionRelationsInferred: number; }; export const AnalysisProcessActionTypes = { @@ -38,8 +45,8 @@ export const AnalysisProcessActionTypes = { 'data-modeling/analysis-stats/NAMESPACE_SAMPLE_FETCHED', NAMESPACE_SCHEMA_ANALYZED: 'data-modeling/analysis-stats/NAMESPACE_SCHEMA_ANALYZED', - NAMESPACES_RELATIONS_INFERRED: - 'data-modeling/analysis-stats/NAMESPACES_RELATIONS_INFERRED', + NAMESPACE_RELATIONS_INFERRED: + 'data-modeling/analysis-stats/NAMESPACE_RELATIONS_INFERRED', ANALYSIS_FINISHED: 'data-modeling/analysis-stats/ANALYSIS_FINISHED', ANALYSIS_FAILED: 'data-modeling/analysis-stats/ANALYSIS_FAILED', ANALYSIS_CANCELED: 'data-modeling/analysis-stats/ANALYSIS_CANCELED', @@ -56,22 +63,19 @@ export type AnalyzingCollectionsStartAction = { database: string; collections: string[]; options: AnalysisOptions; + willInferRelations: boolean; }; export type NamespaceSampleFetchedAction = { type: typeof AnalysisProcessActionTypes.NAMESPACE_SAMPLE_FETCHED; - namespace: string; }; export type NamespaceSchemaAnalyzedAction = { type: typeof AnalysisProcessActionTypes.NAMESPACE_SCHEMA_ANALYZED; - namespace: string; }; export type NamespacesRelationsInferredAction = { - type: typeof AnalysisProcessActionTypes.NAMESPACES_RELATIONS_INFERRED; - namespace: string; - count: number; + type: typeof AnalysisProcessActionTypes.NAMESPACE_RELATIONS_INFERRED; }; export type AnalysisFinishedAction = { @@ -106,24 +110,26 @@ export type AnalysisProgressActions = | AnalysisFailedAction | AnalysisCanceledAction; -const INITIAL_STATE = { +const INITIAL_STATE: AnalysisProcessState = { currentAnalysisOptions: null, - analysisProcessStatus: 'idle' as const, + step: 'IDLE', samplesFetched: 0, schemasAnalyzed: 0, - relationsInferred: 0, + willInferRelations: false, + collectionRelationsInferred: 0, }; export const analysisProcessReducer: Reducer = ( state = INITIAL_STATE, action ) => { + const totalCollections = + state.currentAnalysisOptions?.collections.length ?? 0; if ( isAction(action, AnalysisProcessActionTypes.ANALYZING_COLLECTIONS_START) ) { return { ...INITIAL_STATE, - analysisProcessStatus: 'in-progress', currentAnalysisOptions: { name: action.name, connectionId: action.connectionId, @@ -131,18 +137,42 @@ export const analysisProcessReducer: Reducer = ( collections: action.collections, automaticallyInferRelations: action.options.automaticallyInferRelations, }, + step: 'SAMPLING', + willInferRelations: action.willInferRelations, }; } if (isAction(action, AnalysisProcessActionTypes.NAMESPACE_SAMPLE_FETCHED)) { + const samplesFetched = state.samplesFetched + 1; + const nextStep = 'ANALYZING_SCHEMA'; return { ...state, - samplesFetched: state.samplesFetched + 1, + samplesFetched, + step: samplesFetched === totalCollections ? nextStep : state.step, }; } if (isAction(action, AnalysisProcessActionTypes.NAMESPACE_SCHEMA_ANALYZED)) { + const schemasAnalyzed = state.schemasAnalyzed + 1; + const nextStep = state.willInferRelations + ? 'INFERRING_RELATIONSHIPS' + : 'IDLE'; return { ...state, - schemasAnalyzed: state.schemasAnalyzed + 1, + schemasAnalyzed, + step: schemasAnalyzed === totalCollections ? nextStep : state.step, + }; + } + if ( + isAction(action, AnalysisProcessActionTypes.NAMESPACE_RELATIONS_INFERRED) + ) { + const collectionRelationsInferred = state.collectionRelationsInferred + 1; + const nextStep = 'IDLE'; + return { + ...state, + collectionRelationsInferred, + step: + collectionRelationsInferred === totalCollections + ? nextStep + : state.step, }; } if ( @@ -152,7 +182,7 @@ export const analysisProcessReducer: Reducer = ( ) { return { ...state, - analysisProcessStatus: 'idle', + step: 'IDLE', }; } return state; @@ -218,6 +248,9 @@ export function startAnalysis( }); const cancelController = (cancelAnalysisControllerRef.current = new AbortController()); + const willInferRelations = + preferences.getPreferences().enableAutomaticRelationshipInference && + options.automaticallyInferRelations; dispatch({ type: AnalysisProcessActionTypes.ANALYZING_COLLECTIONS_START, name, @@ -225,6 +258,7 @@ export function startAnalysis( database, collections, options, + willInferRelations, }); try { let relations: Relationship[] = []; @@ -242,31 +276,27 @@ export function startAnalysis( } ); - const accessor = await analyzeDocuments(sample, { - signal: cancelController.signal, - }); - - // TODO(COMPASS-9314): Update how we show analysis progress. dispatch({ type: AnalysisProcessActionTypes.NAMESPACE_SAMPLE_FETCHED, - namespace: ns, + }); + + const accessor = await analyzeDocuments(sample, { + signal: cancelController.signal, }); const schema = await accessor.getMongoDBJsonSchema({ signal: cancelController.signal, }); + dispatch({ type: AnalysisProcessActionTypes.NAMESPACE_SCHEMA_ANALYZED, - namespace: ns, }); + return { ns, schema, sample, isExpanded: DEFAULT_IS_EXPANDED }; }) ); - const attemptRelationshipInference = - preferences.getPreferences().enableAutomaticRelationshipInference && - options.automaticallyInferRelations; - if (attemptRelationshipInference) { + if (willInferRelations) { relations = ( await Promise.all( collections.map( @@ -293,9 +323,7 @@ export function startAnalysis( } ); dispatch({ - type: AnalysisProcessActionTypes.NAMESPACES_RELATIONS_INFERRED, - namespace: ns, - count: relationships.length, + type: AnalysisProcessActionTypes.NAMESPACE_RELATIONS_INFERRED, }); return relationships; } @@ -338,7 +366,7 @@ export function startAnalysis( track('Data Modeling Diagram Created', { num_collections: collections.length, - num_relations_inferred: attemptRelationshipInference + num_relations_inferred: willInferRelations ? relations.length : undefined, }); @@ -392,3 +420,7 @@ export function cancelAnalysis(): DataModelingThunkAction { cancelAnalysisControllerRef.current = null; }; } + +export const selectIsAnalysisInProgress = (state: { + analysisProgress: AnalysisProcessState; +}): boolean => state.analysisProgress.step !== 'IDLE'; diff --git a/packages/compass-data-modeling/test/setup-store.tsx b/packages/compass-data-modeling/test/setup-store.tsx index 1b0d89ea509..5a7ba38bdd9 100644 --- a/packages/compass-data-modeling/test/setup-store.tsx +++ b/packages/compass-data-modeling/test/setup-store.tsx @@ -28,7 +28,7 @@ type ConnectionInfoWithMockData = ConnectionInfo & { }; export type DataModelingStore = Awaited>; -const testConnections = [ +export const testConnections = [ { id: 'one', savedConnectionType: 'favorite' as const, @@ -174,6 +174,19 @@ export const setupStore = ( conn.databases.find((x) => x._id === database)?.collections ); }, + sample: () => { + return new Promise((resolve) => + setTimeout( + () => + resolve([ + { _id: 'sample1' }, + { _id: 'sample2' }, + { _id: 'sample3' }, + ]), + 1 + ) + ); + }, }; }, } as any,