datajunction-ui 0.0.15 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,367 @@
1
+ import { useContext, useEffect, useRef, useState } from 'react';
2
+ import * as React from 'react';
3
+ import DJClientContext from '../../providers/djclient';
4
+ import { ErrorMessage, Field, Form, Formik } from 'formik';
5
+ import { FormikSelect } from '../AddEditNodePage/FormikSelect';
6
+ import AddItemIcon from '../../icons/AddItemIcon';
7
+ import { displayMessageAfterSubmit } from '../../../utils/form';
8
+ import LoadingIcon from '../../icons/LoadingIcon';
9
+ import CodeMirror from '@uiw/react-codemirror';
10
+ import { langs } from '@uiw/codemirror-extensions-langs';
11
+
12
+ export default function AddComplexDimensionLinkPopover({
13
+ node,
14
+ dimensions,
15
+ existingLink = null,
16
+ isEditMode = false,
17
+ onSubmit,
18
+ }) {
19
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
20
+ const [popoverAnchor, setPopoverAnchor] = useState(false);
21
+ const ref = useRef(null);
22
+
23
+ useEffect(() => {
24
+ const handleClickOutside = event => {
25
+ if (ref.current && !ref.current.contains(event.target)) {
26
+ setPopoverAnchor(false);
27
+ }
28
+ };
29
+ document.addEventListener('click', handleClickOutside, true);
30
+ return () => {
31
+ document.removeEventListener('click', handleClickOutside, true);
32
+ };
33
+ }, [setPopoverAnchor]);
34
+
35
+ const joinTypeOptions = [
36
+ { value: 'left', label: 'LEFT' },
37
+ { value: 'right', label: 'RIGHT' },
38
+ { value: 'inner', label: 'INNER' },
39
+ { value: 'full', label: 'FULL' },
40
+ { value: 'cross', label: 'CROSS' },
41
+ ];
42
+
43
+ const joinCardinalityOptions = [
44
+ { value: 'one_to_one', label: 'ONE TO ONE' },
45
+ { value: 'one_to_many', label: 'ONE TO MANY' },
46
+ { value: 'many_to_one', label: 'MANY TO ONE' },
47
+ { value: 'many_to_many', label: 'MANY TO MANY' },
48
+ ];
49
+
50
+ const handleSubmit = async (values, { setSubmitting, setStatus }) => {
51
+ try {
52
+ // If editing, remove the old link first
53
+ if (isEditMode && existingLink) {
54
+ await djClient.removeComplexDimensionLink(
55
+ node.name,
56
+ existingLink.dimension.name,
57
+ existingLink.role,
58
+ );
59
+ }
60
+
61
+ // Add the new/updated link
62
+ const response = await djClient.addComplexDimensionLink(
63
+ node.name,
64
+ values.dimensionNode,
65
+ values.joinOn.trim(),
66
+ values.joinType || 'left',
67
+ values.joinCardinality || 'many_to_one',
68
+ values.role?.trim() || null,
69
+ );
70
+
71
+ if (response.status === 200 || response.status === 201) {
72
+ setStatus({
73
+ success: `Complex dimension link ${
74
+ isEditMode ? 'updated' : 'added'
75
+ } successfully!`,
76
+ });
77
+ setTimeout(() => {
78
+ setPopoverAnchor(false);
79
+ window.location.reload();
80
+ }, 1000);
81
+ } else {
82
+ setStatus({
83
+ failure:
84
+ response.json?.message ||
85
+ `Failed to ${isEditMode ? 'update' : 'add'} link`,
86
+ });
87
+ }
88
+ } catch (error) {
89
+ setStatus({ failure: error.message });
90
+ } finally {
91
+ setSubmitting(false);
92
+ }
93
+ };
94
+
95
+ return (
96
+ <>
97
+ {isEditMode ? (
98
+ <button
99
+ onClick={() => setPopoverAnchor(!popoverAnchor)}
100
+ style={{
101
+ padding: '0.25rem 0.5rem',
102
+ fontSize: '0.75rem',
103
+ background: '#007bff',
104
+ color: 'white',
105
+ border: 'none',
106
+ borderRadius: '4px',
107
+ cursor: 'pointer',
108
+ marginRight: '0.5rem',
109
+ }}
110
+ >
111
+ Edit
112
+ </button>
113
+ ) : (
114
+ <button
115
+ className="edit_button"
116
+ aria-label="AddComplexDimensionLinkTogglePopover"
117
+ tabIndex="0"
118
+ onClick={() => setPopoverAnchor(!popoverAnchor)}
119
+ title="Add complex dimension link with custom SQL"
120
+ style={{
121
+ marginLeft: '0.5rem',
122
+ padding: '0.25rem 0.5rem',
123
+ fontSize: '0.875rem',
124
+ }}
125
+ >
126
+ <AddItemIcon />
127
+ </button>
128
+ )}
129
+ {popoverAnchor && (
130
+ <>
131
+ {/* Backdrop overlay */}
132
+ <div
133
+ style={{
134
+ position: 'fixed',
135
+ top: 0,
136
+ left: 0,
137
+ right: 0,
138
+ bottom: 0,
139
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
140
+ zIndex: 1000,
141
+ display: 'flex',
142
+ alignItems: 'center',
143
+ justifyContent: 'center',
144
+ padding: '2rem',
145
+ }}
146
+ onClick={() => setPopoverAnchor(false)}
147
+ >
148
+ {/* Modal */}
149
+ <div
150
+ role="dialog"
151
+ aria-label="AddComplexDimensionLinkPopover"
152
+ ref={ref}
153
+ onClick={e => e.stopPropagation()}
154
+ style={{
155
+ backgroundColor: 'white',
156
+ borderRadius: '8px',
157
+ boxShadow: '0 4px 20px rgba(0, 0, 0, 0.15)',
158
+ width: '65%',
159
+ maxWidth: '100%',
160
+ maxHeight: '100%',
161
+ overflow: 'visible',
162
+ padding: '1.5rem',
163
+ textTransform: 'none',
164
+ fontWeight: 'normal',
165
+ position: 'relative',
166
+ zIndex: 1000,
167
+ }}
168
+ >
169
+ <div
170
+ style={{ maxHeight: 'calc(85vh - 4rem)', overflowY: 'auto' }}
171
+ >
172
+ <Formik
173
+ initialValues={{
174
+ dimensionNode: existingLink?.dimension.name || '',
175
+ joinType: existingLink?.join_type || 'left',
176
+ joinOn: existingLink?.join_sql || '',
177
+ joinCardinality:
178
+ existingLink?.join_cardinality || 'many_to_one',
179
+ role: existingLink?.role || '',
180
+ }}
181
+ onSubmit={handleSubmit}
182
+ validate={values => {
183
+ const errors = {};
184
+ if (!values.dimensionNode) {
185
+ errors.dimensionNode = 'Required';
186
+ }
187
+ if (!values.joinOn) {
188
+ errors.joinOn = 'Required';
189
+ }
190
+ return errors;
191
+ }}
192
+ >
193
+ {function Render({ isSubmitting, status, values }) {
194
+ return (
195
+ <Form>
196
+ <h3
197
+ style={{
198
+ margin: '0 0 1rem 0',
199
+ fontSize: '1.25rem',
200
+ fontWeight: '600',
201
+ }}
202
+ >
203
+ {isEditMode ? 'Edit' : 'Add'} Complex Dimension Link
204
+ </h3>
205
+ {displayMessageAfterSubmit(status)}
206
+
207
+ <div style={{ marginBottom: '1rem' }}>
208
+ <ErrorMessage name="dimensionNode" component="span" />
209
+ <label htmlFor="dimensionNode">
210
+ Dimension Node *
211
+ </label>
212
+ {isEditMode ? (
213
+ <div
214
+ style={{
215
+ padding: '0.5rem',
216
+ backgroundColor: '#f5f5f5',
217
+ borderRadius: '4px',
218
+ color: '#666',
219
+ }}
220
+ >
221
+ {existingLink?.dimension.name}
222
+ <small
223
+ style={{
224
+ display: 'block',
225
+ marginTop: '0.25rem',
226
+ fontSize: '0.75rem',
227
+ color: '#999',
228
+ }}
229
+ >
230
+ To link a different dimension node, remove this
231
+ link and create a new one
232
+ </small>
233
+ </div>
234
+ ) : (
235
+ <FormikSelect
236
+ selectOptions={dimensions}
237
+ formikFieldName="dimensionNode"
238
+ placeholder="Select dimension"
239
+ />
240
+ )}
241
+ </div>
242
+
243
+ <div
244
+ style={{
245
+ display: 'flex',
246
+ gap: '1rem',
247
+ marginBottom: '1rem',
248
+ }}
249
+ >
250
+ <div style={{ flex: 1 }}>
251
+ <label htmlFor="joinType">Join Type</label>
252
+ <FormikSelect
253
+ selectOptions={joinTypeOptions}
254
+ formikFieldName="joinType"
255
+ placeholder="Select join type"
256
+ defaultValue={
257
+ values.joinType
258
+ ? joinTypeOptions.find(
259
+ opt => opt.value === values.joinType,
260
+ )
261
+ : null
262
+ }
263
+ />
264
+ </div>
265
+ <div style={{ flex: 1 }}>
266
+ <label htmlFor="joinCardinality">
267
+ Join Cardinality
268
+ </label>
269
+ <FormikSelect
270
+ selectOptions={joinCardinalityOptions}
271
+ formikFieldName="joinCardinality"
272
+ placeholder="Select join cardinality"
273
+ defaultValue={
274
+ values.joinCardinality
275
+ ? joinCardinalityOptions.find(
276
+ opt =>
277
+ opt.value === values.joinCardinality,
278
+ )
279
+ : null
280
+ }
281
+ />
282
+ </div>
283
+ </div>
284
+
285
+ <div style={{ marginBottom: '1rem' }}>
286
+ <ErrorMessage name="joinOn" component="span" />
287
+ <label htmlFor="joinOn">Join SQL *</label>
288
+ <small
289
+ style={{ color: '#6c757d', fontSize: '0.75rem' }}
290
+ >
291
+ Specify the join condition
292
+ </small>
293
+ <Field name="joinOn">
294
+ {({ field, form }) => (
295
+ <div
296
+ role="button"
297
+ tabIndex={0}
298
+ className="relative flex bg-[#282a36]"
299
+ >
300
+ <CodeMirror
301
+ id="joinOn"
302
+ name="joinOn"
303
+ extensions={[langs.sql()]}
304
+ value={field.value?.trim()}
305
+ placeholder="e.g., node_table.dimension_id = dimension_table.id"
306
+ width="100%"
307
+ height="150px"
308
+ style={{
309
+ fontSize: '14px',
310
+ textAlign: 'left',
311
+ }}
312
+ onChange={value => {
313
+ form.setFieldValue('joinOn', value);
314
+ }}
315
+ />
316
+ </div>
317
+ )}
318
+ </Field>
319
+ </div>
320
+
321
+ <div style={{ marginBottom: '1rem' }}>
322
+ <label htmlFor="role">Role (Optional)</label>
323
+ <Field
324
+ type="text"
325
+ name="role"
326
+ id="role"
327
+ placeholder="e.g., birth_date, registration_date"
328
+ style={{
329
+ width: '100%',
330
+ padding: '0.5rem',
331
+ }}
332
+ />
333
+ <small
334
+ style={{ color: '#6c757d', fontSize: '0.75rem' }}
335
+ >
336
+ Optional role if linking the same dimension multiple
337
+ times
338
+ </small>
339
+ </div>
340
+
341
+ <button
342
+ className="add_node"
343
+ type="submit"
344
+ aria-label="SaveComplexDimensionLink"
345
+ disabled={isSubmitting}
346
+ style={{ marginTop: '1rem' }}
347
+ >
348
+ {isSubmitting ? (
349
+ <LoadingIcon />
350
+ ) : isEditMode ? (
351
+ 'Save Changes'
352
+ ) : (
353
+ 'Add Link'
354
+ )}
355
+ </button>
356
+ </Form>
357
+ );
358
+ }}
359
+ </Formik>
360
+ </div>
361
+ </div>
362
+ </div>
363
+ </>
364
+ )}
365
+ </>
366
+ );
367
+ }
@@ -106,7 +106,7 @@ export default function LinkDimensionPopover({
106
106
  initialValues={{
107
107
  column: column.name,
108
108
  node: node.name,
109
- updatedDimensionNodes: '',
109
+ updatedDimensionNodes: dimensionNodes || [],
110
110
  }}
111
111
  onSubmit={handleSubmit}
112
112
  >