Skip to content

Commit b42fae4

Browse files
authored
feat: diagram edit handling COMPASS-9312 (#6932)
1 parent 64b9c31 commit b42fae4

File tree

15 files changed

+505
-61
lines changed

15 files changed

+505
-61
lines changed

package-lock.json

Lines changed: 21 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compass-data-modeling/src/components/diagram-editor.tsx

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ import {
1919
spacing,
2020
Button,
2121
palette,
22+
ErrorSummary,
2223
} from '@mongodb-js/compass-components';
2324
import { cancelAnalysis, retryAnalysis } from '../store/analysis-process';
25+
import type { Edit, StaticModel } from '../services/data-model-storage';
26+
import { UUID } from 'bson';
2427

2528
const loadingContainerStyles = css({
2629
width: '100%',
@@ -73,17 +76,29 @@ const modelPreviewStyles = css({
7376
});
7477

7578
const editorContainerStyles = css({
76-
height: 160 + 34 + 16,
7779
display: 'flex',
7880
flexDirection: 'column',
7981
gap: 8,
8082
boxShadow: `0 0 0 2px ${palette.gray.light2}`,
8183
});
8284

83-
const editorContainerApplyButtonStyles = css({
85+
const editorContainerApplyContainerStyles = css({
8486
paddingLeft: 8,
8587
paddingRight: 8,
86-
alignSelf: 'flex-end',
88+
justifyContent: 'flex-end',
89+
gap: spacing[200],
90+
display: 'flex',
91+
width: '100%',
92+
height: spacing[100],
93+
});
94+
95+
const editorContainerPlaceholderButtonStyles = css({
96+
paddingLeft: 8,
97+
paddingRight: 8,
98+
alignSelf: 'flex-start',
99+
display: 'flex',
100+
gap: spacing[200],
101+
paddingTop: spacing[200],
87102
});
88103

89104
const DiagramEditor: React.FunctionComponent<{
@@ -92,18 +107,19 @@ const DiagramEditor: React.FunctionComponent<{
92107
onUndoClick: () => void;
93108
hasRedo: boolean;
94109
onRedoClick: () => void;
95-
model: unknown;
110+
model: StaticModel | null;
111+
editErrors?: string[];
96112
onRetryClick: () => void;
97113
onCancelClick: () => void;
98-
// TODO
99-
onApplyClick: (edit: unknown) => void;
114+
onApplyClick: (edit: Omit<Edit, 'id' | 'timestamp'>) => void;
100115
}> = ({
101116
step,
102117
hasUndo,
103118
onUndoClick,
104119
hasRedo,
105120
onRedoClick,
106121
model,
122+
editErrors,
107123
onRetryClick,
108124
onCancelClick,
109125
onApplyClick,
@@ -118,6 +134,43 @@ const DiagramEditor: React.FunctionComponent<{
118134
}
119135
}, [applyInput]);
120136

137+
const applyPlaceholder =
138+
(type: 'AddRelationship' | 'RemoveRelationship') => () => {
139+
let placeholder = {};
140+
switch (type) {
141+
case 'AddRelationship':
142+
placeholder = {
143+
type: 'AddRelationship',
144+
relationship: {
145+
id: new UUID().toString(),
146+
relationship: [
147+
{
148+
ns: 'db.sourceCollection',
149+
cardinality: 1,
150+
fields: ['field1'],
151+
},
152+
{
153+
ns: 'db.targetCollection',
154+
cardinality: 1,
155+
fields: ['field2'],
156+
},
157+
],
158+
isInferred: false,
159+
},
160+
};
161+
break;
162+
case 'RemoveRelationship':
163+
placeholder = {
164+
type: 'RemoveRelationship',
165+
relationshipId: new UUID().toString(),
166+
};
167+
break;
168+
default:
169+
throw new Error(`Unknown placeholder ${placeholder}`);
170+
}
171+
setApplyInput(JSON.stringify(placeholder, null, 2));
172+
};
173+
121174
const modelStr = useMemo(() => {
122175
return JSON.stringify(model, null, 2);
123176
}, [model]);
@@ -172,6 +225,20 @@ const DiagramEditor: React.FunctionComponent<{
172225
></CodemirrorMultilineEditor>
173226
</div>
174227
<div className={editorContainerStyles} data-testid="apply-editor">
228+
<div className={editorContainerPlaceholderButtonStyles}>
229+
<Button
230+
onClick={applyPlaceholder('AddRelationship')}
231+
data-testid="placeholder-addrelationship-button"
232+
>
233+
Add relationship
234+
</Button>
235+
<Button
236+
onClick={applyPlaceholder('RemoveRelationship')}
237+
data-testid="placeholder-removerelationship-button"
238+
>
239+
Remove relationship
240+
</Button>
241+
</div>
175242
<div>
176243
<CodemirrorMultilineEditor
177244
language="json"
@@ -180,7 +247,8 @@ const DiagramEditor: React.FunctionComponent<{
180247
maxLines={10}
181248
></CodemirrorMultilineEditor>
182249
</div>
183-
<div className={editorContainerApplyButtonStyles}>
250+
<div className={editorContainerApplyContainerStyles}>
251+
{editErrors && <ErrorSummary errors={editErrors} />}
184252
<Button
185253
onClick={() => {
186254
onApplyClick(JSON.parse(applyInput));
@@ -238,6 +306,7 @@ export default connect(
238306
model: diagram
239307
? selectCurrentModel(getCurrentDiagramFromState(state))
240308
: null,
309+
editErrors: diagram?.editErrors,
241310
};
242311
},
243312
{

packages/compass-data-modeling/src/services/data-model-storage.ts

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,81 @@
11
import { z } from '@mongodb-js/compass-user-data';
2+
import type { MongoDBJSONSchema } from 'mongodb-schema';
3+
4+
export const RelationshipSideSchema = z.object({
5+
ns: z.string(),
6+
cardinality: z.number(),
7+
fields: z.array(z.string()),
8+
});
9+
10+
export type RelationshipSide = z.output<typeof RelationshipSideSchema>;
11+
12+
export const RelationshipSchema = z.object({
13+
id: z.string().uuid(),
14+
relationship: z.tuple([RelationshipSideSchema, RelationshipSideSchema]),
15+
isInferred: z.boolean(),
16+
});
17+
18+
export type Relationship = z.output<typeof RelationshipSchema>;
19+
20+
export const StaticModelSchema = z.object({
21+
collections: z.array(
22+
z.object({
23+
ns: z.string(),
24+
jsonSchema: z.custom<MongoDBJSONSchema>((value) => {
25+
const isObject = typeof value === 'object' && value !== null;
26+
return isObject && 'bsonType' in value;
27+
}),
28+
indexes: z.array(z.record(z.unknown())),
29+
shardKey: z.record(z.unknown()).optional(),
30+
displayPosition: z.tuple([z.number(), z.number()]),
31+
})
32+
),
33+
relationships: z.array(RelationshipSchema),
34+
});
35+
36+
export type StaticModel = z.output<typeof StaticModelSchema>;
37+
38+
const EditSchemaBase = z.object({
39+
id: z.string().uuid(),
40+
timestamp: z.string().datetime(),
41+
});
42+
43+
const EditSchemaVariants = z.discriminatedUnion('type', [
44+
z.object({
45+
type: z.literal('SetModel'),
46+
model: StaticModelSchema,
47+
}),
48+
z.object({
49+
type: z.literal('AddRelationship'),
50+
relationship: RelationshipSchema,
51+
}),
52+
z.object({
53+
type: z.literal('RemoveRelationship'),
54+
relationshipId: z.string().uuid(),
55+
}),
56+
]);
57+
58+
export const EditSchema = z.intersection(EditSchemaBase, EditSchemaVariants);
59+
60+
export type Edit = z.output<typeof EditSchema>;
61+
62+
export const validateEdit = (
63+
edit: unknown
64+
): { result: true; errors?: never } | { result: false; errors: string[] } => {
65+
try {
66+
EditSchema.parse(edit);
67+
return { result: true };
68+
} catch (e) {
69+
return {
70+
result: false,
71+
errors: (e as z.ZodError).issues.map(({ path, message }) =>
72+
message === 'Required'
73+
? `'${path}' is required`
74+
: `Invalid field '${path}': ${message}`
75+
),
76+
};
77+
}
78+
};
279

380
export const MongoDBDataModelDescriptionSchema = z.object({
481
id: z.string(),
@@ -11,8 +88,7 @@ export const MongoDBDataModelDescriptionSchema = z.object({
1188
*/
1289
connectionId: z.string().nullable(),
1390

14-
// TODO: define rest of the schema based on arch doc / tech design
15-
edits: z.array(z.unknown()).default([]),
91+
edits: z.array(EditSchema).default([]),
1692
});
1793

1894
export type MongoDBDataModelDescription = z.output<

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import type { Reducer } from 'redux';
22
import { isAction } from './util';
33
import type { DataModelingThunkAction } from './reducer';
4-
import { analyzeDocuments } from 'mongodb-schema';
4+
import { analyzeDocuments, type MongoDBJSONSchema } from 'mongodb-schema';
55
import { getCurrentDiagramFromState } from './diagram';
66
import type { Document } from 'bson';
77
import type { AggregationCursor } from 'mongodb';
8+
import type { Relationship } from '../services/data-model-storage';
89

910
export type AnalysisProcessState = {
1011
currentAnalysisOptions:
@@ -61,9 +62,8 @@ export type AnalysisFinishedAction = {
6162
type: AnalysisProcessActionTypes.ANALYSIS_FINISHED;
6263
name: string;
6364
connectionId: string;
64-
// TODO
65-
schema: Record<string, unknown>;
66-
relations: unknown[];
65+
collections: { ns: string; schema: MongoDBJSONSchema }[];
66+
relations: Relationship[];
6767
};
6868

6969
export type AnalysisFailedAction = {
@@ -157,7 +157,7 @@ export function startAnalysis(
157157
try {
158158
const dataService =
159159
services.connections.getDataServiceForConnection(connectionId);
160-
const schema = await Promise.all(
160+
const collections = await Promise.all(
161161
namespaces.map(async (ns) => {
162162
const sample: AggregationCursor<Document> = dataService.sampleCursor(
163163
ns,
@@ -188,7 +188,7 @@ export function startAnalysis(
188188
type: AnalysisProcessActionTypes.NAMESPACE_SCHEMA_ANALYZED,
189189
namespace: ns,
190190
});
191-
return [ns, schema];
191+
return { ns, schema };
192192
})
193193
);
194194
if (options.automaticallyInferRelations) {
@@ -201,7 +201,7 @@ export function startAnalysis(
201201
type: AnalysisProcessActionTypes.ANALYSIS_FINISHED,
202202
name,
203203
connectionId,
204-
schema: Object.fromEntries(schema),
204+
collections,
205205
relations: [],
206206
});
207207
void services.dataModelStorage.save(

0 commit comments

Comments
 (0)