Skip to content

Commit 4e1a0b2

Browse files
authored
feat(data-modeling): display errors when creating a diagram COMPASS-9307 (#6892)
* display errors * add tests * tests store setup * tests * clean up * error tests * check * rename folder * ensure that componet is rendered * handle connection error by closing modal
1 parent c034d9f commit 4e1a0b2

File tree

9 files changed

+775
-39
lines changed

9 files changed

+775
-39
lines changed

packages/compass-data-modeling/src/components/new-diagram-form.spec.tsx

Lines changed: 431 additions & 0 deletions
Large diffs are not rendered by default.

packages/compass-data-modeling/src/components/new-diagram-form.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
Banner,
2222
Button,
2323
css,
24+
ErrorSummary,
2425
FormFieldContainer,
2526
Modal,
2627
ModalBody,
@@ -29,9 +30,14 @@ import {
2930
Option,
3031
Select,
3132
SelectTable,
33+
spacing,
3234
TextInput,
3335
} from '@mongodb-js/compass-components';
3436

37+
const footerStyles = css({
38+
gap: spacing[200],
39+
});
40+
3541
const FormStepContainer: React.FunctionComponent<{
3642
title: string;
3743
description?: string;
@@ -56,7 +62,7 @@ const FormStepContainer: React.FunctionComponent<{
5662
<>
5763
<ModalHeader title={title} subtitle={description}></ModalHeader>
5864
<ModalBody>{children}</ModalBody>
59-
<ModalFooter>
65+
<ModalFooter className={footerStyles}>
6066
<Button
6167
onClick={onNextClick}
6268
disabled={isNextDisabled}
@@ -90,6 +96,7 @@ type NewDiagramFormProps = {
9096
selectedDatabase: string | null;
9197
collections: string[];
9298
selectedCollections: string[];
99+
error: Error | null;
93100

94101
onCancel: () => void;
95102
onNameChange: (name: string) => void;
@@ -115,6 +122,7 @@ const NewDiagramForm: React.FunctionComponent<NewDiagramFormProps> = ({
115122
selectedDatabase,
116123
collections,
117124
selectedCollections,
125+
error,
118126
onCancel,
119127
onNameChange,
120128
onNameConfirm,
@@ -174,7 +182,7 @@ const NewDiagramForm: React.FunctionComponent<NewDiagramFormProps> = ({
174182
onConfirmAction: onCollectionsSelectionConfirm,
175183
confirmActionLabel: 'Generate',
176184
isConfirmDisabled:
177-
!selectedCollections || selectCollections.length === 0,
185+
!selectedCollections || selectedCollections.length === 0,
178186
onCancelAction: onDatabaseSelectCancel,
179187
cancelLabel: 'Back',
180188
};
@@ -203,7 +211,7 @@ const NewDiagramForm: React.FunctionComponent<NewDiagramFormProps> = ({
203211
<TextInput
204212
label="New data model name"
205213
value={diagramName}
206-
data-testId="new-diagram-name-input"
214+
data-testid="new-diagram-name-input"
207215
onChange={(e) => {
208216
onNameChange(e.currentTarget.value);
209217
}}
@@ -215,6 +223,7 @@ const NewDiagramForm: React.FunctionComponent<NewDiagramFormProps> = ({
215223
<FormFieldContainer>
216224
<Select
217225
label=""
226+
aria-label="Select connection"
218227
value={selectedConnectionId ?? ''}
219228
data-testid="new-diagram-connection-selector"
220229
onChange={onConnectionSelect}
@@ -240,6 +249,7 @@ const NewDiagramForm: React.FunctionComponent<NewDiagramFormProps> = ({
240249
<FormFieldContainer>
241250
<Select
242251
label=""
252+
aria-label="Select database"
243253
value={selectedDatabase ?? ''}
244254
data-testid="new-diagram-database-selector"
245255
onChange={onDatabaseSelect}
@@ -317,6 +327,7 @@ const NewDiagramForm: React.FunctionComponent<NewDiagramFormProps> = ({
317327
isLoading={isLoading}
318328
>
319329
{formContent}
330+
{error && <ErrorSummary errors={[error.message]} />}
320331
</FormStepContainer>
321332
</Modal>
322333
);
@@ -351,6 +362,7 @@ export default connect(
351362
selectedDatabase,
352363
databaseCollections,
353364
selectedCollections,
365+
error,
354366
} = state.generateDiagramWizard;
355367
return {
356368
isModalOpen: inProgress,
@@ -366,6 +378,7 @@ export default connect(
366378
selectedDatabase,
367379
collections: databaseCollections ?? [],
368380
selectedCollections: selectedCollections ?? [],
381+
error,
369382
};
370383
},
371384
{

packages/compass-data-modeling/src/index.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,19 @@
1-
import { applyMiddleware, createStore } from 'redux';
2-
import thunk from 'redux-thunk';
31
import { registerHadronPlugin } from 'hadron-app-registry';
42
import { preferencesLocator } from 'compass-preferences-model/provider';
53
import { connectionsLocator } from '@mongodb-js/compass-connections/provider';
64
import { telemetryLocator } from '@mongodb-js/compass-telemetry/provider';
75
import { createLoggerLocator } from '@mongodb-js/compass-logging/provider';
86
import type { WorkspaceComponent } from '@mongodb-js/compass-workspaces';
97
import DataModelingComponent from './components/data-modeling';
10-
import reducer from './store/reducer';
118
import { mongoDBInstancesManagerLocator } from '@mongodb-js/compass-app-stores/provider';
129
import { dataModelStorageServiceLocator } from './provider';
10+
import { activateDataModelingStore } from './store';
1311

1412
const DataModelingPlugin = registerHadronPlugin(
1513
{
1614
name: 'DataModeling',
1715
component: DataModelingComponent,
18-
activate(initialProps, services, { cleanup }) {
19-
const cancelControllerRef = { current: null };
20-
const store = createStore(
21-
reducer,
22-
applyMiddleware(
23-
thunk.withExtraArgument({ ...services, cancelControllerRef })
24-
)
25-
);
26-
return { store, deactivate: cleanup };
27-
},
16+
activate: activateDataModelingStore,
2817
},
2918
{
3019
preferences: preferencesLocator,

packages/compass-data-modeling/src/provider/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ type DataModelStorageServiceState = {
1111
items: MongoDBDataModelDescription[];
1212
};
1313

14-
const noopDataModelStorageService: DataModelStorageServiceState &
14+
export const noopDataModelStorageService: DataModelStorageServiceState &
1515
DataModelStorage = {
1616
status: 'INITIAL',
1717
error: null,

packages/compass-data-modeling/src/store/analysis-process.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,12 @@ export function startAnalysis(
209209
if (cancelController.signal.aborted) {
210210
dispatch({ type: AnalysisProcessActionTypes.ANALYSIS_CANCELED });
211211
} else {
212+
services.logger.log.error(
213+
services.logger.mongoLogId(1_001_000_350),
214+
'DataModeling',
215+
'Failed to analyze schema',
216+
{ err }
217+
);
212218
dispatch({
213219
type: AnalysisProcessActionTypes.ANALYSIS_FAILED,
214220
error: err as Error,

packages/compass-data-modeling/src/store/generate-diagram-wizard.ts

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export type GenerateDiagramWizardState = {
2323
databaseCollections: string[] | null;
2424
selectedCollections: string[] | null;
2525
automaticallyInferRelations: boolean;
26+
error: Error | null;
2627
};
2728

2829
export enum GenerateDiagramWizardActionTypes {
@@ -42,6 +43,7 @@ export enum GenerateDiagramWizardActionTypes {
4243

4344
SELECT_DATABASE = 'data-modeling/generate-diagram-wizard/SELECT_DATABASE',
4445
CONFIRM_SELECT_DATABASE = 'data-modeling/generate-diagram-wizard/CONFIRM_SELECT_DATABASE',
46+
DATABASES_FETCH_FAILED = 'data-modeling/generate-diagram-wizard/DATABASES_FETCH_FAILED',
4547
COLLECTIONS_FETCHED = 'data-modeling/generate-diagram-wizard/COLLECTIONS_FETCHED',
4648
CANCEL_SELECTED_DATABASE = 'data-modeling/generate-diagram-wizard/CANCEL_SELECTED_DATABASE',
4749
COLLECTIONS_FETCH_FAILED = 'data-modeling/generate-diagram-wizard/COLLECTIONS_FETCH_FAILED',
@@ -104,6 +106,11 @@ export type SelectDatabaseAction = {
104106
database: string;
105107
};
106108

109+
export type DatabasesFetchFailedAction = {
110+
type: GenerateDiagramWizardActionTypes.DATABASES_FETCH_FAILED;
111+
error: Error;
112+
};
113+
107114
export type ConfirmSelectDatabaseAction = {
108115
type: GenerateDiagramWizardActionTypes.CONFIRM_SELECT_DATABASE;
109116
};
@@ -121,6 +128,7 @@ export type CollectionsFetchedAction = {
121128

122129
export type CollectionsFetchFailedAction = {
123130
type: GenerateDiagramWizardActionTypes.COLLECTIONS_FETCH_FAILED;
131+
error: Error;
124132
};
125133

126134
export type SelectCollectionsAction = {
@@ -147,17 +155,21 @@ export type GenerateDiagramWizardActions =
147155
| DatabasesFetchedAction
148156
| SelectDatabaseAction
149157
| ConfirmSelectDatabaseAction
158+
| DatabasesFetchFailedAction
150159
| CancelSelectedDatabaseAction
151160
| CollectionsFetchedAction
152161
| SelectCollectionsAction
153162
| ToggleInferRelationsAction
154-
| ConfirmSelectedCollectionsAction;
163+
| ConfirmSelectedCollectionsAction
164+
| CollectionsFetchFailedAction
165+
| ConnectionFailedAction;
155166

156167
const INITIAL_STATE: GenerateDiagramWizardState = {
157168
inProgress: false,
158169
step: 'ENTER_NAME',
159170
diagramName: '',
160171
selectedConnectionId: null,
172+
error: null,
161173
connectionDatabases: null,
162174
selectedDatabase: null,
163175
databaseCollections: null,
@@ -186,13 +198,15 @@ export const generateDiagramWizardReducer: Reducer<
186198
if (isAction(action, GenerateDiagramWizardActionTypes.CANCEL_CONFIRM_NAME)) {
187199
return {
188200
...state,
201+
error: null,
189202
step: 'ENTER_NAME',
190203
};
191204
}
192205
if (isAction(action, GenerateDiagramWizardActionTypes.SELECT_CONNECTION)) {
193206
return {
194207
...state,
195208
selectedConnectionId: action.id,
209+
error: null,
196210
};
197211
}
198212
if (
@@ -220,6 +234,15 @@ export const generateDiagramWizardReducer: Reducer<
220234
selectedDatabase: null,
221235
};
222236
}
237+
if (
238+
isAction(action, GenerateDiagramWizardActionTypes.DATABASES_FETCH_FAILED)
239+
) {
240+
return {
241+
...state,
242+
step: 'SELECT_DATABASE',
243+
error: action.error,
244+
};
245+
}
223246
if (isAction(action, GenerateDiagramWizardActionTypes.SELECT_DATABASE)) {
224247
return {
225248
...state,
@@ -295,7 +318,16 @@ export const generateDiagramWizardReducer: Reducer<
295318
) {
296319
return {
297320
...state,
298-
inProgress: false,
321+
error: action.error,
322+
step: 'SELECT_DATABASE',
323+
};
324+
}
325+
if (
326+
isAction(action, GenerateDiagramWizardActionTypes.CONFIRM_SELECT_DATABASE)
327+
) {
328+
return {
329+
...state,
330+
step: 'LOADING_COLLECTIONS',
299331
};
300332
}
301333
return state;
@@ -326,22 +358,45 @@ export function confirmSelectConnection(): DataModelingThunkAction<
326358
| ConnectionConnectedAction
327359
| DatabasesFetchedAction
328360
| ConnectionFailedAction
361+
| DatabasesFetchFailedAction
329362
> {
330363
return async (dispatch, getState, services) => {
331364
dispatch({
332365
type: GenerateDiagramWizardActionTypes.CONFIRM_SELECT_CONNECTION,
333366
});
367+
368+
const { selectedConnectionId } = getState().generateDiagramWizard;
369+
if (!selectedConnectionId) {
370+
return;
371+
}
334372
try {
335-
const { selectedConnectionId } = getState().generateDiagramWizard;
336-
if (!selectedConnectionId) {
337-
return;
338-
}
339373
const connectionInfo =
340374
services.connections.getConnectionById(selectedConnectionId)?.info;
341375
if (!connectionInfo) {
342376
return;
343377
}
344378
await services.connections.connect(connectionInfo);
379+
// ConnectionsService.connect does not throw an error if it fails to establish a connection,
380+
// so explicitly checking if error is in the connection item and throwing it.
381+
const connectionError =
382+
services.connections.getConnectionById(selectedConnectionId)?.error;
383+
if (connectionError) {
384+
throw connectionError;
385+
}
386+
} catch (err) {
387+
services.logger.log.error(
388+
services.logger.mongoLogId(1_001_000_348),
389+
'DataModeling',
390+
'Failed to select connection',
391+
{ err }
392+
);
393+
dispatch({
394+
type: GenerateDiagramWizardActionTypes.CONNECTION_FAILED,
395+
});
396+
return;
397+
}
398+
399+
try {
345400
dispatch({ type: GenerateDiagramWizardActionTypes.CONNECTION_CONNECTED });
346401
const mongoDBInstance =
347402
services.instanceManager.getMongoDBInstanceForConnection(
@@ -362,7 +417,16 @@ export function confirmSelectConnection(): DataModelingThunkAction<
362417
}),
363418
});
364419
} catch (err) {
365-
dispatch({ type: GenerateDiagramWizardActionTypes.CONNECTION_FAILED });
420+
services.logger.log.error(
421+
services.logger.mongoLogId(1_001_000_351),
422+
'DataModeling',
423+
'Failed to list databases',
424+
{ err }
425+
);
426+
dispatch({
427+
type: GenerateDiagramWizardActionTypes.DATABASES_FETCH_FAILED,
428+
error: err as Error,
429+
});
366430
}
367431
};
368432
}
@@ -414,8 +478,15 @@ export function confirmSelectDatabase(): DataModelingThunkAction<
414478
}),
415479
});
416480
} catch (err) {
481+
services.logger.log.error(
482+
services.logger.mongoLogId(1_001_000_349),
483+
'DataModeling',
484+
'Failed to select database',
485+
{ err }
486+
);
417487
dispatch({
418488
type: GenerateDiagramWizardActionTypes.COLLECTIONS_FETCH_FAILED,
489+
error: err as Error,
419490
});
420491
}
421492
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { PreferencesAccess } from 'compass-preferences-model/provider';
2+
import type { ConnectionsService } from '@mongodb-js/compass-connections/provider';
3+
import type { TrackFunction } from '@mongodb-js/compass-telemetry/provider';
4+
import type { Logger } from '@mongodb-js/compass-logging/provider';
5+
import type { MongoDBInstancesManager } from '@mongodb-js/compass-app-stores/provider';
6+
import type { DataModelStorageService } from '../provider';
7+
import { applyMiddleware, createStore } from 'redux';
8+
import reducer from './reducer';
9+
import thunk from 'redux-thunk';
10+
import type { ActivateHelpers } from 'hadron-app-registry';
11+
12+
export type DataModelingStoreOptions = Record<string, unknown>;
13+
14+
export type DataModelingStoreServices = {
15+
preferences: PreferencesAccess;
16+
connections: ConnectionsService;
17+
instanceManager: MongoDBInstancesManager;
18+
dataModelStorage: DataModelStorageService;
19+
track: TrackFunction;
20+
logger: Logger;
21+
};
22+
23+
export function activateDataModelingStore(
24+
_: DataModelingStoreOptions,
25+
services: DataModelingStoreServices,
26+
{ cleanup }: ActivateHelpers
27+
) {
28+
const cancelControllerRef = { current: null };
29+
const store = createStore(
30+
reducer,
31+
applyMiddleware(
32+
thunk.withExtraArgument({ ...services, cancelControllerRef })
33+
)
34+
);
35+
return { store, deactivate: cleanup };
36+
}

0 commit comments

Comments
 (0)